Spaces:
Running
Running
Download docs/admin-guide/index.html from Couscouskabyle/iscc-app: direct link, hf CLI and curl.
- Browser
- Download file 326 kB
-
https://huggingface.co/spaces/Couscouskabyle/iscc-app/resolve/main/docs/admin-guide/index.html
- Command line
-
hf download hf://spaces/Couscouskabyle/iscc-app/docs/admin-guide/index.html
-
curl -L -o index.html https://huggingface.co/spaces/Couscouskabyle/iscc-app/resolve/main/docs/admin-guide/index.html
326 kB
<!doctype html>
<html lang="en" data-depth="2">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Sinetti Admin Guide</title>
<style>
/* Field Journal palette — values mirror tokens.py / frontend/src/theme.css */
:root{
--bg:#E9DFC6; --panel:#FBF7EC; --raised:#FFFCF4; --hair:#C9B98A; --hair-soft:#D6C8A8;
--ink:#23271C; --dim:#574F3B; --muted:#655A3E; --accent:#3E6B4F; --warm:#B5613C;
--danger-bg:#F7E0D7; --danger-ink:#7A241C; --danger-border:#E0A992; --surface:#F0ECE0;
--serif:"Newsreader Variable",Georgia,"Times New Roman",serif;
--sans:"Inter Variable",system-ui,-apple-system,"Segoe UI",sans-serif;
--mono:ui-monospace,"Cascadia Mono",Consolas,monospace;
--toc-w:270px; --bar-h:52px;
}
*{box-sizing:border-box}
html{scroll-behavior:smooth;scroll-padding-top:calc(var(--bar-h) + 16px)}
body{margin:0;background:var(--bg);color:var(--ink);font:400 15.5px/1.6 var(--sans)}
a{color:var(--accent)} a:hover{color:var(--warm)}
code{font:0.86em var(--mono);background:var(--surface);border:1px solid var(--hair-soft);border-radius:4px;padding:0 4px}
pre{font:12.5px/1.55 var(--mono);background:#2A2E22;color:#F0E9D6;border-radius:8px;padding:12px 14px;overflow-x:auto;margin:10px 0}
pre code{background:none;border:0;padding:0;color:inherit;font-size:inherit}
/* top bar */
.bar{position:sticky;top:0;z-index:20;height:var(--bar-h);display:flex;align-items:center;gap:18px;
padding:0 20px;background:var(--ink);color:#F0E9D6}
.bar__brand{font:600 1.1rem var(--serif);letter-spacing:.01em;white-space:nowrap}
.bar__brand small{font:400 .78rem var(--sans);opacity:.7;margin-left:8px}
.depth{display:flex;margin-left:auto;border:1px solid #6d6a55;border-radius:999px;overflow:hidden}
.depth button{all:unset;cursor:pointer;padding:5px 14px;font:500 .8rem var(--sans);color:#d9d2ba;white-space:nowrap}
.depth button[aria-pressed="true"]{background:#F0E9D6;color:var(--ink)}
.depth button:focus-visible{outline:2px solid var(--warm);outline-offset:-2px}
.bar__tools button{all:unset;cursor:pointer;font:.78rem var(--sans);color:#d9d2ba;padding:4px 8px;border-radius:6px}
.bar__tools button:hover{background:#3a3e30}
/* layout */
.shell{display:grid;grid-template-columns:var(--toc-w) minmax(0,1fr);max-width:1320px;margin:0 auto}
nav.toc{position:sticky;top:var(--bar-h);align-self:start;height:calc(100vh - var(--bar-h));overflow:auto;
padding:22px 14px 40px 20px;border-right:1px solid var(--hair);font-size:.84rem}
nav.toc h4{font:600 .7rem var(--sans);letter-spacing:.09em;text-transform:uppercase;color:var(--muted);margin:18px 0 6px}
nav.toc a{display:flex;gap:7px;align-items:start;padding:5px 8px;border-radius:5px;color:var(--ink);text-decoration:none;line-height:1.35}
nav.toc a:hover{background:var(--panel)}
nav.toc a.on{background:var(--topic-soft);box-shadow:inset 3px 0 var(--topic);color:var(--ink)}
nav.toc .guideIcon{width:17px;height:17px;color:var(--topic);margin-top:1px}
main{padding:34px 44px 120px;min-width:0}
/* type */
h1{font:600 2.3rem/1.15 var(--serif);margin:0 0 6px}
h2{font:600 1.65rem/1.2 var(--serif);margin:0 0 4px;display:flex;align-items:center;gap:10px}
h2 .n{font:600 .85rem var(--sans);color:var(--topic);padding:0}
h3{font:600 1.12rem/1.3 var(--serif);margin:26px 0 6px}
h4{font:600 .92rem var(--sans);margin:18px 0 4px}
p{margin:8px 0} ul,ol{margin:8px 0;padding-left:22px} li{margin:3px 0}
.lede{font:400 1.08rem/1.55 var(--serif);color:var(--dim);max-width:70ch}
.where{font:.8rem var(--sans);color:var(--muted);margin:0 0 10px}
.where b{color:var(--ink);font-weight:600}
section.area{background:var(--panel);border:1px solid var(--hair);border-radius:12px;padding:26px 30px;margin:26px 0;
box-shadow:0 2px 8px rgba(70,50,20,.06)}
section.plain{margin:26px 0}
.topic--neutral{--topic:#3E6B4F;--topic-soft:#E6EFE8;--topic-line:#B9CFBF}
.topic--people{--topic:#71507B;--topic-soft:#EFE7F3;--topic-line:#D2C1DB}
.topic--data{--topic:#276C68;--topic-soft:#E4F1ED;--topic-line:#B5D3C9}
.topic--app{--topic:#385F8B;--topic-soft:#E6EDF6;--topic-line:#BACCE0}
section.area{border-top:3px solid var(--topic);background:linear-gradient(var(--topic-soft),var(--panel) 98px)}
.guideIcon{flex:none;vertical-align:middle}
.guideIcon .iconWash{opacity:0}
.topicIcon,.guideTitleIcon{display:inline-flex;align-items:center;justify-content:center;flex:none;border:1px solid var(--topic-line);background:var(--topic-soft);color:var(--topic)}
.topicIcon{width:38px;height:38px;border-radius:10px}
.guideTitleIcon{width:48px;height:48px;margin-right:12px;border-radius:12px}
.guideTitleIcon svg{width:26px;height:26px}
/* "in short" — the summary layer */
.short{background:var(--raised);border-left:4px solid var(--accent);border-radius:0 8px 8px 0;padding:10px 16px;margin:12px 0 4px}
.short b.tag{display:block;font:600 .68rem var(--sans);letter-spacing:.1em;text-transform:uppercase;color:var(--accent);margin-bottom:2px}
.short p{margin:4px 0}
section.area .short{border-left-color:var(--topic)}
section.area .short b.tag{color:var(--topic)}
/* callouts */
.note,.warn,.rule{border-radius:8px;padding:9px 14px;margin:12px 0;font-size:.92rem}
.note{background:#E7EFF7;border:1px solid #BECDDF;color:#274765}
.warn{background:var(--danger-bg);border:1px solid var(--danger-border);color:var(--danger-ink)}
.rule{background:#E6EFE8;border:1px solid #B9CFBF;color:#294B34}
.note b:first-child,.warn b:first-child,.rule b:first-child{font-weight:600}
/* tables */
.tw{overflow-x:auto;margin:12px 0}
table{border-collapse:collapse;width:100%;font-size:.88rem;background:var(--raised)}
th,td{border:1px solid var(--hair-soft);padding:6px 10px;text-align:left;vertical-align:top}
th{background:var(--surface);font:600 .74rem var(--sans);letter-spacing:.04em;text-transform:uppercase;color:var(--dim)}
/* procedures — the detail layer */
details.proc{border:1px solid var(--hair);border-radius:9px;background:var(--raised);margin:10px 0}
details.proc>summary{cursor:pointer;list-style:none;padding:10px 14px;font:600 .95rem var(--sans);display:flex;gap:10px;align-items:center}
details.proc>summary::-webkit-details-marker{display:none}
details.proc>summary::before{content:"▸";color:var(--warm);transition:transform .15s}
details.proc[open]>summary::before{transform:rotate(90deg)}
details.proc>summary .k{margin-left:auto;font:500 .7rem var(--sans);color:var(--muted);text-transform:uppercase;letter-spacing:.06em}
details.proc .body{padding:2px 18px 14px 34px;border-top:1px solid var(--hair-soft)}
ol.steps{counter-reset:s;list-style:none;padding-left:0}
ol.steps>li{counter-increment:s;position:relative;padding-left:34px;margin:9px 0}
ol.steps>li::before{content:counter(s);position:absolute;left:0;top:1px;width:22px;height:22px;border-radius:50%;
background:var(--accent);color:#fff;font:600 .72rem/22px var(--sans);text-align:center}
.expect{color:var(--dim);font-size:.9rem}
.expect::before{content:"Expect: ";font-weight:600;color:var(--accent)}
/* figures */
figure{margin:16px 0;background:var(--raised);border:1px solid var(--hair-soft);border-radius:10px;padding:14px 14px 8px;color:var(--ink)}
figure svg{display:block;max-width:100%;height:auto;margin:0 auto;font-family:var(--sans)}
figcaption{font:.82rem/1.45 var(--sans);color:var(--dim);margin-top:8px;padding-top:8px;border-top:1px dashed var(--hair-soft)}
/* area cards on the summary page */
.cards{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:12px;margin:14px 0}
.card{display:block;text-decoration:none;color:var(--ink);background:var(--raised);border:1px solid var(--hair);border-radius:10px;padding:12px 14px}
.card{position:relative;border-top:3px solid var(--topic);padding-right:54px}
.card:hover{border-color:var(--accent);color:var(--ink);box-shadow:0 3px 10px rgba(70,50,20,.10)}
.card .n{font:600 .72rem var(--sans);color:var(--topic);letter-spacing:.08em}
.cardIcon{display:flex;align-items:center;justify-content:center;position:absolute;right:12px;top:12px;width:32px;height:32px;border-radius:8px;background:var(--topic-soft);color:var(--topic)}
.card h4{margin:2px 0 4px;font:600 1rem var(--serif)}
.card p{margin:0;font-size:.85rem;color:var(--dim);line-height:1.45}
.card .freq{display:inline-block;margin-top:7px;font:.7rem var(--sans);color:var(--muted);border:1px solid var(--hair-soft);border-radius:999px;padding:1px 8px}
/* depth switch: d2 = area layer, d3 = procedure layer */
html[data-depth="1"] .d2, html[data-depth="1"] .d3, html[data-depth="2"] .d3{display:none !important}
.depthhint{font:.78rem var(--sans);color:var(--muted);margin-top:10px}
html[data-depth="3"] .depthhint--more, html[data-depth="1"] .depthhint--less{display:none}
/* diagram vocabulary — page-level so every figure shares it */
svg .bx{fill:var(--raised);stroke:var(--dim);stroke-width:1.2}
svg .bx2{fill:var(--surface);stroke:var(--hair);stroke-width:1.2}
svg .app{fill:#E6EFE8;stroke:var(--accent);stroke-width:2}
svg .bad{fill:var(--danger-bg);stroke:var(--danger-border);stroke-width:1.2}
svg .gate{fill:var(--warm)}
svg .gt{fill:#fff;font-weight:600;font-size:12px;text-anchor:middle;letter-spacing:.06em}
svg .t{fill:var(--ink);font-size:14px;font-weight:600;text-anchor:middle}
svg .s{fill:var(--dim);font-size:11.5px;text-anchor:middle}
svg .al{fill:var(--muted);font-size:11px;text-anchor:middle;font-style:italic}
svg .ln{fill:var(--accent);font-size:11px;font-weight:700;letter-spacing:.1em}
svg .ar{stroke:var(--dim);stroke-width:1.4;fill:none}
svg .ard{stroke:var(--dim);stroke-width:1.4;fill:none;stroke-dasharray:5 4}
svg .ah{fill:var(--dim)}
svg .l{text-anchor:start} svg .r{text-anchor:end}
.legend{display:flex;flex-wrap:wrap;gap:6px 18px;font-size:.8rem;color:var(--dim);margin-top:6px}
.legend i{display:inline-block;width:26px;height:12px;border-radius:6px;vertical-align:-1px;margin-right:6px}
.legend .g{background:var(--warm)} .legend .a{background:#E6EFE8;border:2px solid var(--accent)} .legend .b{background:var(--raised);border:1px solid var(--dim)}
.pill{display:inline-block;font:600 .68rem var(--sans);letter-spacing:.05em;text-transform:uppercase;border-radius:999px;padding:1px 8px;vertical-align:1px}
.pill.ui{background:#E6EFE8;color:var(--accent);border:1px solid #B9CFBF}
.pill.cli{background:#2A2E22;color:#F0E9D6}
.pill.gap{background:var(--danger-bg);color:var(--danger-ink);border:1px solid var(--danger-border)}
kbd{font:600 .8em var(--sans);background:var(--raised);border:1px solid var(--hair);border-bottom-width:2px;border-radius:5px;padding:0 6px;white-space:nowrap}
@media (max-width:900px){
code{overflow-wrap:anywhere}
pre code{overflow-wrap:normal}
.shell{grid-template-columns:1fr} nav.toc{display:none} main{padding:22px 16px 80px}
section.area{padding:18px 16px} .bar__brand small{display:none}
.bar{gap:8px;padding:0 10px} .bar__brand{font-size:.92rem;min-width:0;overflow:hidden;text-overflow:ellipsis}
.depth{flex:none} .depth button{padding:5px 8px;font-size:.7rem} .bar__tools{display:none}
}
@media print{
.bar,nav.toc{display:none} .shell{display:block} body{background:#fff;font-size:11pt}
section.area{break-inside:auto;box-shadow:none} details.proc{break-inside:avoid}
html[data-depth] .d2, html[data-depth] .d3{display:revert !important}
}
/* Material names diagram (section 10) */
.mn{--mnc:#7A4A1E;--mnc-soft:#F6EBDD;--mnc-line:#E2C9A6;margin:14px 0 6px}
.mn .flow{display:grid;grid-template-columns:1fr 1.7fr 1.25fr;gap:14px;margin:20px 0 10px;align-items:stretch}
.mn .col h3{font:600 12px/1 var(--sans);letter-spacing:.08em;text-transform:uppercase;color:var(--muted);margin:0 0 8px}
.mn .box{background:var(--raised);border:1px solid var(--hair-soft);border-radius:8px;padding:10px 12px;margin-bottom:8px;color:var(--ink)}
.mn .box b{color:var(--ink)}
.mn .src small,.mn .surf small{display:block;color:var(--dim);font-size:12.5px}
.mn .step{position:relative;border-left:4px solid var(--topic);background:var(--topic-soft);border-color:var(--topic-line);border-left-color:var(--topic)}
.mn .step.code{background:var(--mnc-soft);border-color:var(--mnc-line);border-left-color:var(--mnc)}
.mn .step .q{font:600 15px/1.3 var(--serif);color:var(--ink)}
.mn .step .who{float:right;font:600 11px/1 var(--sans);padding:3px 7px;border-radius:99px;background:var(--raised);border:1px solid var(--topic-line);color:var(--topic)}
.mn .step.code .who{border-color:var(--mnc-line);color:var(--mnc)}
.mn .step .ans{font-size:13px;color:var(--dim);margin-top:3px}
.mn .step .ex{font:12.5px var(--mono);color:var(--ink);margin-top:6px;background:var(--raised);border-radius:5px;padding:4px 7px;display:inline-block}
.mn .merge{display:flex;align-items:center;gap:10px;margin:8px 0 6px;background:var(--raised);border:1px solid var(--mnc-line);border-radius:6px;padding:8px 10px}
.mn .merge .raws{display:flex;flex-direction:column;gap:3px}
.mn .merge .raws code{font-size:12px;color:var(--ink)}
.mn .merge .into{font-size:18px;color:var(--mnc)}
.mn .merge .one code{font-weight:600;color:var(--ink)}
.mn .merge .one small{display:block;font-size:11.5px;color:var(--dim)}
.mn .how{margin:4px 0 0;padding-left:18px;font-size:12.5px;color:var(--dim)}
.mn .how li{margin-bottom:2px}
.mn .how b{color:var(--ink)}
.mn .ladderTitle{font:600 12px/1 var(--sans);letter-spacing:.08em;text-transform:uppercase;color:var(--muted);margin:22px 0 8px}
.mn .ladder{display:flex;align-items:stretch;gap:8px;flex-wrap:wrap}
.mn .rung{flex:1;min-width:130px;background:var(--raised);border:1px solid var(--hair-soft);border-radius:8px;padding:10px 12px;color:var(--ink)}
.mn .rung b{display:block;font:600 24px/1.1 var(--serif);color:var(--ink)}
.mn .rung span{font-size:12.5px;color:var(--dim)}
.mn .step2{align-self:center;color:var(--muted)}
.mn .ladderNote{font-size:13px;color:var(--dim);margin:10px 0 18px}
.mn .ladderNote b,.mn .ladderNote i{color:var(--ink)}
.mn .side{display:flex;gap:12px;align-items:flex-start;margin-top:10px;padding:10px 12px;border:1px dashed var(--hair);border-radius:8px;background:var(--surface)}
.mn .rung--side{flex:0 0 190px;background:#E6EDF6;border-color:#BACCE0}
.mn .side p{margin:0;font-size:13px;color:var(--dim)}
.mn .side p b,.mn .side p i{color:var(--ink)}
.mn .nestLegend{display:flex;flex-wrap:wrap;gap:16px;font-size:12.5px;color:var(--dim);margin:0 0 10px}
.mn .nestLegend span{display:inline-flex;align-items:center;gap:6px}
.mn .nestLegend .sw.k{background:#EFE7F3;border:1px solid #D2C1DB}
.mn .nestLegend .sw.f{background:#E4F1ED;border:1px solid #B5D3C9}
.mn .nestLegend .sw.g{background:#F6EBDD;border:1px solid #E2C9A6}
.mn .nestLegend .sw.w{background:var(--raised);border:1px solid var(--hair-soft)}
.mn .chipEx,.mn .chip{font:600 11px/1 var(--sans);font-style:normal;padding:3px 7px;border-radius:99px;background:#D7EDE6;border:1px solid #A8D6C9;color:#1F6B5A;white-space:nowrap}
.mn .chip{margin-left:6px;vertical-align:1px}
.mn .prod .chip{background:#E1E1F4;border-color:#B3B3DE;color:#3E3E8A}
.mn .chip.none{opacity:.85}
.mn .nest{display:grid;grid-template-columns:1fr 1fr;gap:14px}
.mn .kind{background:#EFE7F3;border:1px solid #D2C1DB;border-radius:12px;padding:10px 12px 12px}
.mn .kh{font:600 13px/1 var(--sans);letter-spacing:.08em;text-transform:uppercase;color:#71507B;margin:2px 0 10px}
.mn .fam{background:#E4F1ED;border:1px solid #B5D3C9;border-radius:10px;padding:10px 10px 8px;margin-bottom:8px}
.mn .prod .fam{background:#E6EDF6;border-color:#BACCE0}
.mn .fh{font:600 16px/1.2 var(--serif);color:var(--ink);margin-bottom:8px}
.mn .fh b{color:var(--ink)}
.mn .fh small,.mn .gh small{display:block;font:12px var(--sans);color:var(--dim);margin-top:2px}
.mn .grp{background:#F6EBDD;border:1px solid #E2C9A6;border-radius:8px;padding:8px 9px;margin-bottom:7px}
.mn .gh{font:600 13px/1.35 var(--sans);color:var(--ink);margin-bottom:6px}
.mn .w{display:flex;justify-content:space-between;gap:8px;align-items:baseline;background:var(--raised);border:1px solid var(--hair-soft);border-radius:5px;padding:3px 7px;margin-bottom:3px}
.mn .w code{font-size:11.5px;color:var(--ink);overflow-wrap:anywhere}
.mn .w em{font:600 11.5px var(--sans);font-style:normal;color:var(--dim);white-space:nowrap}
.mn .more{font-size:12px;color:var(--muted);font-style:italic;padding:2px 4px}
.mn .arrow{text-align:center;color:var(--muted);font-size:13px;line-height:1;margin:-2px 0 6px}
.mn .surf .tags{margin-top:5px}
.mn .tag{display:inline-block;font:600 10.5px/1 var(--sans);padding:3px 6px;border-radius:4px;margin:0 3px 3px 0;color:var(--ink)}
.mn .t1{background:#E4F1ED;border:1px solid #B5D3C9}
.mn .t2{background:#E6EDF6;border:1px solid #BACCE0}
.mn .t3{background:#F6EBDD;border:1px solid #E2C9A6}
.mn .t4{background:#EFE7F3;border:1px solid #D2C1DB}
.mn .legend{display:flex;gap:18px;flex-wrap:wrap;font-size:12.5px;color:var(--dim);margin:6px 0 18px}
.mn .legend span{display:inline-flex;align-items:center;gap:6px}
.mn .sw{width:14px;height:14px;border-radius:3px;display:inline-block}
.mn table{width:100%;border-collapse:collapse;font-size:13.5px;margin-top:6px}
.mn th,.mn td{text-align:left;padding:8px 10px;border-bottom:1px solid var(--hair-soft);vertical-align:top;color:var(--ink)}
.mn th{font:600 11.5px var(--sans);letter-spacing:.06em;text-transform:uppercase;color:var(--muted);background:var(--surface)}
.mn .rule{background:var(--topic-soft);border:1px solid var(--topic-line);border-radius:8px;padding:10px 14px;margin-top:16px;font-size:14px;color:var(--ink)}
.mn .flow{grid-template-columns:minmax(0,.9fr) minmax(0,1.6fr) minmax(0,1.2fr)}
.mn .col,.mn .box,.mn .step{min-width:0}
.mn code{overflow-wrap:anywhere}
.mn .ladder{flex-wrap:wrap}
@media (max-width:1180px){.mn .flow{grid-template-columns:minmax(0,1fr)}.mn .nest{grid-template-columns:minmax(0,1fr)}}
/* Company names diagram (section 10) */
.mn .t5{background:#F3EBD2;border:1px solid #DCC48A}
.mn .t6{background:#E9ECE0;border:1px solid #C3CBA8}
.mn .chain{display:grid;grid-template-columns:minmax(0,1.1fr) auto minmax(0,1fr);gap:10px;align-items:center;margin:8px 0 10px}
.mn .chain.three{grid-template-columns:minmax(0,1.2fr) auto minmax(0,1fr) auto minmax(0,.8fr)}
.mn .chain .link{font-size:18px;color:var(--muted);text-align:center}
.mn .chain .node{background:var(--raised);border:1px solid var(--hair-soft);border-radius:8px;padding:8px 10px;min-width:0}
.mn .chain .node b{display:block;font:600 14px/1.3 var(--serif);color:var(--ink)}
.mn .chain .node small{display:block;color:var(--dim);font-size:12px;margin-top:2px}
.mn .chain .node code{display:block;margin:2px 0;font-size:11.5px;overflow-wrap:anywhere}
.mn .cbbox{margin-top:16px;border:1px solid var(--topic-line);background:var(--topic-soft);border-radius:10px;padding:12px 14px}
.mn .cbbox h3{margin:0 0 4px;font:600 13px/1.3 var(--sans);letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}
.mn .links{margin-top:8px;background:var(--surface);border:1px dashed var(--hair);border-radius:8px;padding:8px 10px;font-size:13px;color:var(--dim)}
.mn .links b{color:var(--ink)}
.mn .grp .gh .badge{font:700 10px var(--sans);letter-spacing:.07em;text-transform:uppercase;padding:2px 6px;border-radius:4px;margin-left:6px;background:#E1E1F4;border:1px solid #B3B3DE;color:#3E3E8A}
</style>
</head>
<body>
<div class="bar">
<div class="bar__brand">Sinetti Admin Guide<small id="asof">operator prose · keep in the same commit as the product</small></div>
<div class="depth" role="group" aria-label="Reading depth">
<button data-d="1" aria-pressed="false" title="One-page summary: what each area is for">Summary</button>
<button data-d="2" aria-pressed="true" title="How each area works, with schemas">How it works</button>
<button data-d="3" aria-pressed="false" title="Everything, including step-by-step procedures">Procedures</button>
</div>
<div class="bar__tools">
<button id="openAll" class="d3" title="Expand every procedure">Expand all</button>
<button id="closeAll" class="d3" title="Collapse every procedure">Collapse all</button>
</div>
</div>
<div class="shell">
<nav class="toc" id="toc" aria-label="Contents"></nav>
<main id="main">
<svg width="0" height="0" style="position:absolute" aria-hidden="true"><defs>
<marker id="ah" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="ah" d="M0 0L10 5L0 10z"/></marker>
</defs></svg>
<div data-toc-group="The big picture"></div>
<section class="plain topic--neutral" id="start">
<h1><span class="guideTitleIcon"><svg class="guideIcon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d='M4 4.5A2 2 0 0 1 6 3h13v16H6a2 2 0 0 0-2 2z'/><path d='M4 19.5A2 2 0 0 1 6 18h13'/></svg></span>Sinetti Admin Guide</h1>
<p class="lede">Everything the Sinetti admin controls, from the one-page picture down to the exact command. Written for the person who holds the admin login, the deploy desk and the scheduled jobs.</p>
<h2 style="margin-top:26px"><span class="topicIcon"><svg class="guideIcon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d='M4 4.5A2 2 0 0 1 6 3h13v16H6a2 2 0 0 0-2 2z'/><path d='M4 19.5A2 2 0 0 1 6 18h13'/></svg></span><span class="n">0</span>How to read this guide</h2>
<p>The guide has three depths. Use the switch in the top bar; your choice is remembered.</p>
<div class="tw"><table>
<tr><th style="width:150px">Depth</th><th>What you get</th><th style="width:150px">Reading time</th></tr>
<tr><td><b>Summary</b></td><td>The whole-system schema and, for every area, a short <i>In short</i> box: what it is, why it exists, what you do there.</td><td>≈ 10 minutes</td></tr>
<tr><td><b>How it works</b></td><td>Adds the mechanism for each area: a schema, the moving parts, the rules that bite, what the screens show.</td><td>≈ 1 hour</td></tr>
<tr><td><b>Procedures</b></td><td>Adds step-by-step procedures with exact screens, buttons, commands, what you should see, and how to recover.</td><td>Reference — open what you need</td></tr>
</table></div>
<p class="d2">Marks used throughout: <span class="pill ui">Screen</span> a task done in the app's Admin menu · <span class="pill cli">Desk</span> a task done from a terminal on the operator machine · <span class="pill gap">Gap</span> something the product does not do yet, stated plainly so nobody assumes otherwise.</p>
<p class="d2">Names follow the project vocabulary: <b>Sinetti admin</b> = you (a login with the admin flag, plus access to the operator machine); <b>customer</b> = an organisation under a bilateral agreement; <b>member</b> = a customer's user; <b>the app</b> = the production Hugging Face Space; <b>operator machine</b> = the Windows PC that holds the repository, the deploy desk and the local scheduled tasks.</p>
<div class="note d2"><b>Where facts come from.</b> Code wins. This file is the map; the runbooks under <code>docs/</code> are the territory. An admin-behaviour change must update the matching runbook, this guide, and any in-app catalog/copy in the same commit (see <code>.cursor/rules/admin-operator-docs-freshness.mdc</code>). Numbers that live outside the repository (live job settings, secrets) are described by where to read them — Environment, Data ops, Sources freshness — never quoted from memory. A cheap mechanical checklist lives in <code>tests/test_admin_operator_docs.py</code>.</div>
</section>
<section class="area topic--neutral" id="picture">
<h2><span class="topicIcon"><svg class="guideIcon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx='6' cy='6' r='2'/><circle cx='6' cy='18' r='2'/><circle cx='18' cy='9' r='2'/><path d='M6 8v8'/><path d='M8 6h5a3 3 0 0 1 3 3'/></svg></span><span class="n">1</span>The whole system in one page</h2>
<div class="short"><b class="tag">In short</b>
<p>Three streams meet in one app: <b>data</b> (what customers see), <b>code</b> (what the app does) and <b>people</b> (who gets in). Each stream runs mostly by itself, and each has exactly one gate where nothing moves until the Sinetti admin decides: <b>promote</b> a data release, <b>approve</b> a deploy, <b>grant or withdraw</b> access. Everything else in this guide is detail about one of those three streams, or about watching them.</p>
</div>
<figure>
<svg viewBox="0 0 1100 560" role="img" aria-label="Three streams — data, code and people — flow left to right into the Sinetti app. Each stream passes one admin-controlled gate: Promote, Approve, Access.">
<!-- DATA lane -->
<text class="ln l" x="20" y="32">DATA — what customers see</text>
<rect class="bx" x="20" y="44" width="130" height="56" rx="8"/><text class="t" x="85" y="68">Public registers</text><text class="s" x="85" y="86">ISCC · REDcert · INS…</text>
<rect class="bx" x="206" y="44" width="130" height="56" rx="8"/><text class="t" x="271" y="68">Refresh jobs</text><text class="s" x="271" y="86">nightly + weekly</text>
<rect class="bx" x="392" y="44" width="130" height="56" rx="8"/><text class="t" x="457" y="68">Candidate</text><text class="s" x="457" y="86">private dataset repo</text>
<rect class="gate" x="578" y="54" width="100" height="36" rx="18"/><text class="gt" x="628" y="77">PROMOTE</text>
<rect class="bx" x="734" y="44" width="130" height="56" rx="8"/><text class="t" x="799" y="68">Active release</text><text class="s" x="799" y="86">pointer + 2 previous</text>
<path class="ar" d="M150 72H204" marker-end="url(#ah)"/><text class="al" x="178" y="64">fetch</text>
<path class="ar" d="M336 72H390" marker-end="url(#ah)"/><text class="al" x="364" y="64">publish</text>
<path class="ar" d="M522 72H576" marker-end="url(#ah)"/><text class="al" x="550" y="64">review</text>
<path class="ar" d="M678 72H732" marker-end="url(#ah)"/><text class="al" x="706" y="64">activate</text>
<path class="ar" d="M864 72H918" marker-end="url(#ah)"/><text class="al" x="892" y="64">serves</text>
<rect class="bx2" x="392" y="128" width="130" height="40" rx="8"/><text class="t" x="457" y="153">Trust Review</text>
<path class="ard" d="M457 100V126" marker-end="url(#ah)"/>
<text class="al l" x="532" y="153">checks every candidate · advisory findings land in Admin → Data Trust</text>
<!-- CODE lane -->
<text class="ln l" x="20" y="202">CODE — what the app does</text>
<rect class="bx" x="20" y="214" width="130" height="56" rx="8"/><text class="t" x="85" y="238">main branch</text><text class="s" x="85" y="256">merged work</text>
<rect class="bx" x="206" y="214" width="130" height="56" rx="8"/><text class="t" x="271" y="238">Pinned commit</text><text class="s" x="271" y="256">on the deploy desk</text>
<rect class="bx" x="392" y="214" width="130" height="56" rx="8"/><text class="t" x="457" y="238">Release gate</text><text class="s" x="457" y="256">full tests · 45–90 min</text>
<rect class="gate" x="578" y="224" width="100" height="36" rx="18"/><text class="gt" x="628" y="247">APPROVE</text>
<rect class="bx" x="734" y="214" width="130" height="56" rx="8"/><text class="t" x="799" y="238">Publish</text><text class="s" x="799" y="256">rebuild ≈ 8 min · smoke</text>
<path class="ar" d="M150 242H204" marker-end="url(#ah)"/><text class="al" x="178" y="234">pin</text>
<path class="ar" d="M336 242H390" marker-end="url(#ah)"/><text class="al" x="364" y="234">test</text>
<path class="ar" d="M522 242H576" marker-end="url(#ah)"/><text class="al" x="550" y="234">passed</text>
<path class="ar" d="M678 242H732" marker-end="url(#ah)"/><text class="al" x="706" y="234">live</text>
<path class="ar" d="M864 242H918" marker-end="url(#ah)"/><text class="al" x="892" y="234">deploys</text>
<!-- PEOPLE lane -->
<text class="ln l" x="20" y="372">PEOPLE — who gets in</text>
<rect class="bx" x="20" y="384" width="130" height="56" rx="8"/><text class="t" x="85" y="408">Agreement</text><text class="s" x="85" y="426">signed bilaterally</text>
<rect class="bx" x="206" y="384" width="130" height="56" rx="8"/><text class="t" x="271" y="408">Organisation</text><text class="s" x="271" y="426">plan · seats · limits</text>
<rect class="bx" x="392" y="384" width="130" height="56" rx="8"/><text class="t" x="457" y="408">Invite</text><text class="s" x="457" y="426">member sets password</text>
<rect class="gate" x="578" y="394" width="100" height="36" rx="18"/><text class="gt" x="628" y="417">ACCESS</text>
<rect class="bx" x="734" y="384" width="130" height="56" rx="8"/><text class="t" x="799" y="408">Every request</text><text class="s" x="799" y="426">plan · rate · budget</text>
<path class="ar" d="M150 412H204" marker-end="url(#ah)"/><text class="al" x="178" y="404">configure</text>
<path class="ar" d="M336 412H390" marker-end="url(#ah)"/><text class="al" x="364" y="404">invite</text>
<path class="ar" d="M522 412H576" marker-end="url(#ah)"/><text class="al" x="550" y="404">login</text>
<path class="ar" d="M678 412H732" marker-end="url(#ah)"/><text class="al" x="706" y="404">allowed</text>
<path class="ar" d="M864 412H918" marker-end="url(#ah)"/><text class="al" x="892" y="404">checked</text>
<!-- APP + STATE -->
<rect class="app" x="920" y="44" width="160" height="396" rx="10"/>
<text class="t" x="1000" y="226" style="font-size:17px">Sinetti app</text><text class="s" x="1000" y="246">Hugging Face Space</text><text class="s" x="1000" y="264">login-gated</text><text class="s" x="1000" y="280">no anonymous view</text>
<rect class="bx2" x="920" y="486" width="160" height="56" rx="8"/><text class="t" x="1000" y="510">Persistent state</text><text class="s" x="1000" y="528">customer work · history</text>
<path class="ar" d="M1000 442V484" marker-start="url(#ah)" marker-end="url(#ah)"/><text class="al r" x="990" y="468">reads · writes</text>
</svg>
<figcaption>Three streams, one app. Warm pills are the only points where a human decision is required. Data refreshes itself but never reaches customers until promoted; code never reaches production without a passed gate and a recorded approval; nobody logs in without an organisation you configured. Customer work (watchlists, saved views, alerts, audit trails) lives in persistent state, apart from all three streams — which is why a deploy or a data promotion never touches it.
<div class="legend"><span><i class="g"></i>You decide</span><span><i class="a"></i>Production</span><span><i class="b"></i>Automatic step</span></div>
</figcaption>
</figure>
<h3>The areas, and where each is explained</h3>
<div class="cards">
<a class="card topic--people" href="#logins"><span class="cardIcon"><svg class="guideIcon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d='M12 3l7 3v5c0 4.4-3 8-7 10-4-2-7-5.6-7-10V6z'/></svg></span><span class="n">PEOPLE · 2</span><h4>Logins, MFA and sessions</h4><p>Who can sign in, how admins prove identity twice, how to cut a session off.</p><span class="freq">on event</span></a>
<a class="card topic--people" href="#customers"><span class="cardIcon"><svg class="guideIcon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx='9' cy='8' r='3.2'/><path d='M3 20c0-3.3 2.7-5 6-5s6 1.7 6 5'/><path d='M16 5.2a3.2 3.2 0 0 1 0 5.6M22 20c0-2.6-1.6-4.2-4-4.8'/></svg></span><span class="n">PEOPLE · 3</span><h4>Customers, plans and seats</h4><p>Turn a signed agreement into an organisation with features, registers and seats.</p><span class="freq">per customer</span></a>
<a class="card topic--people" href="#lifecycle"><span class="cardIcon"><svg class="guideIcon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d='M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4'/><path d='M16 17l5-5-5-5'/><path d='M21 12H9'/></svg></span><span class="n">PEOPLE · 4</span><h4>Pause, offboard, delete</h4><p>Four ways to withdraw access, from reversible to permanent.</p><span class="freq">on event</span></a>
<a class="card topic--people" href="#limits"><span class="cardIcon"><svg class="guideIcon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d='M4 21v-7M4 10V3M12 21v-9M12 8V3M20 21v-5M20 12V3M1 14h6M9 8h6M17 16h6'/></svg></span><span class="n">PEOPLE · 5</span><h4>Limits: AI, rate, export</h4><p>Monthly AI budgets in dollars; request-rate and CSV-export ceilings; the export audit.</p><span class="freq">monthly review</span></a>
<a class="card topic--people" href="#isolation"><span class="cardIcon"><svg class="guideIcon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d='M12 3 2 8l10 5 10-5z'/><path d='M2 12l10 5 10-5'/><path d='M2 16l10 5 10-5'/></svg></span><span class="n">PEOPLE · 6</span><h4>Customer isolation</h4><p>What is private per member, shared per organisation, or global — and why admin cannot cross it.</p><span class="freq">know it</span></a>
<a class="card topic--data" href="#refresh"><span class="cardIcon"><svg class="guideIcon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle class='iconWash' cx='12' cy='12' r='9' fill='url(#iconWashGold)' stroke='none'/><circle cx='12' cy='12' r='9'/><path d='M3 12h18'/><path d='M12 3a14 14 0 0 1 0 18'/><path d='M12 3a14 14 0 0 0 0 18'/></svg></span><span class="n">DATA · 7</span><h4>Where data comes from</h4><p>One nightly cloud job and a set of weekly tasks on the operator machine.</p><span class="freq">daily glance</span></a>
<a class="card topic--data" href="#releases"><span class="cardIcon"><svg class="guideIcon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d='M12 3v12'/><path d='m7 10 5 5 5-5'/><path d='M5 21h14'/></svg></span><span class="n">DATA · 8</span><h4>How data goes live</h4><p>Candidate → release → active pointer. Promote, roll back, self-heal.</p><span class="freq">weekly-ish</span></a>
<a class="card topic--data" href="#trust"><span class="cardIcon"><svg class="guideIcon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d='M12 3l7 3v5c0 4.4-3 8-7 10-4-2-7-5.6-7-10V6z'/></svg></span><span class="n">DATA · 9</span><h4>Is the data right?</h4><p>Data ops briefing, Data Trust, Trust Review findings, the Admin QA inbox.</p><span class="freq">daily glance</span></a>
<a class="card topic--data" href="#curation"><span class="cardIcon"><svg class="guideIcon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d='M20 12 12 20 3 11V3h8z'/><circle cx='7.5' cy='7.5' r='1.3'/></svg></span><span class="n">DATA · 10</span><h4>Curation</h4><p>Material names, company identity merges, related companies, market templates, site footprints.</p><span class="freq">weekly</span></a>
<a class="card topic--data" href="#knowledge"><span class="cardIcon"><svg class="guideIcon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d='M4 4.5A2 2 0 0 1 6 3h13v16H6a2 2 0 0 0-2 2z'/><path d='M4 19.5A2 2 0 0 1 6 18h13'/></svg></span><span class="n">DATA · 11</span><h4>Knowledge and law</h4><p>The documents Ask can cite, source freshness, the regulatory change monitor.</p><span class="freq">weekly</span></a>
<a class="card topic--data" href="#comms"><span class="cardIcon"><svg class="guideIcon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x='2.5' y='4.5' width='19' height='15' rx='2'/><path d='m3 7 9 6 9-6'/></svg></span><span class="n">DATA · 12</span><h4>Email and Ask safety</h4><p>What the app sends, how to preview it, and the abuse-incident inbox.</p><span class="freq">daily glance</span></a>
<a class="card topic--app" href="#shipping"><span class="cardIcon"><svg class="guideIcon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d='M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z'/><path d='M14 2v6h6M8 13h8M8 17h8M8 9h2'/></svg></span><span class="n">APP · 13</span><h4>Shipping a release</h4><p>One resumable command: scope check → gate → approval → dry run → publish.</p><span class="freq">per release</span></a>
<a class="card topic--app" href="#health"><span class="cardIcon"><svg class="guideIcon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d='M3 12h4l3 8 4-16 3 8h4'/></svg></span><span class="n">APP · 14</span><h4>Health and monitoring</h4><p>Two health endpoints, the uptime probe, the monitors that email you.</p><span class="freq">when paged</span></a>
<a class="card topic--app" href="#state"><span class="cardIcon"><svg class="guideIcon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d='M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z'/><path d='M17 21v-8H7v8'/><path d='M7 3v5h8'/></svg></span><span class="n">APP · 15</span><h4>State, backup, recovery</h4><p>What is irreplaceable, where it lives, how to back it up and prove a restore.</p><span class="freq">daily 06:15, ran once 00:09</span></a>
</div>
<h3 class="d2">Your rhythm</h3>
<div class="tw d2"><table>
<tr><th style="width:120px">When</th><th>What</th><th style="width:230px">Where</th></tr>
<tr><td><b>Daily</b><br>≈ 5 min</td><td>Read the morning briefing: is each register <code>ok</code>, did queues move, any trust findings. Glance at the pending-access badge and open Ask-safety incidents. Read the admin daily brief email if it arrived.</td><td>Admin → Data ops · Admin → Customers (badge) · Admin → Ask → Safety</td></tr>
<tr><td><b>Weekly</b><br>≈ 30 min</td><td>Confirm the weekly local tasks ran (nothing tagged <i>overdue</i>). Triage unresolved Trust Review findings. Mark new material names reviewed. Clear the regulatory review queue. Decide whether the newest data candidate deserves promotion.</td><td>Admin → Data ops → Sources · Data Trust · Materials · Ask → Regulatory monitor · <a href="#releases">§8</a></td></tr>
<tr><td><b>Monthly</b></td><td>Access review: every admin and every customer member still justified. AI spend against each agreement. Law-layer refresh result. Confirm backups exist and are recent.</td><td>Access, Organisations (each organisation's <i>Members and AI caps</i> and the People table's AI spend) · <a href="#state">§15</a></td></tr>
<tr><td><b>On event</b></td><td>New customer → <a href="#customers">§3</a>. Someone leaves → <a href="#lifecycle">§4</a>. New app version → <a href="#shipping">§13</a>. Alert email → <a href="#health">§14</a>. Something looks wrong → <a href="#broken">§19</a>.</td><td></td></tr>
</table></div>
<div class="rule d2"><b>Five rules that outrank everything else.</b>
<ol>
<li><b>Serving data is released, never edited.</b> No file copy into the app's data folder, ever; only promote and rollback.</li>
<li><b>A receipt file is not proof of health.</b> A job that was killed writes nothing, so yesterday's receipt still looks green. Check the age of the evidence.</li>
<li><b>Offboarding is not deletion, and deletion is not reversible.</b> Pick the weakest action that achieves the goal.</li>
<li><b>Never roll the app back past a security control that has been used</b> (session revocation, offboarding, purge). An older version would not enforce it.</li>
<li><b>Mandates and consumption never ship.</b> Every release passes <code>-Exclude consumption,mandates</code>; a refusal there is the system working.</li>
</ol>
</div>
</section>
<div data-toc-group="People — who gets in"></div>
<section class="area topic--people" id="logins">
<h2><span class="topicIcon"><svg class="guideIcon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d='M12 3l7 3v5c0 4.4-3 8-7 10-4-2-7-5.6-7-10V6z'/></svg></span><span class="n">2</span>Logins, MFA and sessions</h2>
<p class="where"><b>Where:</b> Admin → Customers → <i>Access</i> tab · account menu → Settings → <i>Security</i> · Space secrets</p>
<div class="short"><b class="tag">In short</b>
<p>Nobody sees anything without a login, and nobody gets a login unless you created it. There are two kinds of account: a handful of <b>owner-provisioned</b> accounts defined in a Space secret (you, and any early user), and <b>invited</b> accounts that customers' members create from an emailed link. Admins must also pass a six-digit code sent to their email. A session lasts four hours, stays signed in when the browser window is closed, and can be cut off at any moment. If the login configuration is missing or broken, the app locks everyone out rather than letting anyone in.</p>
<p>The Sinetti app limits simultaneous password checks and rapid sign-in attempts. A caller asked to wait can retry shortly; five failed passwords still pause that account from the same network source for fifteen minutes. The Sinetti operator should check the auth runbook when several customers share a hosting proxy.</p>
<p>The Sinetti app keeps browser sessions in a protected cookie and checks browser writes against that session. Signing out or switching accounts clears private browser state. New password hashes use stronger protection; invited accounts upgrade on successful sign-in, while the Sinetti operator must replace older roster hashes in the account secret. A password reset also cancels pending sign-in steps. Company sign-in, when configured, must finish in the browser that started it.</p>
</div>
<div class="d2">
<figure>
<svg viewBox="0 0 1000 250" role="img" aria-label="Login flow: password, then for admins an emailed six-digit code, then a four-hour session that is re-checked on every request.">
<rect class="bx" x="20" y="40" width="150" height="56" rx="8"/><text class="t" x="95" y="64">Password</text><text class="s" x="95" y="82">5 misses → 15 min lock</text>
<rect class="bx2" x="240" y="40" width="150" height="56" rx="8"/><text class="t" x="315" y="64">Admin account?</text><text class="s" x="315" y="82">flag on the user</text>
<rect class="gate" x="460" y="50" width="170" height="36" rx="18"/><text class="gt" x="545" y="73">EMAILED CODE</text>
<rect class="app" x="700" y="40" width="160" height="56" rx="8"/><text class="t" x="780" y="64">Session</text><text class="s" x="780" y="82">signed · 4 hours</text>
<path class="ar" d="M170 68H238" marker-end="url(#ah)"/>
<path class="ar" d="M390 68H458" marker-end="url(#ah)"/><text class="al" x="424" y="60">yes</text>
<path class="ar" d="M630 68H698" marker-end="url(#ah)"/>
<path class="ar" d="M315 96V128H780V98" marker-end="url(#ah)"/><text class="al" x="545" y="122">no — members go straight to a session</text>
<rect class="bx2" x="20" y="170" width="840" height="56" rx="8"/>
<text class="t l" x="36" y="194">Every request re-checks the session</text>
<text class="s l" x="36" y="212">signature valid · session not revoked · account active (not disabled / offboarded) · organisation trial or active · not expired</text>
<path class="ard" d="M780 96V168" marker-end="url(#ah)"/>
</svg>
<figcaption>A session is not a one-time pass. Because every request re-checks account and organisation state, disabling a member or suspending an organisation takes effect on that person's very next click — there is no waiting for the four hours to run out.</figcaption>
</figure>
<h3>Two kinds of account</h3>
<div class="tw"><table>
<tr><th></th><th>Owner-provisioned (roster)</th><th>Invited</th></tr>
<tr><td><b>Lives in</b></td><td>The Space secret <code>SINETTI_USERS</code> (a JSON list with password <i>hashes</i>, never passwords)</td><td>The app's own database, created when the invitee sets a password</td></tr>
<tr><td><b>Created by</b></td><td>You, from the operator machine, followed by a Space restart</td><td>You send an invite (or approve an access request); the member does the rest</td></tr>
<tr><td><b>Can be admin</b></td><td>Yes — <code>"admin": true</code> in the record</td><td>Not through the app <span class="pill gap">Gap</span> — invites always create non-admin members</td></tr>
<tr><td><b>Forgot password</b></td><td>No self-service; you re-issue a hash</td><td>Self-service emailed link, valid 1 hour, single use; resets every open session</td></tr>
<tr><td><b>Use it for</b></td><td>Yourself and a second admin</td><td>Every customer member — this is the normal path</td></tr>
</table></div>
<p>When the same person could match both, the invited account wins. A roster user who has no organisation is placed in the <code>owner</code> organisation at the next start — and the owner organisation sees <i>everything</i>. Never leave a customer's member there; move them to their own organisation immediately (<a href="#customers">§3</a>).</p>
<h3>Admin verification (emailed code)</h3>
<p class="where">Verified against <code>docs/auth-runbook.md</code> and the hosted marker probe, 25 September 2026. The code on <code>main</code> uses the emailed code. The hosted marker reported that day names <code>aed958ce55bc396b2221458c70de896f3fb31d2d</code>, which already contains that sign-in. The marker is the deployment script's reported source commit, not an independent runtime hash, and this guide has not watched a live administrator login.</p>
<p>An admin's password alone opens nothing. Sinetti emails a six-digit code and shows <b>Check your email</b>. The code lasts ten minutes, works once, and a newer code cancels it. A new code can be sent at most once a minute and five times an hour. Five wrong codes lock the step for fifteen minutes. The code goes to the account's <code>email</code>, else an email-shaped username, else <code>SINETTI_ADMIN_EMAIL</code>. Roster administrators in <code>SINETTI_USERS</code> should carry an <code>"email"</code> field. With no address anywhere, the screen says the code could not be sent. Mail uses the same path as password reset. If delivery is down, administrator sign-in is down until it is back; ordinary members are unaffected.</p>
<p>Sensitive actions — disable, offboard, reactivate, session revocation, permanent deletion — use the same code. The dialog offers <kbd>Email me a code</kbd>; enter it within five minutes. On the operator machine, <code>scripts\run_fastapi_v2.ps1</code> sets <code>SINETTI_LOCAL_ADMIN_MFA_BYPASS=1</code>, so administrators sign in with the password alone. Quotes pasted around <code>SINETTI_ADMIN_EMAIL</code> are ignored.</p>
<div class="warn"><b>Authenticator enrolment is the historical path.</b> It applied before the emailed-code commit. On a build that reports marker <code>aed958ce</code>, do not scan a QR code or store recovery codes. <code>scripts\break_glass_reset_admin_mfa.py</code> resets that old factor only; it does not send an email code. A missing <code>SINETTI_MFA_ENCRYPTION_KEY</code> mattered for the authenticator. Do not delete it until an administrator has signed in with an emailed code on the build you are changing.</div>
<p>The app refuses any change that would leave zero active admins, and refuses self-demotion and self-offboarding. <b>Keep two administrators</b>, each with a working email. A sole administrator with no deliverable address has no in-app way in.</p>
<h3>What fails closed</h3>
<ul>
<li>Missing <code>SINETTI_SESSION_SECRET</code> or an empty user list → nobody can log in. There is no anonymous mode to fall back to.</li>
<li>Two roster records with the same <code>user_id</code> → the <b>whole roster is discarded</b>, not just the duplicate. Validate the JSON before saving the secret.</li>
<li>A member whose organisation is suspended, offboarded or past its agreement end date is refused at login with “<i>{organisation}</i>'s access is paused — contact Sinetti.”</li>
<li>Login answers never reveal whether an account exists: every failure is the same “Invalid username or password.”</li>
</ul>
</div>
<div class="d3">
<h3>Procedures</h3>
<details class="proc" id="p-roster-user"><summary>Add an owner-provisioned (roster) user<span class="k">Desk · 10 min + Space restart</span></summary><div class="body">
<ol class="steps">
<li>On the operator machine, produce a password hash using a hidden prompt; keep the password out of shell history and process arguments.
<pre><code>python -c "from getpass import getpass; from sinetti_services.user_directory import hash_password; print(hash_password(getpass('Password: ')))"</code></pre>
<div class="expect">one line starting <code>pbkdf2_sha256$600000$…</code></div></li>
<li>Add a record to the JSON list. <code>user_id</code> is the permanent key that owns the person's data — choose it once and never change it.
<pre><code>{"user_id": "u_second_admin", "username": "jane", "password_pbkdf2": "pbkdf2_sha256$600000$…",
"display_name": "Jane Doe", "admin": true, "email": "jane@example.com"}</code></pre></li>
<li>Check the whole list parses as JSON and that no <code>user_id</code> repeats.</li>
<li>Save it in both places: the Space (Settings → Variables and secrets → <code>SINETTI_USERS</code>) and the operator machine's <code>.env</code>.</li>
<li>Restart the Space. Hand over username and password by a channel other than email.</li>
<li>If this is a customer's member rather than an admin, open Admin → Customers → Organisations, pick their organisation, open <i>Members and AI caps</i> and add their <code>user_id</code> — otherwise they sit in the all-seeing <code>owner</code> organisation.</li>
</ol></div></details>
<details class="proc" id="p-mfa-enrol"><summary>Sign in as an admin (emailed code)<span class="k">Screen · 2 min</span></summary><div class="body">
<ol class="steps">
<li>Enter the administrator password. The screen shows <b>Check your email</b>. No session exists yet.</li>
<li>Enter the six-digit code. Expect the app. A code older than ten minutes, already used, or replaced by a newer send is refused.</li>
<li>If nothing arrives: confirm the roster record has <code>"email"</code>, that <code>SINETTI_ADMIN_EMAIL</code> is set, and that the configured Infomaniak or customer SMTP transport delivers mail (<a href="#comms">§12</a>). The server log line is <code>email_code_mail_failed</code>. Production and private installations fail closed on unsent mail and never log codes, even with local SQLite storage. Customer SMTP requires authenticated STARTTLS with a verified certificate; the customer platform team mounts the password secret and, when needed, its private CA as described in the auth runbook.</li>
</ol>
<div class="note"><b>Historical authenticator enrolment.</b> Builds from before the emailed-code commit asked for a QR code, a first confirmation, and ten recovery codes. Do not follow those steps on a build that reports marker <code>aed958ce</code>.</div></div></details>
<details class="proc" id="p-mfa-reset"><summary>An administrator cannot get in<span class="k">Screen / Desk</span></summary><div class="body">
<ol class="steps">
<li><b>After the emailed-code release is deployed:</b> there is no authenticator to reset. Put a reachable address on the roster record (or fix <code>SINETTI_ADMIN_EMAIL</code>), restart the Space, and sign in again. Five wrong codes still lock the step for fifteen minutes — wait it out.</li>
<li><b>Historical authenticator lockout</b> (builds from before emailed codes): the locked-out admin uses a recovery code, then re-enrols. Otherwise a second admin opens Admin → Customers → <i>Access</i> → <i>Users</i> → <kbd>More</kbd> → <kbd>Reset MFA</kbd>, types a reason, confirms, and enters their own code. That control does not send an email code.
<div class="expect">“Account state updated.” The person enrols afresh at next login.</div></li>
<li><b>Sole administrator locked out on production</b> (lost phone and lost recovery codes): from the operator machine, dry-run then apply. Expect about five minutes of downtime. The script pauses the Space, keeps an untouched backup under <code>%LOCALAPPDATA%\Sinetti\break-glass\<timestamp>\</code>, resets the factor, uploads, checks the upload byte for byte, and restarts even when a step fails. It refuses during the nightly window (00:00–02:00 UTC) and while any Hugging Face job is running.
<pre><code>python scripts\break_glass_reset_admin_mfa.py --user-id u_admin
python scripts\break_glass_reset_admin_mfa.py --user-id u_admin --apply</code></pre>
<div class="expect">the next password login shows the enrolment screen. First used 19 September 2026.</div></li>
</ol></div></details>
<details class="proc" id="p-revoke"><summary>Cut off sessions — one person, yourself, or everyone<span class="k">Screen / Desk</span></summary><div class="body">
<ol class="steps">
<li><b>One member:</b> Admin → Customers → <i>Access</i> → <i>Users</i> → <kbd>More</kbd> → <kbd>Revoke sessions</kbd> → reason → <kbd>Confirm session revocation</kbd>. Their next click lands on the login page.</li>
<li><b>Your own other devices:</b> Settings → Security → <kbd>Revoke</kbd> on one device, or <kbd>Revoke other sessions</kbd>.</li>
<li><b>Everyone, immediately</b> (suspected compromise): replace the Space secret <code>SINETTI_SESSION_SECRET</code> with a new long random string and restart the Space. Every existing token stops verifying. Everyone logs in again; nothing else is lost.</li>
</ol></div></details>
<details class="proc" id="p-password"><summary>Change or reset a password<span class="k">Screen / Desk</span></summary><div class="body">
<ol class="steps">
<li><b>Invited member:</b> they press <i>Forgot password?</i> on the login page. The confirmation text is identical whether or not the address exists. The emailed link lasts one hour; using it logs out all their sessions. There is deliberately no admin screen that reveals a reset link — if mail is broken, fix mail (<a href="#comms">§12</a>).</li>
<li><b>Roster user:</b> hash a new password (as in <i>Add a roster user</i>), replace <code>password_pbkdf2</code> in the secret, restart the Space. <span class="pill gap">Gap</span> Nothing in the roster path is known to end their open sessions, so revoke them as well to be sure.</li>
</ol></div></details>
<details class="proc" id="p-mfa-key"><summary>Rotate the MFA encryption key<span class="k">Desk · production, until emailed codes are deployed</span></summary><div class="body">
<p>This still matters on production while authenticator factors are stored. No automated command exists. Back up the identity store and note the current key version, deploy code able to read the old key while writing the new, re-encrypt every stored authenticator, confirm each admin can still log in and <code>/api/ready</code> is ready, and only then retire the old key. Pasting a new key into the secret locks every admin out. After the emailed-code release is live, login no longer reads those factors; leave the key in place until that sign-in has been proved, then treat leftover <code>auth_mfa_*</code> rows as historical.</p></div></details>
</div>
</section>
<section class="area topic--people" id="customers">
<h2><span class="topicIcon"><svg class="guideIcon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx='9' cy='8' r='3.2'/><path d='M3 20c0-3.3 2.7-5 6-5s6 1.7 6 5'/><path d='M16 5.2a3.2 3.2 0 0 1 0 5.6M22 20c0-2.6-1.6-4.2-4-4.8'/></svg></span><span class="n">3</span>Customers: organisations, plans and seats</h2>
<p class="where"><b>Where:</b> Admin → Customers (tabs <i>Organisations</i>, <i>Plans</i> and <i>Access</i>)</p>
<p><b>Admin → Customers → Email delivery</b> answers “did my customers get mail?” without opening the database: emails sent in the last 7 days (with how many failed), members with a verified address out of all members, Market lists with a daily summary switched on, and customer alert attempts recorded. Below sit the most recent emails (date, kind, masked recipient). A row marked <i>Not sent</i> failed at the mail provider; the next daily run retries it. If members have no verified address, ask them to open Settings → Notifications and confirm it with the code, or invite them so the sign-in address is proven. The screen is read-only.</p>
<p>On a private installation (Docker), the server creates no Market list alerts and sends no daily summaries: change alerts come from Sinetti's own nightly data refresh, which does not run there yet. Email delivery therefore shows how many members have a verified address and lists what the installation does email (sign-in and security codes, address-confirmation codes, access requests and user reports to the administrator address). It shows no sent totals or history, because no list mail exists. Market monitoring on that installation says the same: monitors are saved lists, the Inbox stays empty, and no email option is offered. The hosted app is unchanged.</p>
<p>Contracts for a customer deal live next door in <b>Admin → Customers → Legal</b>: the Data Processing Agreement, privacy policy, terms of service and the counsel packet, each shown straight from <code>docs/legal/</code> with a <kbd>Download .md</kbd> button for sending to counsel or a customer. All four are DRAFTS until counsel has reviewed them; edit the files in git, never on the page.</p>
<div class="short"><b class="tag">In short</b>
<p>Sinetti takes no online payment. A customer signs a bilateral agreement; you then mirror that agreement in the app as an <b>organisation</b> holding a <b>plan</b>. The plan decides which features are unlocked, which registers are visible and the monthly AI budget; the organisation carries the number of <b>seats</b>. Each member belongs to exactly one organisation. Nothing on the Organisations or Plans tab bills anybody; they only mirror the agreement.</p>
<p><b>Organisations</b> is a list on the left (<i>Everyone</i>, then <i>Customers</i>, then <i>Staff</i> — the <code>owner</code> organisation) and the selected organisation on the right: plan, seats, contract, and the people in it with how they joined, when they were last seen (Zurich time) and what they spent on AI this month. A box at the top, <i>Needs a decision</i>, appears only when something does: people who joined through an access request naming a company but sit in the staff organisation, or a customer organisation with no members or on Trial with no contract end. <i>Everyone</i> shows all people grouped by organisation, with filters. The list folds to a rail of initials with the button at its top; the browser remembers that. <b>Plans</b> shows every plan as a column so they compare side by side. The address <code>/admin/billing?org=<org_id></code> opens one organisation.</p>
</div>
<div class="d2">
<figure>
<svg viewBox="0 0 1000 300" role="img" aria-label="A plan is a template of features, registers and AI budgets. An organisation holds one plan, a seat count and a status. Members each belong to exactly one organisation. Together these determine what a member can do.">
<rect class="bx" x="20" y="30" width="250" height="136" rx="8"/><text class="t" x="145" y="56">Plan (template)</text>
<text class="s" x="145" y="78">features: DD · Company research ·</text><text class="s" x="145" y="94">CSV export · PDF deep-dives</text>
<text class="s" x="145" y="110">Market balances · Batch check</text>
<text class="s" x="145" y="128">registers: ISCC · REDcert · INS · …</text><text class="s" x="145" y="148">AI budgets: chat · narrative · research</text>
<rect class="bx" x="375" y="30" width="250" height="120" rx="8"/><text class="t" x="500" y="56">Organisation (the customer)</text>
<text class="s" x="500" y="80">status: trial · active · suspended · offboarded</text><text class="s" x="500" y="100">seats: the live ceiling on members</text><text class="s" x="500" y="120">agreement period end (optional)</text>
<rect class="bx" x="730" y="30" width="250" height="120" rx="8"/><text class="t" x="855" y="56">Members</text>
<text class="s" x="855" y="80">each in exactly one organisation</text><text class="s" x="855" y="100">seat order = order they were added</text><text class="s" x="855" y="120">adding elsewhere moves them</text>
<path class="ar" d="M270 90H373" marker-end="url(#ah)"/><text class="al" x="322" y="82">one plan</text>
<path class="ar" d="M730 90H627" marker-end="url(#ah)"/><text class="al" x="678" y="82">belong to</text>
<rect class="app" x="250" y="216" width="500" height="60" rx="8"/><text class="t" x="500" y="240">What a member can do right now</text><text class="s" x="500" y="260">unlocked features · visible registers · seat granted at login · AI budget · rate & export limits</text>
<path class="ar" d="M500 150V214" marker-end="url(#ah)"/><text class="al l" x="510" y="188">recomputed after every admin change</text>
</svg>
<figcaption>The plan is a reusable template; the organisation is the customer. Seats live on the organisation — a plan's “seat limit” is only the default copied when the plan is first applied, so to sell two more seats you edit the organisation, not the plan.</figcaption>
</figure>
<h3>What members choose for themselves on first login</h3>
<p>The first time a member opens Sinetti they see a three-step welcome. It asks their <b>work</b> (sourcing and trading, compliance, certification body, market analysis, or something else), which materials and regions they follow, and which tour to start. Their answers change three things and nothing else: the questions Ask Sinetti suggests, one Market monitoring list called <i>My materials</i>, and, only if they tick the box, an email when that list changes. Nobody is sent mail unless they choose it. Every member lands on the same Home page.</p>
<p><b>Where to see it:</b> Admin → Customers → <i>Organisations</i> → the organisation → <i>Members and AI caps</i>: the table has a <i>Work</i> column, “—” for a member who has not chosen. Members change their own answer in Settings → <i>Your work</i>. Admin accounts never see the welcome. There is nothing for you to configure, and nothing to recover: if a member's role is wrong, they edit it themselves.</p>
<h3>The eight features a plan can unlock</h3>
<div class="tw"><table>
<tr><th>In the plan editor</th><th>What the member gains</th></tr>
<tr><td>Counterparty DD</td><td>The due-diligence profile on a holder</td></tr>
<tr><td>Company research</td><td>Online business-footprint research (spends AI + search budget)</td></tr>
<tr><td>CSV Export</td><td>Downloading filtered results as a file (still subject to export limits, <a href="#limits">§5</a>)</td></tr>
<tr><td>PDF Deep-Dives</td><td>The parsed-certificate deep-dive views</td></tr>
<tr><td>Market Balances</td><td>The market-balances add-on. Not part of either seeded plan, and its code is never deployed (<a href="#shipping">§13</a>)</td></tr>
<tr><td>Batch check</td><td>Check an official ISCC or REDcert sustainability declaration, in Excel or as a text-layer PDF, a Nabisy proof of sustainability PDF, or a Nabisy account-statement Excel. A single declaration or proof is one cargo. An account statement is one verdict per row: the scheme column is a BLE code, so the certificate line misses unless the row also carries an ISCC or REDcert certificate number. The REDcert raw-materials declaration has no single gCO2eq/MJ total, so that line stays a miss until a figure is printed. A Nabisy proof adds the country printed on the form, or the on-site-inspection consent on a downloaded Teilnachweis, judged against the BLE list (Stand 2026-08). The account statement uses Anbauland, because that export does not record inspection consent. Scanned PDFs are refused. Off on the seeded plans. Tick it on the agreement’s plan. Set <code>SINETTI_BATCH_CHECK_ENABLED=0</code> to leave the add-on out of a deployment. The file is read for that organisation and is not stored.</td></tr>
<tr><td>Research data</td><td>Research-derived data next to the certificates: research-confirmed related companies, the ownership line, Production units, research nameplate capacities, researched plants on the Producers map and list, Company research. On every plan by default (existing plans received it once on 29 September 2026). Untick it to make that customer certificates-only. On Admin → Customers → Plans, every plan without it, and on Organisations every customer organisation on such a plan, carries a <b>Certificates only</b> badge (no badge when the plan is unknown, or for the owner organisation). The global research data switch on Research → Coverage still overrides every plan. Takes effect on the member's next page load</td></tr>
<tr><td>Plant table</td><td>The third Producers panel next to List and Map: one row per researched plant plus one row per certified producer not researched yet, sortable and filterable by column. Needs Research data: the plan editor disables the box without it and the server refuses the combination. Not part of either seeded plan; the owner organisation always has it. Without it the Producers page shows List and Map only. No download</td></tr>
</table></div>
<p>Verified against the plan-editor feature keys, 17 October 2026. Batch check stays off the seeded plans. Until a plan includes it, the nav item is absent and a direct link does not open a locked page.</p>
<p>A member who opens a locked feature sees a polite in-place message — “<i>{Feature}</i> is not included in your plan — contact your administrator.” — never an error page. Registers outside the plan simply do not exist for that member: certificates, counts, maps and exports are all filtered at one choke point.</p>
<h3>Seeded plans and the owner organisation</h3>
<div class="tw"><table>
<tr><th>Plan</th><th>Features</th><th>Registers</th><th>Seats (default)</th><th>AI $/month (chat · narrative · research)</th></tr>
<tr><td><code>trial</code></td><td>PDF Deep-Dives</td><td>ISCC, REDcert, INS, 2BSvs, RSB</td><td>2</td><td>2 · 1 · 1</td></tr>
<tr><td><code>design_partner</code></td><td>DD, Company research, CSV Export, PDF Deep-Dives</td><td>ISCC, REDcert, INS, 2BSvs, RSB</td><td>5</td><td>25 · 10 · 15</td></tr>
</table></div>
<p>These are starting points, not commercial truth: the signed agreement is authoritative and you shape a plan to match it. The special organisation <code>owner</code> ignores plans entirely — all features, all registers — but it does <b>not</b> bypass AI or rate metering; only the admin flag on the person does that.</p>
<h3>Rules that surprise people</h3>
<ul>
<li><b>Seats are checked when you add people, and again at login.</b> Saving a members list or sending an invite that would take an organisation past its seats (members plus open invites) is refused with a message naming the seat count; raise the seats, remove a member or revoke an invite first. Removing members from an already over-seated organisation still works. At login the arrival order still applies: if an organisation has 5 seats and 6 members, the sixth-added member is refused with “All 5 seats for <i>{organisation}</i> are in use — contact your administrator.”</li>
<li><b>Members are picked from known users.</b> The drawer shows one row per member (name and user id, self-declared work, AI spend against each cap) under “Members · N of S seats used”. The cross at the end of a row removes that member, <i>Add member</i> offers active known users who are not members yet, and <kbd>Save</kbd> stores the list exactly as shown. A row in red reading “not a known user” is a member id with no login behind it (a typo or a deleted login); remove it. New people join through Access → Invite, not here. Removing a member ends their access at once; their private work stays stored under the old organisation.</li>
<li><b>The contract end date stops sign-in.</b> <i>Contract ends</i> in the organisation's edit panel (<kbd>Rename</kbd> or <kbd>Change plan</kbd>; blank = no end date) is the last day members can sign in; from the next day (UTC) login answers “<i>{organisation}</i>'s access is paused — contact Sinetti.” The organisation list shows “until 31 Dec 2026”, the Contract tile reads “Until 31 Dec 2026”, and a passed date turns the status pill to <b>Expired</b> (never for the owner organisation, which login does not check). Changing the plan keeps the date; clear the field and <kbd>Save</kbd> to remove it. Sessions already open are not cut at midnight; suspend the organisation for that.</li>
<li><b>Adding someone who belongs elsewhere moves them</b>, silently, and they lose sight of their previous organisation's private work (<a href="#isolation">§6</a>). Today a move is “remove from one organisation, add to the other” in <i>Members and AI caps</i>; the one-step <i>Move to organisation</i> action (which also re-stamps AI budgets and writes a receipt) is announced on the People table but not built yet.</li>
<li><b>AI budgets follow the plan.</b> They are stamped onto members when the plan is assigned, and <kbd>Save plan</kbd> with changed AI budgets re-stamps every member of every organisation on that plan, except a member whose cap you typed by hand in the organisation's <i>Members and AI caps</i> section (<a href="#limits">§5</a>). A member added later has no plan budget until the organisation is saved with its plan again or the plan's budgets change.</li>
<li><b>An invite that names a new organisation creates it</b> with 5 seats and status <code>active</code> — not <code>trial</code> — on the plan picked in the invite bar. An organisation created from the Organisations tab (<kbd>+ New organisation</kbd>) starts as <code>trial</code>.</li>
<li><b>An invite or an approved request never changes an existing organisation's plan.</b> When the organisation id is already known, the invite bar shows its current plan as read-only text (“{plan} · unchanged”) with a <i>Change in Organisations</i> link; sending the invite, redeeming it and approving a request all leave the plan, seats and AI budgets as they are. Change a plan only in <i>Organisations</i> (<kbd>Change plan</kbd>). The new member receives that organisation's plan AI budgets when they set their password.</li>
<li><b>Regulatory registers are a deliberate grant.</b> The plan editor has two register groups: <i>Certification schemes</i> (ISCC, REDcert, INS, 2BSvs, RSB) and <i>Regulatory registers</i> (LCFS, EPA, CFR, Oregon, BC). The seeded plans stay certification-only; tick a regulatory register on the agreement's plan to show it. RenovaBio has no box: it is added automatically to a plan that holds LCFS, CFR, Oregon and BC, and the line under the boxes says whether this plan includes it.</li>
<li><span class="pill gap">Gap</span> The plan editor validates feature and register names (an unknown name is refused with “unknown scheme key”), but not AI-budget names typed through the API. Through the screen you pick from boxes, so this only matters for scripted changes.</li>
</ul>
<h3 id="customer-edition">The customer edition</h3>
<p><b>Customer administrator recovery (SIN-0909 baseline).</b> The customer platform team appoints the initial administrator through customer-edition installation and bootstrap. The Sinetti app retains that user's organisation-scoped authority in <code>/state/customer-admin.json</code>, includes it in customer-state backup, and checks it against the restored database and retained installation identity before publishing a restore. An exact bootstrap repeat preserves credentials and authority; missing or different authority on an initialized account is refused. The customer platform team retains <code>edition.json</code>, installation configuration and recovery secrets separately. This baseline still requires its exact-image and customer-host acceptance; see the <a href="../enterprise/private-docker/install.md">private installation runbook</a>.</p>
<p><b>Customer administrator controls.</b> The Sinetti app checks the protected customer-side appointment on every session probe; a database operator flag does not grant customer administrator access. The customer Access page lists the appointed organisation's members and invitations. Designated administrator peers are protected; ordinary-member actions follow their current account state. Customer invitations use an email address and name, and customer reactivation retains the appointed organisation. Environment exposes only the expiring-soon window. An action requesting fresh verification uses the account's emailed code and retries the captured action once; leaving or cancelling the verification clears the pending action. Billing, plans, operator requests, permanent deletion, models and colour administration are outside these customer controls.</p>
<p>The customer edition is the reduced build installed on a customer-controlled server (Sinetti Private, Docker). The full edition keeps every module; the customer edition leaves the surfaces below out of its registration and its browser. Nothing is deleted from the code base, and a trimmed surface can return later as a released result.</p>
<ul>
<li><b>What the customer gets.</b> Browse, Verify, holder pages, watchlists and saved views, maps and Certified Sites with capacities, feedstock and fuel deep-dives, audit facts, certificate PDFs through live links, login with an e-mailed code, and customer-side organisation administration (access, setup, e-mail delivery) — for <b>ISCC and REDcert only</b>.</li>
<li><b>What the customer does not get.</b> Counterparty Profile, CB reputation and the CB deep-dive, Business Footprint and DD history, Ask, every other admin screen, operator monitoring (change monitor, pending release, source freshness, refresh plan, job health, data ops), consumption, mandates and market balances, and every other register: INS, 2BSvs, RSB, RenovaBio, LCFS, EPA, CFR, Oregon, BC and NEa.</li>
<li><b>How the browser follows.</b> <code>GET /api/capabilities</code> carries an <code>edition</code> block on the customer edition only: the name, the two schemes it serves and the list of absent surfaces. The browser narrows the plan's registers to those two schemes and closes the Ask and market-balances switches from it. The full edition sends no <code>edition</code> block and behaves as before.</li>
<li><b>The block hides; it does not enforce.</b> Plan scope still filters every certificate, count, map and export on the server, and the customer build omits the trimmed routes from its registration. Set the customer's plan to ISCC and REDcert only (§3, <i>Procedures</i>).</li>
<li>The browser follows the block in these places: the masthead drops Monitoring, Certification Bodies and every Admin entry except Environment (setup) and Customers (access); those routes send the visitor to Browse; the holder page drops the Scheme risk block, the company research button, the Research status line and the Dutch REV account line; the High risk chip is gone from Browse cards, Verify, certificate pages and Geography (the register's own ISCC blocklist signal stays); the Browse cards drop the Company research button and the Also.. (Counterparty Fit) link and drawer, and Geography drops its Company research panel; no freshness banner and no Source dates link appear, and the Browse export dialog leaves out the source date line, because the monitoring routes are not served; Ask and the Consumption entries are closed. The register-derived decision summary on the holder page stays, and so does the Alerts entry, because the watchlist Lists live there.</li>
<li><b>The scheme chips follow the served data.</b> The Scheme group in the Browse filter drawer lists the registers present in the rows the server returns, so on an installation whose data and plan are ISCC and REDcert only it shows those two. Verified in the built bundle on 23 October 2026 on a desk that still holds every register: all registers listed, because nothing is filtered there. <span class="pill gap">Gap</span> No customer data candidate exists on the operator machine yet, so the two-chip result has not been seen end to end; check it on the first customer installation. Browse also still makes one request to the unserved freshness route on page load; it answers 404 and nothing shows.</li>
</ul>
</div>
<div class="d3">
<h3>Procedures</h3>
<details class="proc" id="p-onboard"><summary>Onboard a customer, end to end<span class="k">Screen · 20 min</span></summary><div class="body">
<ol class="steps">
<li><b>Before touching the app</b>, run the two-customer acceptance gate if this is the first real customer or the first since a security-relevant release (<a href="#p-acceptance">procedure in §15</a>).</li>
<li><b>Shape the plan.</b> Admin → Customers → <i>Plans</i>. Reuse <code>design_partner</code> (<kbd>Edit</kbd> under its name), or press <kbd>+ New plan</kbd>: give it an ID and name, tick features, certification schemes and any regulatory registers the agreement covers, enter AI budgets (blank = unlimited), <kbd>Save plan</kbd>.</li>
<li><b>Invite the first member.</b> <i>Access</i> tab → <i>Invite</i>: email, name, an organisation id (type a new one, e.g. <code>acme</code>; a <i>New org name</i> field and the <i>Plan</i> list appear), pick the plan, <kbd>Send invite</kbd>. The plan is applied only because the organisation is new.
<div class="expect">“Invite created”. If the note says <i>email dry-run</i>, no email left the building — copy the set-password link shown and fix mail (<a href="#comms">§12</a>).</div></li>
<li><b>Set the seats and the contract end.</b> <i>Organisations</i> tab → the new organisation → <kbd>Change plan</kbd> → <i>Seats</i> = the number in the agreement, <i>Contract ends</i> = the agreement's last day (blank when it has none) → <kbd>Save</kbd>.</li>
<li><b>Invite the remaining members</b> with the same organisation id. Invites last 7 days; <kbd>Resend</kbd> issues a fresh link, <kbd>Revoke</kbd> kills one.</li>
<li><b>Set limits if the agreement differs from defaults</b>: AI caps and request/export ceilings (<a href="#limits">§5</a>).</li>
<li><b>Verify as the customer.</b> Log in as a non-admin member of that organisation (not with your admin account — admins bypass metering and see admin chrome). Check: only agreed registers appear, locked features show the plan message, an export works, another organisation's saved views are absent.</li>
<li><b>Record it outside the app.</b> The agreement reference, who provisioned, and when, go in your contract records. Sinetti stores no contract, price or payment detail by design.</li>
</ol></div></details>
<details class="proc" id="p-approve-request"><summary>Approve or reject an access request<span class="k">Screen · 2 min</span></summary><div class="body">
<ol class="steps">
<li>A badge on Admin → Customers shows pending requests (from the public request form). Open the <i>Access</i> tab → <i>Requests & invites</i>.</li>
<li><b>Set the organisation in the Invite form above the table first</b> — the approval uses it. The plan from that form applies only if the approval creates a new organisation; for an existing organisation the form shows its current plan as unchanged and the approval leaves it alone.</li>
<li>Press <kbd>Approve</kbd> (an invite is issued to the requester) or <kbd>Reject</kbd>.</li>
</ol>
<div class="note">Approving is a commercial act: it creates access under a plan. Reject anything without an agreement behind it.</div></div></details>
<details class="proc" id="p-change-org"><summary>Change a customer's plan, seats or members<span class="k">Screen · 2 min</span></summary><div class="body">
<ol class="steps">
<li>Admin → Customers → <i>Organisations</i> → the organisation → <kbd>Rename</kbd> or <kbd>Change plan</kbd>.</li>
<li>Change <i>Plan</i>, <i>Seats</i>, <i>Status</i> or <i>Contract ends</i>, and <kbd>Save</kbd> in the panel. To edit members, open <i>Members and AI caps</i>: the cross at the end of a row removes a member, <i>Add member</i> → <kbd>Add</kbd> adds a known user, and nothing is stored until <kbd>Save members</kbd>. (An AI cap typed in the same table is the exception: it saves on leaving the field.)</li>
<li><kbd>Save</kbd>. The change applies on each member's next request. Assigning a plan also re-stamps AI budgets on current members.</li>
<li>After adding members to an existing customer, re-select the plan and save once more so the newcomers receive AI budgets.</li>
</ol></div></details>
</div>
</section>
<section class="area topic--people" id="lifecycle">
<h2><span class="topicIcon"><svg class="guideIcon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d='M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4'/><path d='M16 17l5-5-5-5'/><path d='M21 12H9'/></svg></span><span class="n">4</span>Pause, offboard, delete</h2>
<p class="where"><b>Where:</b> Admin → Customers → <i>Access</i> → Users (people) · Admin → Customers → Organisations (organisations)</p>
<div class="short"><b class="tag">In short</b>
<p>There are four strengths of “stop”. <b>Disable</b> a person or <b>suspend</b> an organisation to pause — memberships stay intact and one click resumes. <b>Offboard</b> when the relationship has ended — access is quarantined, every session dies, but all of the customer's work is kept. <b>Permanently delete</b> only after offboarding, only after a verified backup, and only when you mean it: it cannot be undone. Every one of these leaves a receipt naming who acted and when. All but suspending an organisation also ask for a typed reason and a fresh emailed verification code. On production, until the emailed-code release is deployed, that code is still the authenticator.</p>
</div>
<div class="d2">
<figure>
<svg viewBox="0 0 1000 230" role="img" aria-label="Lifecycle ladder: active, paused, offboarded, permanently deleted. The first two steps are reversible; deletion is not.">
<rect class="app" x="20" y="60" width="180" height="64" rx="8"/><text class="t" x="110" y="88">Active</text><text class="s" x="110" y="106">normal use</text>
<rect class="bx" x="280" y="60" width="180" height="64" rx="8"/><text class="t" x="370" y="84">Paused</text><text class="s" x="370" y="102">person: Disabled</text><text class="s" x="370" y="116">organisation: Suspended</text>
<rect class="bx" x="540" y="60" width="180" height="64" rx="8"/><text class="t" x="630" y="84">Offboarded</text><text class="s" x="630" y="102">membership + plan quarantined</text><text class="s" x="630" y="116">all customer work kept</text>
<rect class="bad" x="800" y="60" width="180" height="64" rx="8"/><text class="t" x="890" y="88">Permanently deleted</text><text class="s" x="890" y="106">private records gone</text>
<path class="ar" d="M200 80H278" marker-end="url(#ah)"/><path class="ar" d="M280 106H202" marker-end="url(#ah)"/>
<path class="ar" d="M460 80H538" marker-end="url(#ah)"/><path class="ar" d="M540 106H462" marker-end="url(#ah)"/>
<path class="ar" d="M720 92H798" marker-end="url(#ah)"/>
<text class="al" x="240" y="72">pause</text><text class="al" x="240" y="124">resume</text>
<text class="al" x="500" y="72">offboard</text><text class="al" x="500" y="124">reactivate</text>
<text class="al" x="760" y="84">purge</text><text class="al" x="760" y="112">no way back</text>
<text class="s l" x="20" y="176">Every arrow: typed reason · explicit confirmation · emailed code from the last 5 minutes · security event + ownership receipt.</text>
<text class="s l" x="20" y="196">Refused outright: acting on yourself or your own organisation · removing the last active admin · reactivating into an organisation with no free seat.</text>
</svg>
<figcaption>Pick the leftmost state that achieves the goal. Pausing is for a late invoice or a security doubt; offboarding is for the end of an agreement; deletion is for a contractual or legal erasure request.</figcaption>
</figure>
<div class="tw"><table>
<tr><th>Action</th><th>Use when</th><th>Effect on access</th><th>Effect on their data</th><th>Undo</th></tr>
<tr><td>Disable a person</td><td>One person must stop; their place in the organisation stays</td><td>Refused on next request; sessions revoked</td><td>None</td><td><kbd>Reactivate</kbd></td></tr>
<tr><td>Suspend an organisation</td><td>Commercial or operational pause</td><td>All members refused at login and on next request</td><td>None; memberships and plan stay live</td><td><kbd>Reactivate suspension</kbd></td></tr>
<tr><td>Offboard a person</td><td>They left the customer</td><td>Account quarantined, membership removed, sessions revoked</td><td>Kept</td><td><kbd>Reactivate</kbd>, choosing a target organisation</td></tr>
<tr><td>Offboard an organisation</td><td>The agreement ended</td><td>Roster and plan moved to quarantine; every member session revoked</td><td>Kept</td><td><kbd>Reactivate</kbd> on the organisation — restores roster and plan together</td></tr>
<tr><td>Permanently delete</td><td>Erasure is required</td><td>—</td><td>Every private record in every registered category removed; shared register data untouched; a minimal receipt kept</td><td><b>None</b></td></tr>
</table></div>
<div class="warn"><b>Do not “fix” an offboarded organisation with the ordinary editor.</b> Changing status, plan or members on an offboarded organisation is refused (“Use the dedicated organisation reactivation route”). The <kbd>Reactivate</kbd> action checks seat capacity and restores plan and roster in one step. Old sessions never come back; everyone logs in again.</div>
<div class="warn"><b>Rollback boundary.</b> Once anyone has been offboarded or purged in production, an app version that predates these controls must never be deployed: it would ignore the quarantine and hand access back. The release gate carries a dedicated stage (“identity and offboarding safety acceptance”) for this reason.</div>
</div>
<div class="d3">
<h3>Procedures</h3>
<details class="proc" id="p-person-stop"><summary>Disable, offboard or reactivate a person<span class="k">Screen · 2 min</span></summary><div class="body">
<ol class="steps">
<li>Admin → Customers → <i>Access</i> → <i>Users</i>. The row button is <kbd>Disable</kbd> (or <kbd>Reactivate</kbd>); <kbd>More</kbd> opens <kbd>Revoke sessions</kbd>, <kbd>Reset MFA</kbd>, <kbd>Offboard</kbd>.</li>
<li>Choose the action, type a reason (3+ characters — write something a stranger would understand in a year), confirm.</li>
<li>If prompted, press <kbd>Email me a code</kbd>, enter it, and press <kbd>Verify and retry</kbd>. On production, until the emailed-code release is deployed, this is still the authenticator code.
<div class="expect">“Account state updated. Event <i>n</i> · <i>k</i> sessions revoked.” Ask the person to refresh: they must land on the login page.</div></li>
<li>To reactivate an offboarded person, pick the <i>Target organisation</i> in the dialog. Refused if that organisation has no free seat.</li>
</ol>
<div class="note">Roster accounts (defined in the Space secret) cannot be managed from this screen; the refusal message is deliberately generic. Remove or edit them in the secret instead.</div></div></details>
<details class="proc" id="p-org-stop"><summary>Suspend, offboard or reactivate an organisation<span class="k">Screen · 2 min</span></summary><div class="body">
<ol class="steps">
<li>Admin → Customers → <i>Organisations</i> → the organisation → <i>Suspend or offboard</i>.</li>
<li><kbd>Suspend</kbd> pauses at once; <kbd>Reactivate suspension</kbd> resumes. Neither asks for more. Every status change from here or the edit panel's <i>Status</i> list is recorded like offboarding: the organisation keeps who changed it, when and “Status trial → suspended in Admin → Billing”, and an ownership receipt repeats it.</li>
<li><kbd>Offboard</kbd> opens the reason dialog → <kbd>Confirm offboarding</kbd> → emailed code if asked (authenticator on production until that release is deployed).
<div class="expect">Status <code>offboarded</code>; an event number and the count of revoked sessions.</div></li>
<li>To bring the customer back: <kbd>Reactivate</kbd> → <kbd>Confirm reactivation</kbd>. Roster and plan return together.</li>
<li>Note the event number in your own records. Never copy credentials or customer content there.</li>
</ol></div></details>
<details class="proc" id="p-purge"><summary>Permanently delete a customer's or a person's data<span class="k">Screen · irreversible</span></summary><div class="body">
<div class="warn"><b>This destroys data and cannot be undone.</b> It is available only for an already-offboarded organisation or person, and only when the operator machine has a backup location configured (<code>SINETTI_BACKUP_DIR</code>).</div>
<ol class="steps">
<li>Open the offboarded organisation (Organisations → the organisation → Suspend or offboard) or person (Access → Users → More) and choose <kbd>Permanently delete</kbd>.</li>
<li><kbd>Preview permanent deletion</kbd> — read the per-category counts. The preview expires; if anything changes meanwhile the app demands <kbd>Refresh preview</kbd>.</li>
<li><kbd>Create and verify backup</kbd> — wait for “Backup verified.” A stale preview invalidates the backup too, by design.</li>
<li>Type the exact identifier shown to confirm, then <kbd>Delete permanently</kbd>. Enter the emailed code if asked (authenticator on production until that release is deployed).</li>
<li>Deletion runs category by category under a ten-minute execution lease. The worker renews it while it is alive. If the browser closes mid-way, reopen the same panel: <kbd>Resume deletion</kbd> continues the same request. If the worker was killed, wait about ten minutes for the lease to lapse, then resume. A message <code>execution lease is no longer owned</code> means another operator holds it, or the lease lapsed during a real stall — do not start a second purge of the same subject.
<div class="expect">A receipt: completion time, administrator, backups verified. The receipt names the subject only as a one-way digest.</div></li>
</ol></div></details>
</div>
</section>
<section class="area topic--people" id="limits">
<h2><span class="topicIcon"><svg class="guideIcon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d='M4 21v-7M4 10V3M12 21v-9M12 8V3M20 21v-5M20 12V3M1 14h6M9 8h6M17 16h6'/></svg></span><span class="n">5</span>Limits: AI spend, request rate, exports</h2>
<p class="where"><b>Where:</b> Admin → Customers → <i>Organisations</i> (the People table's <i>AI spend</i> column, each organisation's <i>Members and AI caps</i> and <i>Data access limits</i>) and <i>Plans</i> (<i>Default AI caps</i> under the table)</p>
<div class="short"><b class="tag">In short</b>
<p>Two meters protect the business. The <b>AI meter</b> prices every assistant answer, narrative rewrite and company-research run in real dollars and stops a member at their monthly cap. The <b>data-access meter</b> caps how fast anyone can page through the data and how many rows they may export per day and per month — the practical defence against scraping, in place of a captcha. Both follow the same rule: a personal setting beats the default, the default beats the built-in value. Both refuse when in doubt; admins bypass both but are still recorded.</p>
</div>
<div class="d2">
<figure>
<svg viewBox="0 0 1000 170" role="img" aria-label="Limit resolution: the app looks for a personal setting first, then the default row, then the built-in value.">
<rect class="bx" x="20" y="40" width="240" height="64" rx="8"/><text class="t" x="140" y="66">1 · Personal setting</text><text class="s" x="140" y="86">this member (or this organisation)</text>
<rect class="bx" x="380" y="40" width="240" height="64" rx="8"/><text class="t" x="500" y="66">2 · Default row</text><text class="s" x="500" y="86">shown as “*” — applies to everyone</text>
<rect class="bx2" x="740" y="40" width="240" height="64" rx="8"/><text class="t" x="860" y="66">3 · Built-in value</text><text class="s" x="860" y="86">in code — last resort</text>
<path class="ar" d="M260 72H378" marker-end="url(#ah)"/><text class="al" x="320" y="64">none set</text>
<path class="ar" d="M620 72H738" marker-end="url(#ah)"/><text class="al" x="680" y="64">none set</text>
<text class="s l" x="20" y="140">A setting that exists but is blank means “unlimited” and still wins — blank is a decision, not an absence.</text>
</svg>
<figcaption>The first box that holds a setting decides. This is why a cap you typed by hand on one member survives every later plan change: a hand-typed value is a personal setting, and plan budgets never overwrite those.</figcaption>
</figure>
<h3>AI meter</h3>
<div class="tw"><table>
<tr><th>Metered feature</th><th>What spends</th><th>Built-in cap</th><th>What the member reads at the cap</th></tr>
<tr><td>Chat</td><td>Ask Sinetti answers</td><td>$5.00 / month</td><td>“Monthly Ask Sinetti allowance used - resets <i>date</i>.”</td></tr>
<tr><td>Narrative</td><td>AI rewrite of narratives</td><td>$2.00 / month</td><td>“Monthly narrative-refinement allowance used; resets <i>date</i>.”</td></tr>
<tr><td>Research</td><td>Company research (model + web search at $0.005 per query)</td><td>$3.00 / month</td><td>“Monthly Company research allowance used - resets <i>date</i>.”</td></tr>
</table></div>
<ul>
<li>The month is the calendar month in UTC; caps reset on the 1st. A warning state begins at 80 % of the cap.</li>
<li>Before calling the model, the app <b>reserves</b> the worst-case cost; afterwards it records the exact cost and releases the rest. If the reservation cannot be made — no identity, storage trouble, cap reached — the call never happens. You are never billed for a request the meter could not see.</li>
<li>Prices per model are constants in the code. A model with no price is refused rather than metered at zero. Review the table against the provider's current prices when you change models.</li>
<li>Plan budgets are stamped on members at plan assignment, and again on every member of the plan's organisations when you change the plan's AI budgets; they are marked as plan-owned. A cap you type by hand is marked as yours and <b>is never overwritten by a plan again</b>. <span class="pill gap">Gap</span> No screen hands a member back to plan control.</li>
<li>The Customers table's <i>AI spend</i> column reads this month's spend for the whole customer against the sum of its members' caps (personal cap, else the default). When any member's cap is unlimited, the column shows the spend alone.</li>
</ul>
<h3>Data-access meter</h3>
<div class="tw"><table>
<tr><th>Limit</th><th>Built-in value</th><th>Notes</th></tr>
<tr><td>Requests per 5 minutes — per member</td><td>2 000</td><td>An older installation may still hold 120 in its default row; check the screen, not this table</td></tr>
<tr><td>Requests per 5 minutes — per organisation</td><td>10 000</td><td>All members together</td></tr>
<tr><td>Export rows per day</td><td>5 000</td><td></td></tr>
<tr><td>Export rows per month</td><td>20 000</td><td></td></tr>
<tr><td>Rows in a single file</td><td>5 000</td><td>Hard ceiling. <i>Periodic unlimited</i> lifts the daily/monthly/rate allowances, never this</td></tr>
</table></div>
<p>A member over a limit sees only “We couldn't complete this action. Please try again later or contact support.” — no numbers, no countdown, no headers. The limits are invisible on purpose: a scraper learns nothing about where the edge is.</p>
<p>Every export attempt — issued, refused or failed — is written to the <b>export audit</b>: who, when, which filters, how many rows, a fingerprint of the exact selection. Never the rows themselves. Audit entries are kept 365 days; limit changes are logged with before and after values.</p>
</div>
<div class="d3">
<h3>Procedures</h3>
<details class="proc" id="p-ai-cap"><summary>Set or lift an AI cap<span class="k">Screen · 1 min</span></summary><div class="body">
<ol class="steps">
<li>Admin → Customers → <i>Organisations</i> → the person's organisation → <i>Members and AI caps</i>. The table shows each member's spend and an editable cap per feature.</li>
<li>Type a dollar amount in the person's cell and click away — it saves on leaving the field, without <kbd>Save</kbd>. Blank on a person = follow the default. The default lives in <i>Default AI caps</i> under the Plans table (Plans tab); blank there = unlimited for everyone without a personal cap.</li>
<li>For invited members, set budgets through their plan instead (Plans → Edit → AI budgets → <kbd>Save plan</kbd>): the new budgets reach every current member of the plan's organisations, except members with a hand-typed cap.</li>
</ol></div></details>
<details class="proc" id="p-data-limits"><summary>Set request and export limits for a customer or a member<span class="k">Screen · 2 min</span></summary><div class="body">
<ol class="steps">
<li>Admin → Customers → <i>Organisations</i> → the organisation → <i>Data access limits</i>.</li>
<li>Under <i>Organisation limits</i>, fill <i>Requests / 5 min</i>, <i>Daily export rows</i>, <i>Monthly export rows</i> (blank = inherit) or tick <i>Periodic unlimited</i> → <kbd>Save limits</kbd>. <kbd>Reset inheritance</kbd> returns to defaults.</li>
<li>For one member, press <kbd>Override</kbd> in the members table and do the same.</li>
<li>The usage line (“Used this month: …”) tells you whether a complaint of “it stopped working” is a limit.</li>
</ol></div></details>
<details class="proc" id="p-export-audit"><summary>Investigate an export<span class="k">Screen · 5 min</span></summary><div class="body">
<ol class="steps">
<li>Admin → Customers → <i>Organisations</i> → the organisation → <i>Data access limits</i> → open <i>Export audit — n events</i>.</li>
<li>Read <i>When · User · Outcome · Rows · Reason · Filename</i>; page with <kbd>Older</kbd> / <kbd>Newer</kbd>.</li>
<li>A run of <code>refused</code> with a rate reason from one member is the signature of automated harvesting: consider <kbd>Disable</kbd> while you ask the customer.</li>
</ol></div></details>
</div>
</section>
<section class="area topic--people" id="isolation">
<h2><span class="topicIcon"><svg class="guideIcon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d='M12 3 2 8l10 5 10-5z'/><path d='M2 12l10 5 10-5'/><path d='M2 16l10 5 10-5'/></svg></span><span class="n">6</span>Customer isolation</h2>
<div class="short"><b class="tag">In short</b>
<p>Register data is shared by everyone — it is public information, enriched. What customers <i>do</i> with it is not shared. Personal work belongs to a member inside their organisation; research belongs to the organisation; nothing crosses between customers, and the admin flag does not open another customer's private research. The browser never says which organisation it belongs to — the server works it out from the login — so there is nothing for anyone to forge.</p>
</div>
<div class="d2">
<div class="tw"><table>
<tr><th style="width:210px">Scope</th><th>What lives there</th><th>Who can see it</th></tr>
<tr><td><b>Private to a member</b><br>(organisation + person)</td><td>Watchlist pins, saved views, alert dismissals, notification preferences and delivery records</td><td>That member only — not colleagues in the same organisation</td></tr>
<tr><td><b>Shared in an organisation</b></td><td>Counterparty DD history; company-research briefs and their evidence</td><td>Every member of that organisation</td></tr>
<tr><td><b>Global</b></td><td>Certificates, pathways, alerts raised from register changes, the cache of public company-registry lookups</td><td>Everyone, filtered by their plan's registers</td></tr>
</table></div>
<ul>
<li><b>Moving a member between organisations does not move their work.</b> They start empty in the new one; if they return, the old work reappears. No screen copies records across.</li>
<li><b>No organisation, no access.</b> A login with no active organisation cannot hold a session; private features answer <code>tenant_required</code>.</li>
<li>Rows from before isolation existed that could not be attributed to anyone are parked in a quarantine that no login can reach. They are kept, not served.</li>
<li>Company research shipped with the app is a read-only seed used only while an organisation has no research of its own. One customer's research is never a fallback for another.</li>
</ul>
<p>The two-customer acceptance gate (<a href="#p-acceptance">§15</a>) proves these rules on two throwaway organisations. Run it before each real customer.</p>
</div>
</section>
<div data-toc-group="Data — what customers see"></div>
<section class="area topic--data" id="refresh">
<h2><span class="topicIcon"><svg class="guideIcon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle class='iconWash' cx='12' cy='12' r='9' fill='url(#iconWashGold)' stroke='none'/><circle cx='12' cy='12' r='9'/><path d='M3 12h18'/><path d='M12 3a14 14 0 0 1 0 18'/><path d='M12 3a14 14 0 0 0 0 18'/></svg></span><span class="n">7</span>Where data comes from</h2>
<p class="where"><b>Where:</b> Admin → Data ops (tabs <i>Refresh</i> · <i>Sources</i>) · Hugging Face Jobs · Windows Task Scheduler on the operator machine</p>
<div class="short"><b class="tag">In short</b>
<p>Register data is collected by machines, not by you. One <b>nightly cloud job</b> refreshes ISCC and REDcert (2BSvs and RSB join it once the job is re-registered with them — see <i>Activating 2BSvs and RSB</i> in <code>docs/daily-refresh-runbook.md</code>); a handful of <b>weekly tasks on the operator machine</b> refresh the registers that block cloud addresses or change slowly (INS, the North-American registers, the Dutch account list). Each run validates what it fetched, rebuilds the app database, has it reviewed, and publishes a <b>candidate</b> to a private dataset repository. Your job is to notice when a run did <i>not</i> happen — and a run that was killed leaves no trace except silence.</p>
</div>
<div class="d2">
<figure>
<svg viewBox="0 0 1000 300" role="img" aria-label="Two origins of data: a nightly cloud job for ISCC and REDcert, and weekly tasks on the operator machine for the other registers. Both publish candidates to the same private dataset repository.">
<text class="ln l" x="20" y="28">CLOUD — every night</text>
<rect class="bx" x="20" y="40" width="200" height="60" rx="8"/><text class="t" x="120" y="66">ISCC · REDcert</text><text class="s" x="120" y="84">public register downloads</text>
<rect class="bx" x="300" y="40" width="250" height="60" rx="8"/><text class="t" x="425" y="66">Nightly refresh job</text><text class="s" x="425" y="84">Hugging Face · 00:00 UTC · plans for 3 h 30, hard stop 4 h 30</text>
<path class="ar" d="M220 70H298" marker-end="url(#ah)"/><text class="al" x="259" y="62">fetch</text>
<text class="ln l" x="20" y="168">OPERATOR MACHINE — every week</text>
<rect class="bx" x="20" y="180" width="200" height="60" rx="8"/><text class="t" x="120" y="204">INS · LCFS/EPA · CFR</text><text class="s" x="120" y="222">Oregon/BC · NEa</text>
<rect class="bx" x="300" y="180" width="250" height="60" rx="8"/><text class="t" x="425" y="206">Weekly scheduled tasks</text><text class="s" x="425" y="224">Windows Task Scheduler · PC must be on</text>
<path class="ar" d="M220 210H298" marker-end="url(#ah)"/><text class="al" x="259" y="202">fetch</text>
<text class="al l" x="20" y="262">INS is local because the Italian register refuses cloud addresses (verified) — not a bug to fix.</text>
<rect class="app" x="700" y="40" width="280" height="200" rx="10"/>
<text class="t" x="840" y="70">Private dataset repository</text>
<text class="s" x="840" y="98">release channel: one atomic commit</text><text class="s" x="840" y="114">with a checksum manifest (≤ 1 GB)</text>
<text class="s" x="840" y="146">side channel: hosted REDcert PDFs</text><text class="s" x="840" y="162">(unbounded, own commits)</text>
<text class="s" x="840" y="200">= the newest <tspan font-weight="600">candidate</tspan></text><text class="s" x="840" y="216">not yet what customers see</text>
<path class="ar" d="M550 70H698" marker-end="url(#ah)"/><text class="al" x="624" y="62">review · publish</text>
<path class="ar" d="M550 210H698" marker-end="url(#ah)"/><text class="al" x="624" y="202">same steps</text>
</svg>
<figcaption>Two origins, one destination. Every run ends by publishing a complete candidate release; nothing a refresh job does changes what customers see. That happens only in <a href="#releases">§8</a>.</figcaption>
</figure>
<h3>What one nightly run does, in order</h3>
<ol>
<li><b>Hydrate</b> the last verified release into a throwaway workspace (the job never works on the app's own data).</li>
<li><b>Refresh</b> ISCC (certificates, compliance list, blocklist) and REDcert. Each source must pass a contract check — expected columns, plausible row count, parseable dates. A source that fails keeps <b>yesterday's</b> data (<code>kept_previous</code>) and the run is marked <code>partial</code>; one bad source never poisons the rest. Every ISCC certificate without a PDF link gets one here: withdrawn and suspended certificates take the link from the compliance block; the rest cost one register search each, newest in-force first, capped by <code>ISCC_PDF_LINK_MAX_LOOKUPS</code> (default 500). The map rides the data release. Receipt key <code>refresh.ISCC.pdf_links</code>. A miss is retried the next night. The PDF queue only takes certificates that already have a link. Hand backlog: <code>python scripts\resolve_iscc_pdfs.py</code>.</li>
<li><b>Rebuild</b> the app database from that workspace (including whatever PDF artifacts hydrate already had).</li>
<li><b>Trust Review</b> that candidate (<a href="#trust">§9</a>).</li>
<li><b>Publish</b> a register candidate to the dataset repository and restart the app so it notices it. A receipt and <code>job_heartbeat.json</code> are flushed after this step. Hugging Face SIGKILL cannot write a final receipt; the heartbeat is the in-run pulse. A <code>running</code> receipt is not a successful night.</li>
<li><b>Optional tail</b> — geocode, certificate PDFs, ISCC audit reports, REDcert PDF hosting — only if time remains (the process keeps a 75-minute reserve for the second rebuild, review, publish and mail). New tail work is not started inside that reserve. Certificate PDFs take new documents first. The 1,200-PDF cap is split evenly across schemes that still have candidates (ISCC and REDcert on this job); unused share spills back, so a straight ISCC-first prefix cannot leave REDcert with an empty night. Audit reports take known-template and URL-identity repairs first, then new documents. In both stages, previous failed parses may use at most 20% of the cap. Parser-version repairs do not count against that retry share. A cap of 0 skips that stage.</li>
<li><b>Second publish</b> if the tail changed artifacts and time remains. If the tail is killed, the register candidate from the first publish stays.</li>
</ol>
<div class="warn"><b>The job's settings are frozen at registration.</b> Schedule, caps, timeout and register list are baked into the scheduled job when it is registered; editing a file or a Space variable changes nothing. To change any of them you register the job again — and then make sure the old one is no longer active, because exactly one nightly job may run. As registered on 24 September 2026: certificate PDFs 1,200/night, audit reports 100/night, REDcert PDF hosting 300/night, timeout 4 hours, hardware cpu-upgrade (8 vCPU, 32 GB). Re-registered 29 September 2026: the job plans against a 3 h 30 budget and the hard stop moved to 4 h 30, so the stop only fires on a stuck run. The previous nightly was 100 PDFs and 2 hours on cpu-basic (16 GB); that machine ran out of memory when PDF work ran long. Read the live values from the active job, not from this page.</div>
<p class="where">Verified against the nightly PDF retry policy, the registered nightly job settings, and the INS weekly task registration (<code>register_ins_weekly_task.ps1</code>: caps 1,200, four-hour cutoff), 25 September 2026.</p>
<p>The nightly job downloads the Space code revision at the start of its bootstrap and records it as <code>config.space_revision</code> on <code>last_daily_refresh.json</code>. The existing Data ops last-run caption shows its first ten characters; <b>Space unknown</b> means the receipt has no usable recorded revision, including older receipts, malformed values and private code rehearsals. A parser fix on the operator machine runs in the cloud only after that code is deployed. The revision is the code the job downloaded, not the dataset release or proof that the app has activated it. After deploying this receipt change, the Sinetti operator must re-register the nightly job once to install the new bootstrap, then check its next receipt; until then the revision stays unknown. A job error or missing receipt still requires checking the true job status on Hugging Face.</p>
<p class="where">Receipt and caption behavior verified against local bootstrap, receipt and Data ops tests, 9 October 2026. Deployment and job re-registration remain separate operator actions.</p>
<p><b>Pending certificate PDFs (policy approved 10 October 2026).</b> Once compatible code is deployed, a positively diagnosed scanned certificate PDF or a REDcert document positively confirmed unavailable may remain pending during otherwise checked publication. Pending documents remain in the backlog and failed/remaining counts; they are not successful parses. The Sinetti operator must inspect the per-document evidence and the final activation receipt. Missing or malformed evidence, unknown errors, identity or date conflicts, refused replacements, lost material evidence, audit failures, worker failures and failed quality or Trust Review still hold publication of that night's PDF results. Since 23 October 2026 (SIN-0904) they no longer hold the night's register data: the nightly activates its register-only commit, and the activation receipt shows tier "core". A PDF left for the next night because time ran out is not a hold. A hold requires investigation or correction, never a weaker check. This local policy approval does not authorize deployment, another paid run or a schedule change. See the <a href="../daily-refresh-runbook.md#owner-approved-publication-policy-for-pending-certificate-pdfs">daily refresh runbook</a>.</p>
<h3>Preparing an additional PDF drain</h3>
<p>The Sinetti operator can prepare a bounded proposal with <code>scripts/prepare_pdf_drain.py</code>; the detailed procedure is in <code>docs/pdf-drain-runbook.md</code>. Preparation is local-only: it starts no job, enables no schedule and reads no credentials. It does not change the nightly settings or enable automatic promotion. Recurring drain scheduling stays off.</p>
<p>Two authorised publishing trials finished on 25 September 2026. Both published a reviewed core revision and a reviewed tail revision, left auto-activation off, and kept the ISCC, REDcert and INS register files unchanged. The first selected 50 certificates and added no new parses. The second, on the repaired parser, selected 100: 79 were re-reads that stayed parsed, and 4 previously unsupported certificates became parsed (349 pathway rows). Sixteen stayed unsuccessful. Receipts, durations and costs are in <code>docs/pdf-drain-runbook.md</code>. Another paid run needs a new explicit approval with an exact job count and cost ceiling. Do not promote while any cloud job is running.</p>
<p>The prepared drain requires Trust Review of the retained certificate sources before both publications, without recording a new register fetch. It preserves the existing business overrides and Trust Review history, while keeping its report, heartbeat, history and PDF cache separate. A failed or incomplete required review stops publication; findings still need the normal manual decision. Concurrent promotion remains blocked while another cloud job runs.</p>
<p>A completed job or an overall <code>ok</code> report is not proof that PDF work was saved. The Sinetti operator must verify <code>tail_publish.status = ok</code>, the published revision, and useful PDF results retained by the next baseline. A hard interruption before publication can lose work and require a repeat. There is no recurring drain schedule. Turn one on only after a later trial shows durable parsed-document progress, and stop or reduce it when the backlog clears.</p>
<p class="where">Verified against both 25 September 2026 trial receipts in <code>docs/pdf-drain-runbook.md</code>. Recurring drain remains disabled.</p>
<h3>Morning cloud OCR job</h3>
<p><b>Publication policy approved on 9 October 2026:</b> the nightly data refresh and morning OCR run should each make their checked release live after finishing. This is being rolled out under SIN-0481; both live schedules were still set to candidate-only at the last configuration check. After enablement, failed data checks keep the previous release live, and a failed website restart triggers recovery to the previous release. The Sinetti operator confirms success from the final activation receipt and the live revision under System health. A completed upload alone does not prove customers received the update. Application-code deployments remain separate.</p>
<p>Scanned ISCC and REDcert certificates cannot be read by the nightly: its machine has no OCR engine. A separate Hugging Face job reads them each morning at 07:00 local time (06:00 in winter), after the nightly has published. It takes up to 60 ISCC and 20 REDcert certificates by default; an explicit REDcert cap of zero disables that lane. Known unchanged scans stay out of nightly text-parser retries. An unsuccessful OCR attempt is retried once when its OCR reader or applicable ISCC layout family improves, then its new ledger entry closes it again. Successful OCR and accepted exceptions remain protected; new document URLs, positive file changes and reviewed rereads remain eligible. The job keeps only readings the approved quality rules accept, merges both lanes, and publishes <b>one candidate release</b>. Production changes only when the Sinetti operator promotes a release. The job refuses an unpinned OCR engine and refuses publication if another job moved the dataset revision. Register it with <code>python scripts\register_hf_cloud_ocr_job.py --apply</code> after the deploy that ships it; the runbook section “Morning cloud OCR job” has the settings, rehearsal and checks.</p>
<p class="where">Verified against <code>scripts/run_iscc_cloud_ocr.py</code>, <code>iscc_cloud_ocr.py</code> and <code>scripts/register_hf_cloud_ocr_job.py</code>, 29 September 2026. Not registered yet: ships with the next deploy.</p>
<h3>The calendar</h3>
<div class="tw"><table>
<tr><th>Job</th><th>Runs on</th><th>When (local time)</th><th>What it does</th></tr>
<tr><td>Daily refresh</td><td>Hugging Face</td><td>nightly, 00:00 UTC</td><td>ISCC + REDcert → candidate (2BSvs + RSB after activation)</td></tr>
<tr><td>Morning cloud OCR</td><td>Hugging Face</td><td>daily, 05:00 UTC (07:00 summer / 06:00 winter)</td><td>Scanned ISCC and REDcert certificates → one candidate. Registered after the deploy that ships it</td></tr>
<tr><td>Sinetti PC daily HF sync</td><td>Operator machine</td><td>daily 03:00 + at logon</td><td>Mirrors production receipts and state to the machine; stages the newest candidate locally. Never promotes. The register script's intended slot is 04:30; the installed trigger is still 03:00.</td></tr>
<tr><td>Sinetti PC daily test run</td><td>Operator machine</td><td>daily 03:00</td><td>Runs the release gate itself against <code>main</code> (<code>--keep-going</code>), so a red stage is found the next morning and a green night leaves proofs a same-day deploy can inherit. Runbook: <code>docs/nightly-suite-runbook.md</code></td></tr>
<tr><td>Sinetti PC 15-min resume interrupted gate</td><td>Operator machine</td><td>every 15 minutes</td><td>If a reboot kills <code>release.ps1</code> mid-stage, the next tick resumes that same candidate. It does not start a new release</td></tr>
<tr><td>Sinetti PC daily trust monitor</td><td>Operator machine</td><td>daily <b>05:30</b></td><td>Emails you the daily workload to clear, and when a refresh or review went missing or failed (<a href="#health">§14</a>)</td></tr>
<tr><td>Sinetti PC daily customer-state backup</td><td>Operator machine</td><td>daily 06:15</td><td>Copies production's customer state, as mirrored nightly into <code>data\prod-mirror</code>, into <code>C:\Users\azizl\sinetti-backups\customer-state</code> and keeps the newest 14 stamps. SQLite files are snapshotted, so an open writer does not leave a torn copy. The task does not read the production bucket and does not upload. Receipt: <code>data\refresh\last_customer_state_backup.json</code>. One stamp, <code>20260925T205902Z</code>, was uploaded once to the private dataset <code>Couscouskabyle/sinetti-customer-state-backups</code>.</td></tr>
<tr><td>Sinetti PC daily regulatory monitor</td><td>Operator machine</td><td>daily 06:30</td><td>Checks laws, North American guidance indexes and five registers for changes (<a href="#knowledge">§11</a>)</td></tr>
<tr><td>Sinetti PC weekly US refresh</td><td>Operator machine</td><td>Sat 07:00</td><td>California LCFS + US EPA registers</td></tr>
<tr><td>Sinetti PC weekly CFR refresh</td><td>Operator machine</td><td>Sun 07:30</td><td>Canadian Clean Fuel Regulations list</td></tr>
<tr><td>Sinetti PC weekly OR-BC refresh</td><td>Operator machine</td><td>Sun 08:30</td><td>Oregon + British Columbia registers</td></tr>
<tr><td>Sinetti RenovaBio weekly refresh</td><td>Hugging Face</td><td>Sun 10:00</td><td>Brazil ANP RenovaBio certificates → candidate. Shown in Browse and on holder pages since stage 2 (30 Sep 2026) (runbook <code>docs/renovabio-refresh-runbook.md</code>). Registered after the next deploy by the cloud-jobs procedure below (<code>register_hf_weekly_register_jobs.py</code>, all four cloud registers or <code>--registers renovabio</code> alone)</td></tr>
<tr><td>Sinetti PC weekly NEa refresh</td><td>Operator machine</td><td>Sun 09:15</td><td>Dutch account holders — a change still needs a human commit</td></tr>
<tr><td>Sinetti PC weekly INS refresh</td><td>Operator machine</td><td>Mon 10:00</td><td>INS register + three capped INS PDF stages</td></tr>
<tr><td>Sinetti PC quarterly company groups</td><td>Operator machine</td><td>1 Jan / Apr / Jul / Oct, 06:45</td><td>Places new holders into corporate groups and records ownership, draining every queue slice by slice (no per-quarter cap; stops after 66 h and resumes next run). Log <code>data/refresh/company_group_drain.log</code>. Needs the operator signed in to Claude Code. See <a href="#curation">§10</a></td></tr>
<tr><td>Law-layer monthly refresh</td><td>Operator machine</td><td>every 4 weeks, Mon 07:30</td><td>Re-fetches regulation texts Ask can cite</td></tr>
<tr><td>Consumption Eurostat monthly</td><td>Operator machine</td><td>every 4 weeks, Mon 05:00</td><td>Market-balances statistics (not deployed)</td></tr>
</table></div>
<p>If the operator machine is off on Monday, INS simply ages and the app says so honestly in its freshness line — visible degradation, never silent. The three INS PDF stages default to a cap of <b>zero</b> in the runner, so an unset machine never parses an INS PDF. Registering the weekly task (<code>scripts/register_ins_weekly_task.ps1</code>) raises each cap to <b>1,200</b> when <code>.env</code> omits it or sets it lower — the same drain budget as the nightly certificate-PDF cap — and sets a four-hour cutoff. Re-register after pulling that script; the cutoff is stored on the task, and the caps are stored in <code>.env</code>. Leave the uncached switches unset unless you deliberately want the local task to refetch from GSE. With <code>INS_PDF_SITE_REEXTRACT=1</code> the site stage also re-reads documents that already have sites, and replaces them only when nothing is lost. A criteria batch that cannot improve is a Trust Review finding, not a failed run that withholds the whole publication.</p>
<h3>How to tell a run really happened</h3>
<ul>
<li><b>Admin → Data ops</b> shows a card per register with a status pill (<code>ok</code> · <code>error</code> · <code>held</code> · <code>not due</code> · <code>no data</code>), what changed (“+12 new · 40 renewed · 3 withdrawn”, and suspended, delisted and terminated on their own — a termination is the holder’s exit and is not a withdrawal), and how the PDF queues moved. Below the briefing, <b>Parse inflow, next 12 months</b> counts certificates still in force onto the day each one ends: one renewal PDF that day, and for ISCC one summary audit report. The numbers are read from the serving release when you open the page. A <b>section bar</b> stays pinned at the top while you scroll the long Data ops pages (Refresh, Sources) and the other long admin pages (Data Trust, Regulatory monitor, System health): the hub tabs plus one pill per section, with the pill of the section in view highlighted; click a pill to jump to that section (a closed section opens). Right after the three summary tiles, <b>Documents still to read — day by day</b> shows line charts, one point per day a refresh ran, ending at the data the live app serves: the last point is the promoted data release’s own count, so the charts move when new data is promoted, and a nightly count on data not yet promoted is not drawn. They keep <b>Digital</b> and <b>Scanned</b> documents apart. A digital PDF has a text layer the parser reads directly; a scanned PDF is an image that needs OCR before anything can be read. <b>Digital certificate PDFs waiting</b> has one small chart per register, each with one line per reason a digital PDF is still waiting (layout not read yet, because the parser does not know the layout; waiting for a check; download to retry), each chart with its own scale. <b>Scanned certificate PDFs waiting</b> keeps one line per register; under the scanned chart a line says how many scanned ISCC certificates the morning OCR run has already tried (and still could not read) against how many it has not tried yet — ISCC and REDcert report the morning OCR queue; INS reads “no OCR job”. <b>ISCC audit reports waiting</b> draws a Digital and a Scanned line. Hover a day for the numbers: on the scanned and audit charts the split into parser, check and retry (scanned days read “needs OCR”); the 30 days / 90 days / All switch sets every chart. A day without a point had no refresh for that register (INS runs weekly). Days recorded before 9 October 2026 were not split when written: they are <b>estimates</b>, shaded and labelled, worked out by sorting the documents that were waiting that day by their reason today (a scan that was later read counts as digital on the days before). In the digital charts only the “layout not read yet” line is estimated on shaded days; check and retry were recorded exactly. From the next nightly data refresh each day stores its own split and is exact. For ISCC and REDcert the waiting counts include only certificates still in force, the same rule as Data Trust <b>Accounted</b>; INS is unchanged. The charts mark the day this rule started with a vertical line labelled “Current certificates only”: the drop on that day is the rule change, not reading, and hovering that day says how many waiting PDFs of ended certificates stopped counting. Under those charts, <b>Holders on the map — in force only</b> counts, per scheme, the holders with a certificate, pathway or registration in force that draw a point on the maps (one unit per holder and country, so a holder in two countries counts twice). One card lists the certification schemes, one the regulatory programmes: a bar for the share on the map, located against in force, the percentage and how many are not on the map (no plant, address or postcode point, so missing from Browse, Certified Sites and the deep-dive maps). The count is taken on the data the live app serves, so it moves when new data is promoted; each nightly data refresh also records its own count, and once two nights exist each card draws a <b>Not on the map — day by day</b> line. The <b>Queue tables</b> section further down carries the full daily movement per register, and <b>Register history</b> shows the last 14 nights (a link shows all of them). Under the three summary tiles, <b>Serving on sinetti.ch</b> compares each register's data on the live app with the newest release in the dataset repository. It recommends a release only when every served register remains present with a valid date that has not moved backwards, and at least one register is newer or newly added. The command to run on your PC is pre-filled with that release (Copy button); the app never moves production itself. When newer data waits, the card headline names the register and the date of its waiting data (for example <b>Newer ISCC waiting</b> 25 Oct 26 · 02:00), and the line under it says what promoting changes and which registers on the same schedule have nothing newer; the table lists only each register’s schedule, live date and status. When the newest release holds newer data but the code deployed on sinetti.ch cannot pull it (for example a register folder that code does not know), the card headline reads <b>Needs a code deploy first</b>, a red strip quotes the reason, and no command is offered until a deploy ships code that can read it; <code>promote_release_on_space.py --list</code> marks the same release. An invalid or regressed candidate is shown as needing review, and an equal-date release with possible PDF work needs manual review. A green newest-data line requires trustworthy evidence or the already served release. While the server is running on partial data (a slow start left company identities out, so holder pages show no company links), the same card shows a terracotta strip saying so and roughly how many seconds remain until it reloads by itself; nothing needs doing unless it is still there after a few minutes, then restart the server.</li>
<li><b>Admin → Data ops → Sources</b> tags each source <i>on schedule</i>, <i>overdue n days</i> or <i>manual</i>. “Overdue” is computed purely from how old the newest evidence is — the one signal a killed job cannot fake.</li>
<li>For the nightly job, the authority is the job's own status on Hugging Face (completed / error). In August 2026 every night for two weeks died of a timeout and wrote no receipt; the app's log kept showing the last good night.</li>
</ul>
</div>
<div class="d3">
<h3>Procedures</h3>
<details class="proc" id="p-check-nightly"><summary>Check the nightly job's true status<span class="k">Desk · 2 min</span></summary><div class="body">
<ol class="steps">
<li>From the operator machine, list recent job runs and the active schedule:
<pre><code>python -c "from huggingface_hub import HfApi; a=HfApi(); [print(j.id, j.status.stage, j.created_at) for j in list(a.list_jobs())[:8]]"
python -c "from huggingface_hub import HfApi; [print(j.id, j.schedule, 'SUSPENDED' if j.suspend else 'ACTIVE') for j in HfApi().list_scheduled_jobs()]"</code></pre>
<div class="expect">last night's run <code>COMPLETED</code>, and exactly one nightly data refresh job <code>ACTIVE</code>. The morning cloud OCR job and the weekly register jobs are separate schedules.</div></li>
<li><code>ERROR</code> with “Job timeout” → the run outgrew its time budget: re-register with a longer timeout or lower caps. <code>ERROR</code> exit 137 → out of memory. Any other error → open the job's log on Hugging Face.</li>
</ol></div></details>
<details class="proc" id="p-register-nightly"><summary>Change the nightly job's settings (re-register)<span class="k">Desk · 15 min</span></summary><div class="body">
<ol class="steps">
<li>Preview first — this prints the full job definition and creates nothing:
<pre><code>python scripts\register_hf_daily_refresh_job.py --dry-run</code></pre></li>
<li>Register with <b>every</b> setting spelled out. A flag you omit falls back to its default, and the default for every PDF cap is 0 — a careless re-registration silently switches parsing off.
<pre><code>python scripts\register_hf_daily_refresh_job.py --schedule "0 0 * * *" --state-dir /data/sinetti `
--bucket-volume Couscouskabyle/iscc-app-storage --refresh-schemes iscc,redcert `
--geocode-schemes iscc,redcert,ins --allow-partial --timeout 270m --flavor cpu-upgrade `
--pdf-delta-max-pdfs 1200 --pdf-delta-prefer-new-documents `
--audit-delta-max-pdfs 100 --audit-delta-prefer-new-documents `
--redcert-pdf-max-fetches 300</code></pre></li>
<li>Suspend the previous job in the Hugging Face Jobs screen, then confirm exactly one is active (previous procedure).</li>
<li>Watch the next night's duration. Raise caps only with clear headroom under the timeout.</li>
</ol></div></details>
<details class="proc" id="p-rehearse"><summary>Rehearse a refresh now, with every outward effect off<span class="k">Desk · minutes to about 2 h</span></summary><div class="body">
<p>Use this after a refresh change, instead of waiting for the scheduled slot. Real work: downloads, schema checks, PDF parsing, corpus bands, rebuild, Trust Review, composed emails, and the shape of the single-commit release. Withheld: the dataset commit, activation, Space upload and restart, mail delivery, paid geocoding, and the Hub control-branch write. A scratch state directory is used. The run refuses during 00:00–02:00 UTC and while another session holds <code>data-release</code>.</p>
<ol class="steps">
<li>List the jobs, then run one or all:
<pre><code>.\.venv\Scripts\python.exe scripts\rehearse_refreshes.py --list
.\.venv\Scripts\python.exe scripts\rehearse_refreshes.py us
.\.venv\Scripts\python.exe scripts\rehearse_refreshes.py all --keep-workspace</code></pre>
<div class="expect">a PASS or FAIL line per job. <code>rehearsal: true</code> on a receipt means it did not publish.</div></li>
<li>A PC rehearsal cannot reproduce a cloud timeout, an out-of-memory kill, or the Space's symlink layout. For that, <code>scripts\rehearse_hf_nightly.py</code> uploads the current checkout to dataset branch <code>rehearsal-code</code> (never the Space, never a <code>main</code>) and runs one job with the live scheduled job's settings. It is a paid cloud job: state the cost ceiling and get approval before launching it. Runbook: <code>docs/rehearsal-runbook.md</code>.</li>
</ol></div></details>
<details class="proc" id="p-local-task"><summary>Check, run or re-create a weekly task on the operator machine<span class="k">Desk · 5 min</span></summary><div class="body">
<ol class="steps">
<li>State and last result:
<pre><code>Get-ScheduledTask -TaskName "Sinetti*" | Get-ScheduledTaskInfo | Format-Table TaskName,LastRunTime,LastTaskResult,NextRunTime</code></pre></li>
<li>Run one now: <code>Start-ScheduledTask -TaskName "Sinetti PC weekly INS refresh"</code>, then read its log, e.g. <code>Get-Content data\refresh\ins_local\runner.log -Tail 20</code>.</li>
<li>Re-create on a new machine with the matching script in <code>scripts\</code>: <code>register_ins_weekly_task.ps1</code>, <code>register_us_weekly_task.ps1</code>, <code>register_orbc_weekly_task.ps1</code>, <code>register_nea_weekly_task.ps1</code>, <code>register_local_hf_sync_task.ps1</code>, <code>register_trust_monitor_task.ps1</code>, <code>register_regulatory_monitor_task.ps1</code>, <code>register_law_monthly_task.ps1</code>, <code>register_nightly_suite_task.ps1</code>, <code>register_company_group_task.ps1</code>, <code>register_resume_interrupted_gate_task.ps1</code>, <code>register_customer_state_backup_task.ps1</code>. <span class="pill gap">Gap</span> there is no registration script for the CFR task in the repository. Re-register the company-group task after changing <code>-Model</code> (current: <code>claude-sonnet-5</code>).</li>
<li><b>Always verify with <code>Get-ScheduledTask</code></b> — a registration script can print success after Windows refused the task.</li>
<li>An INS run whose receipt says <code>upload.status = skipped-pdf-stage-error</code> withheld publication on purpose: one of the three PDF stages failed. Read that stage in <code>data\refresh\ins_local\last_ins_local_refresh.json</code>.</li>
</ol></div></details>
<details class="proc" id="p-weekly-cloud"><summary>Move the US, CFR, Oregon + BC and Brazil (RenovaBio) weekly refreshes to Hugging Face<span class="k">Desk · 15 min, once</span></summary><div class="body">
<p>Approved 28 September 2026 (brief <code>docs/superpowers/briefs/2026-09-28-weekly-registers-to-hf-jobs-brief.md</code>). <span class="pill gap">Not done yet</span> the US, CFR and OR-BC rows above still run on the operator machine, and the Brazil RenovaBio row is cloud-only from the start (runbook <code>docs/renovabio-refresh-runbook.md</code>). The cloud jobs run the <b>deployed</b> Space code, so this can only start after a deploy that includes the four refresh scripts.</p>
<ol class="steps">
<li>Preview: <code>python scripts\register_hf_weekly_register_jobs.py</code>. It covers all four cloud registers (US, CFR, OR-BC, RenovaBio), prints each job's schedule (UTC) with secrets masked and registers nothing. <code>--registers renovabio</code> narrows it to that one register.</li>
<li>Register: add <code>--apply</code>. It refuses while the deployed Space lacks a script, and leaves alone any register that already has an active scheduled job.</li>
<li>After each register's first cloud slot, confirm the job reads <code>COMPLETED</code> in <code>HfApi().list_jobs()</code> and the register shows fresh data under Data ops → Sources. Then disable that register's machine task (RenovaBio has none), e.g. <code>Disable-ScheduledTask -TaskName "Sinetti PC weekly US refresh"</code>.</li>
<li>Switch back: <code>Enable-ScheduledTask</code> on the machine task, and suspend the cloud job in the Hugging Face Jobs page.</li>
</ol></div></details>
</div>
</section>
<section class="area topic--data" id="releases">
<h2><span class="topicIcon"><svg class="guideIcon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d='M12 3v12'/><path d='m7 10 5 5 5-5'/><path d='M5 21h14'/></svg></span><span class="n">8</span>How data goes live</h2>
<p class="where"><b>Where:</b> operator machine · Hugging Face Jobs · <code>/api/ready</code></p>
<div class="short"><b class="tag">In short</b>
<p>What customers see is a <b>release</b>: one database file plus its manifest, stored under its revision id and never modified afterwards. A one-line <b>active pointer</b> names the release being served. Going live means moving that pointer — nothing is copied over anything, so going back is just moving it again. The app reads the pointer once, at start: a new release appears after the next restart, never mid-session. If the active release turns out broken at start, the app heals itself by stepping back to the previous good one, and as a last resort serves the data it was shipped with.</p>
</div>
<div class="d2">
<figure>
<svg viewBox="0 0 1000 300" role="img" aria-label="A candidate is pulled and validated, then promoted into an immutable release directory. The active pointer selects which release the app serves; previous releases and bundled data are fallbacks.">
<rect class="bx" x="10" y="40" width="130" height="60" rx="8"/><text class="t" x="75" y="66">Candidate</text><text class="s" x="75" y="84">dataset repo commit</text>
<rect class="bx" x="180" y="40" width="130" height="60" rx="8"/><text class="t" x="245" y="66">Staged</text><text class="s" x="245" y="84">checksums verified</text>
<rect class="bx2" x="350" y="40" width="130" height="60" rx="8"/><text class="t" x="415" y="62">Guards</text><text class="s" x="415" y="78">schema · counts ·</text><text class="s" x="415" y="92">no register shrank</text>
<rect class="gate" x="520" y="52" width="100" height="36" rx="18"/><text class="gt" x="570" y="75">PROMOTE</text>
<rect class="bx" x="660" y="40" width="130" height="60" rx="8"/><text class="t" x="725" y="66">Release</text><text class="s" x="725" y="84">immutable, by revision</text>
<rect class="app" x="830" y="40" width="160" height="60" rx="8"/><text class="t" x="910" y="66">Active pointer</text><text class="s" x="910" y="84">read once, at app start</text>
<path class="ar" d="M140 70H178" marker-end="url(#ah)"/><path class="ar" d="M310 70H348" marker-end="url(#ah)"/><path class="ar" d="M480 70H518" marker-end="url(#ah)"/><path class="ar" d="M620 70H658" marker-end="url(#ah)"/><path class="ar" d="M790 70H828" marker-end="url(#ah)"/>
<rect class="bx" x="660" y="170" width="130" height="56" rx="8"/><text class="t" x="725" y="194">2 previous releases</text><text class="s" x="725" y="212">kept for rollback</text>
<path class="ard" d="M790 198H870V102" marker-end="url(#ah)"/><text class="al l" x="800" y="190">rollback · self-heal</text>
<rect class="bx2" x="830" y="236" width="160" height="50" rx="8"/><text class="t" x="910" y="258">Bundled data</text><text class="s" x="910" y="274">shipped with the app</text>
<path class="ard" d="M950 234V102" marker-end="url(#ah)"/><text class="al r" x="944" y="226">last resort</text>
<text class="s l" x="10" y="150">The pointer can be moved by: you (manual promotion / rollback), the app at start (self-heal),</text>
<text class="s l" x="10" y="168">or automation after a streak of clean nights — if switched on. Each move records who made it.</text>
<text class="s l" x="10" y="200">This whole store exists twice: once in production's storage bucket,</text>
<text class="s l" x="10" y="218">once on the operator machine. Promoting one does not promote the other.</text>
</svg>
<figcaption>Going live is a pointer move. Because releases are never edited, “roll back” cannot half-fail, and a bad release cannot damage a good one.</figcaption>
</figure>
<h3>Who may move the pointer</h3>
<div class="tw"><table>
<tr><th>Mover</th><th>When</th><th>Guards applied</th></tr>
<tr><td><b>You — manual promotion</b></td><td>After reviewing a candidate</td><td>Exact revision match (checked three times), deep validation of the database. The shrink guard is <i>not</i> applied: a human may knowingly promote a smaller register.</td></tr>
<tr><td><b>You — rollback</b></td><td>The new release misbehaves</td><td>Moves to the newest retained release other than the current one</td></tr>
<tr><td><b>The app — self-heal</b></td><td>At every start, if the active release fails validation</td><td>Steps back to the newest retained release that validates. If none does, serves bundled data for that process only and changes nothing on disk.</td></tr>
<tr><td><b>Automation — green streak</b></td><td>Only if enabled by a setting; off by default</td><td>N consecutive valid nightly candidates <b>and</b> corpus counts in band <b>and</b> the shrink guard: no register missing, <b>no register with even one certificate fewer</b>, total not down more than 20 %. Refuses to replace a pointer that a human set.</td></tr>
</table></div>
<p>The strict shrink rule exists because a real accident was a 0.65 % loss — 507 certificates — that a percentage threshold would have waved through. Registers do shrink legitimately; that judgement is reserved for a person.</p>
<h3>Reading <code>/api/ready</code></h3>
<div class="tw"><table>
<tr><th>Field</th><th>Healthy</th><th>Meaning when not</th></tr>
<tr><td><code>data.release.source</code></td><td><code>releases</code></td><td><code>bundled</code> = the pointer is missing, unreadable, or every release failed validation. The app is up but serving the older shipped data; <code>data.release.reason</code> says why.</td></tr>
<tr><td><code>data.release.revision</code></td><td>the revision you last promoted</td><td>A different revision = someone or something else moved the pointer; <code>null</code> = bundled.</td></tr>
<tr><td><code>data.ok</code></td><td><code>true</code></td><td><code>false</code> with <code>validation: critical</code> = the database being served fails its own manifest. The endpoint answers HTTP 503.</td></tr>
</table></div>
<div class="warn"><b>Never open, copy or replace the database file under <code>data/</code> by hand.</b> That pair is the shipped fallback; it lags the active release and answers questions wrongly without any error. Inspection goes through the app's own loader; going live goes through promote.</div>
</div>
<div class="d3">
<h3>Procedures</h3>
<details class="proc" id="p-promote-prod"><summary>Promote a reviewed candidate in production<span class="k">Desk · 15 min + restart</span></summary><div class="body">
<ol class="steps">
<li><b>Identify the candidate.</b> In the dataset repository's history, note the full 40-character commit id of the nightly you want. Confirm in Admin → Data ops that this night was <code>ok</code> for each register and read its Trust Review line.</li>
<li><b>Review it.</b> Stage it on the operator machine and look at it in the local app (next procedure). Check the counts per register against the active release; a drop needs an explanation you believe.</li>
<li><b>Preview the production promotion</b> — rechecks the exact selected commit against the live source dates, then prints the job and creates nothing. Missing, older or invalid source evidence refuses by default; ordinary force flags do not bypass this check. Equal-date PDF work can be selected manually after review. It also runs the code deployed on the Space against the release's file list (about ten seconds): when that code would refuse the release, it stops with <code>REFUSING: the deployed app would refuse this release: …</code> — the release needs a code deploy first, and a cloud job would only end in ERROR. <code>--list</code> marks such a release “needs a code deploy first” instead of recommending it:
<pre><code>python scripts\promote_release_on_space.py --hub-revision <40-char-commit-id> --dry-run</code></pre></li>
<li><b>Run it.</b> This launches a one-off cloud job that mounts production's storage, pulls exactly that revision, validates it and moves the pointer, and the script watches the job to completion. A failed start, log read or restart prints a plain sentence. If a newer nightly published meanwhile, the revision no longer matches and the job refuses — the current release keeps serving. To promote the commit the Data ops card recommends, resolved at the moment you run it, use <code>--recommended</code> instead of <code>--hub-revision</code>. It refuses when nothing is newer. It cannot be combined with <code>--allow-data-rollback</code>.
<pre><code>python scripts\promote_release_on_space.py --hub-revision <40-char-commit-id>
python scripts\promote_release_on_space.py --recommended</code></pre>
<div class="expect">a job id; in its log <code>PROMOTE: {… "status": "activated" …}</code>.</div></li>
<li><b>Restart the Space</b> (Hugging Face → Space → Settings → Restart). The running app never swaps data mid-session.</li>
<li><b>Verify:</b> <code>https://couscouskabyle-iscc-app.hf.space/api/ready</code> shows <code>status: ready</code>, <code>data.release.source: releases</code> and your revision.</li>
<li><b>Intentional rollback only:</b> after reviewing an older release, use its full commit id and <code>--allow-data-rollback</code> on the dry run and real command. It cannot accompany <code>--recommended</code>, still refuses invalid evidence, and does not disable the job's revision checks. Local preflight is not a cloud lock; verify the served revision and source dates after restart.</li>
</ol>
<div class="note">Whether production also promotes by itself depends on a Space variable (<code>SINETTI_AUTO_ACTIVATE_NIGHTS</code>, and the nightly job's <code>SINETTI_PROD_AUTO_ACTIVATE</code>). Read the current values in the Space and job settings. The pointer file records <code>by</code>: <code>manual-promotion</code>, <code>manual-rollback</code>, <code>boot-self-heal</code> or <code>auto-activation</code> — that is how you tell what moved it last. When the nightly data refresh holds production activation for more than a day, the admin gets one reminder mail per day (“production data activation still held since …”) naming the reason, the live revision and the newest published revision, until activation succeeds.</div></div></details>
<details class="proc" id="p-promote-local"><summary>Stage and promote on the operator machine<span class="k">Desk · 10 min</span></summary><div class="body">
<ol class="steps">
<li>Pull the newest candidate without touching what the local app serves:
<pre><code>python scripts\local_hf_sync_once.py --force</code></pre>
<div class="expect"><code>data\refresh\local_hf_sync.json</code> shows <code>source: dataset</code>, <code>space.status: pending-promotion</code> and the revision you expect.</div></li>
<li>Promote exactly that revision, then restart the local app:
<pre><code>python scripts\promote_data_release.py --hub-revision <40-char-commit-id></code></pre>
<div class="expect"><code>status: activated</code>. Any other status ends with <code>decision: kept-current</code> — nothing was half-applied.</div></li>
<li><code>error-locked</code> means another promotion holds the lock (or died holding it); <code>error-revision-mismatch</code> means the staged candidate is not the one you named — sync again.</li>
</ol>
<div class="note">The company-group job's local rebuild (<code>scripts\rebuild_local_release.py</code>) is a different command. It starts from the retained local release with the freshest register data, recomputes only the company layers, and refuses when another session holds <code>data-release</code>, when any register would be older than the base, or when a register would shrink by more than 2%. It does not read this desk's bundled register files.</div></div></details>
<details class="proc" id="p-rollback-data"><summary>Roll serving data back<span class="k">Desk · 5 min + restart</span></summary><div class="body">
<ol class="steps">
<li>On the operator machine: <code>python scripts\promote_data_release.py --rollback</code>, then restart the app. This reaches two releases back; older ones need a fresh sync and promote.</li>
<li>In production: promote the <i>previous</i> revision id with the production procedure above, then restart the Space. If the active release is outright broken you need do nothing — self-heal steps back at the next start.</li>
<li>This is <b>data</b> rollback. Rolling the <b>app version</b> back is a different mechanism (<a href="#shipping">§13</a>); never claim one from the other.</li>
</ol></div></details>
<details class="proc" id="p-after-parse"><summary>After a PDF-parsing campaign: the promote checklist<span class="k">Desk · 1–2 h</span></summary><div class="body">
<p>Whenever parsed-PDF counts advance outside the nightly (a repair campaign, an OCR batch), finish all of this in the same sitting. Skipping the later steps leaves Data Trust current while the Product page, Certificate formats, the count guard and the served release silently drift.</p>
<ol class="steps">
<li>Commit the updated parsed-PDF archive <code>data/cert_pdf_details.sqlite.gz</code>.</li>
<li>Rebuild the structured layer and the app database:
<pre><code>$env:APP_BACKEND = 'local'
.\.venv\Scripts\python.exe scripts\build_cert_pdf_structured_db.py --source-db data\cert_pdf_details.sqlite `
--work-db data\cert_pdf_structured.sqlite --artifact data\cert_pdf_structured.sqlite.gz --replace
.\.venv\Scripts\python.exe -c "from scripts.rebuild_sinetti_app_artifact import rebuild_bundled_app_database; print(rebuild_bundled_app_database())"</code></pre></li>
<li>Run the follow-ons in one command and commit <code>data/cert_format_catalog.json</code> and <code>data/corpus_counts.json</code>. It rebuilds the certificate-format catalogue when it was not built from the archive you just committed, then refreshes the corpus count bands. Exit 1 = a count left its band: investigate, never widen a band to make it pass. The band refresh records which artifact it measured, so run it on the desk holding the corpus you mean to set bands from. A register sitting at zero (<code>certificates_oregon</code>, <code>certificates_bc</code>, ...) means that register left the artifact entirely. Forgetting the catalogue is now caught: the test gate fails until the catalogue matches the committed archive. Admin → Certificate formats says the same thing on the page: the status line at the right of the tab row turns to the warning colour and reads "Out of date — rebuild by hand after a PDF-parsing promote", with an "Out of date" strip below, when the catalogue does not match the archive the server reads. Ask knowledge, Research coverage and Site footprint each carry a one-line note saying they update only by hand.
<pre><code>.\.venv\Scripts\python.exe scripts\finish_details_promote.py</code></pre></li>
<li>Stage and activate the rebuilt pair through promote — never by copying files.</li>
<li>Open Admin → Data Trust: snapshot date, per-register rows and backlog totals must match your campaign notes. Spot-check Product and Certificate formats.</li>
</ol>
<div class="note">Audit-report-only progress needs steps 1, 3 and 5 only.</div></div></details>
</div>
</section>
<section class="area topic--data" id="trust">
<h2><span class="topicIcon"><svg class="guideIcon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d='M12 3l7 3v5c0 4.4-3 8-7 10-4-2-7-5.6-7-10V6z'/></svg></span><span class="n">9</span>Is the data right?</h2>
<p class="where"><b>Where:</b> Admin → Data ops · Admin → Data Trust (tabs <i>Overview</i> · <i>Formats</i> · <i>Admin QA</i> · <i>User reports</i>)</p>
<div class="short"><b class="tag">In short</b>
<p>Every candidate is inspected by <b>Trust Review</b>, an automatic reviewer that files <b>findings</b> — “this register looks stale”, “these identities disagree”, “coverage fell”. Findings are advice for you, kept in a store of their own: they never reach customers, never block anything by themselves, and never send email. Your part is to read the morning briefing, give each finding a disposition with a reason, and know the state of the PDF-parsing backlog. A separate monitor emails you the daily workload, and when a refresh, a review, a repeated application error, or the PC daily test run failed (<a href="#health">§14</a>).</p>
</div>
<div class="d2">
<figure>
<svg viewBox="0 0 1000 250" role="img" aria-label="Trust Review inspects each candidate and writes findings to an isolated store read only by admin screens. A separate monitor watches for missing runs and emails the admin.">
<rect class="bx" x="20" y="40" width="150" height="60" rx="8"/><text class="t" x="95" y="66">Candidate</text><text class="s" x="95" y="84">after each refresh</text>
<rect class="bx" x="230" y="40" width="220" height="60" rx="8"/><text class="t" x="340" y="62">Trust Review — 5 checks</text><text class="s" x="340" y="78">freshness · status evidence ·</text><text class="s" x="340" y="92">identity · coverage · publication</text>
<rect class="bx2" x="510" y="40" width="180" height="60" rx="8"/><text class="t" x="600" y="66">Findings store</text><text class="s" x="600" y="84">own file · admin only</text>
<rect class="gate" x="750" y="52" width="110" height="36" rx="18"/><text class="gt" x="805" y="75">DISPOSITION</text>
<text class="s l" x="872" y="66">state + reason,</text><text class="s l" x="872" y="82">kept as history</text>
<path class="ar" d="M170 70H228" marker-end="url(#ah)"/><path class="ar" d="M450 70H508" marker-end="url(#ah)"/><text class="al" x="479" y="62">files</text><path class="ar" d="M690 70H748" marker-end="url(#ah)"/><text class="al" x="719" y="62">you read</text>
<rect class="bx" x="230" y="160" width="220" height="56" rx="8"/><text class="t" x="340" y="184">Job monitor · daily 05:30</text><text class="s" x="340" y="202">“did the run and its review happen?”</text>
<rect class="bx" x="550" y="160" width="180" height="56" rx="8"/><text class="t" x="640" y="184">One alert email</text><text class="s" x="640" y="202">then one recovery email</text>
<path class="ar" d="M450 188H548" marker-end="url(#ah)"/><text class="al" x="499" y="180">missing / failed</text>
<text class="al l" x="20" y="192">runs on its own —</text><text class="al l" x="20" y="208">a job cannot report its own absence</text>
</svg>
<figcaption>Two different questions, two different mechanisms. <i>Is the data plausible?</i> → findings, read on screen, never emailed. <i>Did the machinery run?</i> → the monitor, which emails. The findings store is deliberately disconnected from customer pages: if it is corrupt or locked, only the admin evidence degrades — Browse, exports, <code>/api/health</code> and <code>/api/ready</code> carry on unchanged.</figcaption>
</figure>
<h3>The screens</h3>
<p><b>Map coordinate delivery (checked 8 October 2026).</b> The Sinetti operator reads <code>coordinate_workspace</code> in the Data ops refresh receipt. Newly recovered holder, address and site points reach Browse, Producers / Certified Sites and the deep-dive maps after the rebuilt data candidate is accepted and activated. An unchanged checkout baseline protects nightly positions; an actual later hand correction supersedes them. Town/postcode fallback evidence stays approximate. A workspace error preserves unreadable inputs and stops the candidate: restore a verified usable copy, keep the receipt and rerun the isolated job. Interrupted coordinate/provenance writes recover from their transaction journal; do not delete it. A successful receipt alone does not establish activation or complete address coverage. Procedure: <code>docs/daily-refresh-runbook.md</code>, “Coordinate delivery and hand corrections”.</p>
<p><b>Map coverage accounting (checked 8 October 2026).</b> The raw refresh report now includes <code>coordinate_coverage</code>: the familiar distinct World holder total, separate certificate-address and physical-site counts, pending/ambiguous/negative work, and changes on retained identities. New holders are shown separately from lost coordinates. A stored singleton pin avoids an unnecessary paid request; another unresolved address remains in scope. Town/postcode evidence is approximate and leaves the site task open. An advisory audit error or damaged prior inventory says coverage/history unavailable and preserves the last readable report. The Sinetti operator can propose zero-cost recovery in a separate bundle using verified published caches and strict local place evidence; acceptance and activation remain separate. The large postcode index is never loaded by nightly accounting. Procedure: <code>docs/daily-refresh-runbook.md</code>, “Coverage inventory and zero-cost catch-up”.</p>
<div class="tw"><table>
<tr><th style="width:190px">Screen</th><th>What it answers</th><th>Can you change anything?</th></tr>
<tr><td><b>Data ops</b><br><span class="where">Admin → Data ops</span></td><td>The morning briefing: per-register run status, certificate changes, PDF and audit queue movement (opening → +added → −done → closing), the next-12-month parse inflow counted from certificate end dates, backlog line charts over days, history tables, the raw refresh report, the geocoding budget, and previews of tonight's emails</td><td>No — read only</td></tr>
<tr><td><b>Data Trust · Overview</b><br><span class="where">Admin → Data Trust</span></td><td>In-force certificate PDFs per register as <b>Accounted</b> (read or a named exception) and <b>Read</b>, with parser and manually verified reads shown separately; the list of certificates not accounted and why; then how all certificate PDFs add up: parsed, no pathways found, accepted exceptions, remaining work split into <i>parser backlog</i>, <i>data-trust review</i> and <i>acquisition retry</i>; audit-report coverage; plus three review queues (below)</td><td>Yes — entity identity, related companies, finding dispositions</td></tr>
<tr><td><b>Formats</b></td><td>Every distinct certificate layout per register (“ISCC Type 1” …) with up to five sample PDFs, tagged <i>Good</i> / <i>Needs action</i> and <i>Machine</i> / <i>Human</i> work. Unfamiliar digital annex tables wait at the top, one shape at a time: the annex page of one specimen, shared by every certificate with that table</td><td>Yes — materials annex, or empty template. Skip leaves it in the queue. The gallery underneath stays a reference</td></tr>
<tr><td><b>Admin QA</b></td><td>The inbox of individual parsing and interpretation issues; stage decisions, preview their effect, write the input the next re-parse consumes</td><td>Yes — staged, then explicitly written</td></tr>
<tr><td><b>User reports</b><br><span class="where">Admin → Data Trust</span></td><td>Customer data problems (from a dossier row or the dossier’s “Tell us” line) and product feedback (footer <b>Feedback</b>), each with the holder, field, shown value, serving releases and the customer’s note</td><td>Yes — close each report with an outcome and a short note; the customer sees it under Activity. It never creates a finding or changes data</td></tr>
</table></div>
<p>“Balanced operating lanes” and “Acquisition retry preview” on the Overview are planning aids. The lane caps are read from the nightly receipt, not from a number baked into the page. They suggest cadence and show what a retry would pick; they cannot start or schedule anything. Reason codes on this page and on Certificate formats use plain labels (for example “Ticked box could not be read”).</p>
<p>A customer report of a wrong or missing record starts with <code>docs/enterprise/hosted-bilateral/data-correction-procedure.md</code>. The report lands in <b>User reports</b> and does not itself create a finding. Record the intake, the reply to the customer, and the source-versus-served evidence before you change anything.</p>
<h3 id="pdf-live-coverage">Verified live PDF coverage</h3>
<p>The Sinetti operator reads three separate results: current <b>source checks</b>, <b>production reconciliation</b>, and reciprocal <b>monitor liveness</b>. The existing Accounted and read screen describes the selected artifact; a local parse or published candidate does not establish production completeness. Reconciliation needs the verified active revision/hash, full expected inventory including required annexes, and independent code, source, API and customer evidence. Missing evidence is <b>unknown</b>. INS live recovery/publication is owner-deferred while ISCC and REDcert are prioritized; INS remains visibly unresolved in the full target, with its local support and history retained.</p>
<p>Every expected current document needs a successful source check within seven days, including successes and accepted exceptions. Full current bytes or an authoritative digest establish freshness; unchanged URL, HEAD or file length does not. Source-only checks can acquire new PDFs without earlier Read/customer proof, while production coverage stays unknown. A changed file immediately supersedes old current proof, even if earlier good rows are preserved after a failed replacement. Source blockers stay in the denominator, are rechecked weekly and never count as <b>Read</b>. Keep diagnosed <b>Failure A–E</b> labels separate from OCR residual Types.</p>
<p>The new lanes default <b>off</b>. <code>SINETTI_PDF_SOURCE_AUDIT_ENABLED=1</code> and <code>SINETTI_PDF_COVERAGE_MONITOR_ENABLED=1</code> are separate explicit opt-ins; audit caps default to zero. An independent request and its SHA256 must be configured through <code>SINETTI_PDF_COVERAGE_INPUT_REQUEST</code> and <code>SINETTI_PDF_COVERAGE_INPUT_REQUEST_SHA256</code>. Local code changes leave existing jobs, flags and schedules as configured. Enabling, source runs, publication, activation and deployment are separate authorized operations; Environment has no control for these flags.</p>
<p>The exact audit caps are <code>SINETTI_PDF_SOURCE_AUDIT_MAX_CHECKS</code>, <code>SINETTI_PDF_SOURCE_AUDIT_ISCC_MAX</code>, <code>SINETTI_PDF_SOURCE_AUDIT_REDCERT_MAX</code> and <code>SINETTI_PDF_SOURCE_AUDIT_INS_MAX</code>. Inspect <code>refresh/pdf_coverage/source-audit.json</code>, exactly <code>{state, report, receipt_binding}</code>, against both full six-field inventories and their independent inventory pins. Latest negative observations and unbound changed requirements remain holds despite retained older success. Only genuine processing adds <code>report_json.pdf_source_freshness</code>; operational job reports use <code>source_version_audit</code>. An unchanged source check creates no generic parse run. The shipped acquisition adapter checks full bytes; independently required ISCC audit reports use verified bytes in both existing sequential and overlap parser routes under existing audit caps. Required REDcert/INS audit-report roles remain explicit unknown/pending in the full denominator because their processing routes are unsupported; unrelated audit reports are not added.</p>
<p>Source-audit JSON reads allow 16 MiB without truncation; narrow heartbeat/hosted-check reads remain 1 MiB. Actual full-population payload capacity is unmeasured. The <b>14-date/two-activated-INS operational gate remains open after the local code merge</b>: current full-population proof, measured capacity, actual deployed/scheduled operation, source/customer evidence and reciprocal liveness remain necessary.</p>
<p>Proposed targets are acquired text PDFs within 24 hours, daily-lane scans within 48 hours, and INS discovery within seven days followed by 48-hour processing. Measure successful checks and queue ages inside the existing runtime/caps before claiming capacity. Local fixtures prove behavior and arithmetic only. Operational acceptance needs <b>14 consecutive Zurich scheduled dates</b> with completions at most <b>26 hours apart</b>, plus <b>two distinct genuine scheduled weekly INS accepted revisions activated and reconciled</b>. In-deadline arrivals lower literal coverage without alone resetting operational consistency. Visible, evidence-bound weekly-checked blockers may coexist with operational consistency; <b>literal 100% sustained</b> additionally needs zero incomplete/blocker cases at every counted checkpoint. Expiry is not recovery, and checkpoints do not prove continuous completeness between runs.</p>
<h3>Findings: states and what they mean</h3>
<div class="tw"><table>
<tr><th>State</th><th>Use it when</th></tr>
<tr><td>New</td><td>Nobody has looked yet. A resolved finding that comes back returns here with its history intact.</td></tr>
<tr><td>Investigating</td><td>You are looking into it</td></tr>
<tr><td>Known gap</td><td>Real, understood, accepted for now</td></tr>
<tr><td>Fix planned</td><td>Real, and work is scheduled</td></tr>
<tr><td>False positive</td><td>The check is wrong for this case</td></tr>
<tr><td>Resolved</td><td>Gone. Findings that stop appearing are resolved automatically.</td></tr>
</table></div>
<p>Each finding type is also classed <b>Blocking</b> or <b>Advisory</b>. The class matters only to automation: a candidate carrying a blocking-class finding is not promoted by itself. It never stops <i>you</i>. Unclassified types count as blocking. Priorities are critical · high · normal · information.</p>
</div>
<div class="d3">
<h3>Procedures</h3>
<details class="proc" id="p-morning"><summary>The five-minute morning check<span class="k">Screen</span></summary><div class="body">
<ol class="steps">
<li>Admin → Data ops. Read the date on “Last-run briefing” first — a briefing from two days ago is itself the finding.</li>
<li>Each register card: pill <code>ok</code>? Do the change numbers look like a normal day? A queue whose closing number never falls means parsing is switched off or stuck.</li>
<li>Follow a card's trust link — it opens Data Trust already filtered to that register's unresolved findings.</li>
<li>Expand <i>Email previews</i> if you want to see exactly what subscribers and you will be sent tonight. Nothing can be sent from there.</li>
</ol></div></details>
<p><b>Material evidence held:</b> a failed or incomplete reparse keeps the last verified source
and creates a review item. Unavailable or unreadable documents do not mean no materials.
Dataset publication stops when source owners, certificate periods or material rows disagree
between the register, raw extraction and served release. The Sinetti operator reviews the
actual source, repairs and reparses it, then rebuilds all three layers. The last accepted release
remains available; deleting a review item or changing a count band is not a repair.
A refused re-read where the kept file and the new read both have no material rows (for example
"no materials declared" read again as an unsupported layout) loses nothing: it stays listed for
review but no longer counts toward the 50-document hold ceiling and never holds publication.
The 50-document ceiling counts only documents newly held in the last three days, and never a
failed download: older holds keep their own certificate on its last verified reading but do not
stop the night's publication. A reading held for more than 14 days is named on a
<i>Held PDF readings</i> line in the daily admin email until it is reviewed.</p>
<p>Data Trust starts with <b>Accounted and read</b>, for in-force certificates only. <i>Accounted</i> is certificates that are read or closed as an accepted exception with a named reason; <i>Read</i> includes parser reads and manually verified PDFs, shown in separate columns. An exception counts as accounted, never as read, is bound to the exact PDF file, and reopens when a new file arrives. A cover that prints more pages than its official PDF holds (“Page 1 of 2” on a one-page file) is never counted as declaring no materials: it shows as <i>Official PDF misses a printed page</i> until reviewed, then as the exception <i>Page missing from the official PDF</i>. The target is accounted 100% after every morning OCR run. <i>Not accounted</i> lists each certificate with why: <i>Needs a decision</i> (the only rows that need you), <i>Not tried yet</i>, <i>Download retry</i> (three mornings, then closed) or <i>Reader fix open</i> with its backlog item. 2BSvs and RSB read “Not read — frozen scheme”. The same list downloads as CSV.</p>
<p>For a certificate that needs a decision, select <b>Review manually</b> to compare its source PDF with editable pathway and site rows. Existing material names and scopes are suggested as the admin types; free text remains available for the exact printed wording. A partial save keeps the certificate open. Marking it manually verified requires a reason and confirmation that every row and site annex was checked. The review is tied to the PDF SHA-256, and review history records each save, reviewer and time. These corrections stay in the operator review record; run the normal PDF promotion process before they appear in customer-serving pathways.</p>
<p>Trust Review findings follow. Open <b>Document overview, audit reports and reconciliation</b> for document coverage, operating lanes and retry work. Entity identity and related-company reviews each have their own disclosure below. Related-company review shows ten pairs per page, with <b>x–y of N pairs</b>, <b>Previous</b> and <b>Next</b>; all candidates remain available. After a decision removes the last row on the last page, the screen returns to the last valid page.</p>
<details class="proc" id="p-finding"><summary>Review a Trust Review finding<span class="k">Screen · 3 min each</span></summary><div class="body">
<ol class="steps">
<li>Admin → Data Trust → <i>Trust Review findings</i> appears right under the tab row, including when the snapshot is unavailable. The snapshot date, artifact and provenance sit at the right end of that tab row; <i>Unverified snapshot</i> shows in warning colour. Data ops findings links land here and preserve their scheme and lifecycle filters. Filter by <i>Scheme</i>, <i>Priority</i>, <i>Check</i>, <i>Lifecycle state</i> (start with <i>Unresolved</i>).</li>
<li><kbd>Review finding</kbd>. Read the claim, the evidence and its source dates, how many records are affected, first and last seen, recurrence, and earlier decisions. <i>Open investigation route</i> jumps to the affected records.</li>
<li>Under <i>Review decision</i> choose a <i>State</i>, write a <i>Reason</i> (3+ characters; write for the person reading it next year), <kbd>Save review decision</kbd>.</li>
<li>If the screen says the finding changed while you were reading, do not force it: read the fresh evidence and decide again.</li>
<li>To change how a whole <i>type</i> of finding treats automation, use <i>Activation classification</i> → <i>Blocking</i> or <i>Advisory</i> → reason → <kbd>Save classification</kbd>. This applies to every finding of that type.</li>
</ol></div></details>
<details class="proc" id="p-pdf-live-coverage"><summary>Inspect a PDF coverage cycle or liveness failure<span class="k">Desk · evidence review</span></summary><div class="body">
<p>The PC combines authenticated hosted ISCC/REDcert source checks with verified local INS receipt/current/latest evidence. Missing local INS proof stays unknown; hosted INS rows cannot replace it. The pinned operational proof binds both actual envelope hashes and separate lane capacity receipts. Preserve the original immutable INS producer request and its file pins; see the Data Trust runbook for the exact composition digest.</p>
<p>An evidence-bound requirement with no PDF hash remains in the denominator and never becomes Read or literal 100%. Only complete, current blocker proof or in-deadline acquisition evidence permits operational evaluation. Parser support alone is insufficient: the existing strong Read contract accepts certificate/material/site annex roles; other required roles, including parsed ISCC audit reports, remain unknown.</p>
<ol class="steps">
<li>Verify the actual <b>Sinetti PC daily trust monitor</b> task at <b>05:30 Zurich time</b>, preserving that local slot through daylight-saving changes. Confirm the deployed hosted schedule separately. The PC consumes <code>--pdf-coverage-invocation-id</code> only with independently pinned <code>scheduler_execution</code> proof from the request's named <code>operational_evidence</code> file matching the current launcher PID, its actual OS birth in <code>launcher_started_at</code>, and original scheduled provenance. Strings, manual runs, <code>--now</code>, copied/stale proof or registration alone cannot qualify. No real wrapper/proof producer is installed, enabled or verified by this merge.</li>
<li>Read the immutable local receipt under <code><state>/refresh/pdf_coverage/receipts/</code> and coverage-only <code>latest.json</code>: its byte hash and previous-receipt link, source/inventory and stable production bindings, pending ages, unresolved cohort, notifications and pass/fail/unknown targets. Actual Ready must name the same active revision/hash before and after reconciliation; movement is retried once, then remains unknown. Immutable production app/manifest/details and current independent code/contract/source/API/customer proof are required; stale mirrors or candidate attachment do not supply it.</li>
<li>Inspect the PC's narrow export <code>refresh/pdf_coverage/monitor-heartbeat.json</code>, independent hosted <code>hosted-checks/</code> and <code>hosted-check-latest.json</code>, and separate local <code>liveness-outcomes/</code> and <code>liveness-latest.json</code> for actual authenticated SDK export/readback. Only <code>refresh/pdf_coverage/hosted-check.json</code> is exported for the PC to read. Full local receipts and audit state are <b>not uploaded</b>. Hosted validation checks the authenticated heartbeat and receipt-hash link without full PC receipt verification. Strong completed bootstrap reconciliation can export an unknown-coverage heartbeat without qualifying acceptance.</li>
<li>The declared hosted slot is <b>02:00 Zurich time in summer, 01:00 Zurich time in winter</b>, preceding that day's <b>05:30 Zurich time</b> PC cycle; verify the actual schedule before relying on this order. The next PC execution finalizes the prior outcome; current pending evidence is never counted. A newer bound negative check invalidates a prior positive by appending immutable evidence. Actual stored incident results, including required recovery/due reminder or genuine no-notification-needed disposition, must bind the canonical receipt before qualification. Required delivery failure remains unknown; retained failed/reserved/unknown delivery and <code>refresh/trust_monitor_reminder.json</code> cannot silently become no-duty success. The PC report uses <code>pdf_coverage_monitor</code>; enabled unknown records <code>monitor-error</code> and exits 2.</li>
<li>A persistence, hash, authentication, notification, export/readback or hosted-check failure keeps acceptance unknown. A heartbeat written before failed readback may remain remotely; preserve the immutable receipt and do not assume rollback. Enabled hosted preflight failure retains acquired/refreshed candidate data and recovery evidence, but reports partial and returns nonzero; the existing production auto-activation guard holds activation whenever overall status is not <code>ok</code>. Missing global proof or deferred INS cannot waive that opt-in hold. Default-off behavior is unchanged. Repair the named incident through the existing reminder route. Do not hand-edit proof or backfill missed dates; the next qualifying scheduled cycle starts day one.</li>
<li>Use the <a href="../data-trust-runbook.md#verified-live-pdf-coverage-and-recurring-source-checks-sin-0663">Data Trust runbook</a> for the complete evidence and acceptance procedure; <a href="../daily-refresh-runbook.md#full-inventory-source-audit-and-hosted-monitor-check-sin-0663">daily refresh</a>, <a href="../ins-local-refresh-runbook.md#ins-source-freshness-and-the-full-pdf-target-sin-0663">local INS</a> and <a href="../rehearsal-runbook.md#pdf-coverage-fixtures-and-rehearsals-sin-0663">rehearsal</a> describe their separate lanes.</li>
</ol></div></details>
<details class="proc" id="p-qa"><summary>Work the Admin QA inbox<span class="k">Screen</span></summary><div class="body">
<ol class="steps">
<li>Data Trust → <i>Admin QA</i>. Filter by <i>Scheme</i>, <i>Issue family</i>, <i>Severity</i>. Numbers on the Overview link straight into the right filter.</li>
<li>Select an issue. Read the plain-language description, the suggested action and, where offered, <i>Open PDF evidence</i>.</li>
<li>Choose: <kbd>Accept suggestion</kbd>, <kbd>Defer</kbd>, <kbd>Mark false positive</kbd> or <kbd>Send to parser/rule backlog</kbd>. Use <i>Edit suggestion</i> to correct the target first. Decisions are only <b>staged</b>.</li>
<li><i>Pending resolutions</i> → <kbd>Approve all pending for publish</kbd>.</li>
<li><i>Publish preview</i> shows the expected effect. <kbd>Write rebuild input file…</kbd> → <kbd>Confirm — write rebuild input</kbd> hands the decisions to the next PDF re-parse. Nothing changes for customers until that re-parse is promoted.</li>
</ol></div></details>
</div>
</section>
<section class="area topic--data" id="curation">
<p><b>Company research request backend (SIN-0561).</b> Admin requests queue for PC pickup even while the PC is offline. Ingested means saved on the PC, not researched or landed. Full-pass estimates require a fresh complete catalog and exact configured subscription/model. If an acknowledgement conflicts with missing journal history, or receipt verification is interrupted, PC pickup and execution stop for operator reconciliation and remain stopped on later ticks and direct commands. The fifteen-minute task definition registers disabled and has not been run. The approved holder Research line shows per-kind dates and provenance, due/review/queue state, standing priority and Research now to admins only. Research → Coverage adds actual bounded queue preview, held reasons, recent per-company outcomes and producer changes. Queue next due preserves current due/hold checks; Full pass first requires a fresh exact estimate and unchecked approval, with targeted, selected-tier or every-in-force scope. These actions only enqueue PC work; the existing globe remains separate. The existing daily admin brief can show recent producer changes with review labels, including plant and capacity evidence additions. The PC whole-dossier path retains ownership/relationships and plant research separately; partial sections do not complete the dossier or full-pass target. Fresh estimates count both stages; legacy plant dates are never invented. Organisation-owned contact research remains separate. See <a href="../company-research-admin-runbook.md">Company research Admin runbook</a>.</p>
<h2><span class="topicIcon"><svg class="guideIcon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d='M20 12 12 20 3 11V3h8z'/><circle cx='7.5' cy='7.5' r='1.3'/></svg></span><span class="n">10</span>Curation</h2>
<p class="where"><b>Where:</b> Admin → Materials · Data Trust (identity queues) · Customers → Market templates · Research (tabs <i>Coverage</i> · <i>Site footprint</i>) · Environment</p>
<div class="short"><b class="tag">In short</b>
<p>Registers name the same thing a hundred ways. Curation is where your judgement becomes part of the product: which long material names share a short label and a family, which two company records are one legal entity, which companies are related, which competitors belong to a market template. These decisions are hundreds of small owner calls that no competitor can download — treat the screens as an asset, not housekeeping.</p>
</div>
<div class="d2">
<div class="tw"><table>
<tr><th style="width:200px">Screen</th><th>What you decide</th><th>When it takes effect</th></tr>
<tr><td><b>Materials</b></td><td>A prioritised <i>To do</i> list (big names still in Other, spelling twins including curated spelling groups, label-vs-family disagreements, long names without a short label); which family each material belongs to and how names roll up; custom families; short display labels; a read-only view of the curated spelling groups, and an <i>All names</i> table you can filter by kind and group by spelling. Opening the page applies the existing automatic suggestions, including names with a single certificate, and writes each change to the auto-apply log. New curated-group copies remain in <i>Spelling twins to align</i> for an explicit <kbd>Accept</kbd>; assigned conflicting families appear separately for review</td><td>Saved on open (automatic suggestions) or on save (curated-group copies and your own edits, after an impact preview when families move). Users see the change after the next data publish, normally the nightly data refresh — not at once</td></tr>
<tr><td><b>Entity identity review</b><br><span class="where">Data Trust · one table</span></td><td><kbd>MERGE</kbd> or <kbd>DO NOT MERGE</kbd> two records across registers that look like one legal entity but were unsafe to merge automatically. The table shows legal names, whether each side is in force, and the flags. Rows badged <b>Applied provisionally · company-group job</b> are same-company merges the job researched on the web and already recorded because the two names are plainly one (one name on two registers); the cards are merged and the row shows the source links and note: <kbd>CONFIRM</kbd> keeps it, <kbd>REVERT</kbd> removes the merge. Rows badged <b>Awaiting your decision · company-group job</b> are merges the job judged ambiguous and did not record: <kbd>ACCEPT</kbd> records the merge in your name, <kbd>REJECT</kbd> writes nothing. Either way the pair is never researched again</td><td>With the next successful data rebuild — not at once</td></tr>
<tr><td><b>Related companies review</b><br><span class="where">Data Trust · one table</span></td><td><kbd>CONFIRM RELATED</kbd> (with a group label), <kbd>SAME ENTITY</kbd> or <kbd>REJECT</kbd>. Each row shows the legal names, in-force state, flags, and the shared fact for the pair. Undecided pairs from the local parent-join report also appear with “same ultimate parent”; previously decided pairs stay out. <kbd>SAME ENTITY</kbd> also joins the two holders’ register records, so their Browse cards merge. The full cluster remains in <code>data/refresh/company_parent_join_review.json</code>. Rows badged <b>Applied provisionally · company-group job</b> are group memberships (two or more source domains) or ownership lines (an official or own-site source) the job researched on the web and already wrote, so customers see them: <kbd>CONFIRM</kbd> keeps the row, <kbd>REVERT</kbd> removes exactly what was written. Rows badged <b>Awaiting your decision · company-group job</b> rest on thinner evidence and are not shown until you decide (an ownership answer of “unknown” never reaches this queue: it claims nothing, is not shown to customers and is researched again after 90 days): <kbd>ACCEPT</kbd> writes exactly the proposed row in your name, <kbd>REJECT</kbd> writes nothing. The right-hand cell names the group or the ownership line; every row shows the source links and the researcher’s note. A rejected or reverted pair is never researched again. The daily admin mail counts both tiers under “Workload to clear”</td><td>Next rebuild</td></tr>
<tr><td><b>Company groups (automatic)</b><br><span class="where">quarterly task “Sinetti PC quarterly company groups” (1 Jan / Apr / Jul / Oct, 06:45)</span></td><td>Nothing to click. Each quarter the job (run by <code>scripts/company_group_drain.ps1</code> until every queue is empty, one commit per slice) places holders not yet decided (by shared first name word, and, since 28 September 2026, holders whose name carries an existing group's name further in, such as TOTSA TotalEnergies Trading) (more than 50% the same ultimate parent), joins holders whose recorded ultimate parent already matches, and records whether that parent is listed, privately held, state-owned or a cooperative. Clusters it will not join automatically are written to <code>data/refresh/company_parent_join_review.json</code>. Research uses headless Claude Code on the operator's subscription, model <code>claude-sonnet-5</code>, and strips <code>ANTHROPIC_API_KEY</code> so it never bills the API. Sign in once with <code>claude</code> then <code>/login</code> in a normal terminal; a dead sign-in shows as <code>researcher failed: Failed to authenticate</code>. Your <kbd>REJECT</kbd> on a pair wins over the group. Receipt: <code>data/refresh/company_group_job.json</code>. A failed merge is retried; the job does not throw away an unmerged slice. Runbook: <code>docs/company-groups-runbook.md</code></td><td>Local rebuild the job runs, then the next code deploy (the override files are code-owned)</td></tr>
<tr><td><b>Market templates</b><br><span class="where">Customers</span></td><td>Which organisations are offered a ready-made competitor cohort; holders pinned into or excluded from it</td><td>At once</td></tr>
<tr><td><b>Site footprint</b></td><td>Agree or correct the automatic classification of a site's surroundings (industrial / office-residential / unclear) — the China pilot. The separate reviewed satellite site-assessment workflow is under development and remains disabled; these existing classifications are not image-backed findings. Scans run by hand (<code>scripts\scan_site_footprints.py</code>) and each server keeps its own scan store; a server with none says “No site-footprint scans exist on this server yet”.</td><td>At once; feeds classifier accuracy</td></tr>
<tr><td><b>Research coverage</b><br><span class="where">Research → Coverage</span></td><td>The <b>research data switch</b> at the top: <kbd>Shown</kbd> or <kbd>Hidden</kbd>. Hidden means customers see certificate data only — no research-confirmed related companies, ownership line, Production units box, research nameplate capacities, researched plants on the Producers map and list, Company research or Ask's research offer. Your own related-company pair decisions and same-company joins stay. Nothing is deleted; the band turns terracotta and names who switched it off. While it is hidden, every admin page shows a terracotta <b>Research hidden</b> pill beside Ask that links back here. To hide research for one customer only, untick <b>Research data</b> on that customer's plan instead. Below it, <b>Company research</b> shows due tiers, held companies, PC status/subscription, recent per-company runs, independent section outcomes and producer changes including plant additions. Partial means the dossier is unfinished. Choose a tier and a bounded next 12–200 due queue; <kbd>Preview queue</kbd> shows the actual PC order. <kbd>Full pass…</kbd> estimates the selected tier, all targeted companies or every in-force holder, then requires explicit approval. Stale/unavailable estimates disable queueing. Queued and ingested are distinct from researched/landed. The facility table remains below, with evidence and capacity sorting. Runbook: <code>docs/company-groups-runbook.md</code> (research data switch)</td><td>Switch: every customer's next page load, no restart. Stored in the persistent state folder, so it survives a redeploy</td></tr>
<tr><td><b>Environment</b></td><td>Assistant model routing (leave on automatic unless diagnosing); the “expiring soon” window in days; a read-only list of which API keys are configured</td><td>On save</td></tr>
</table></div>
<div class="rule"><b>Material families are assigned, not guessed at display time.</b> Since 22 September 2026, opening Admin → Materials applies the existing automatic suggestions and logs them. Curated-group copies added on 9 October 2026 require the Sinetti admin to review the sibling, <kbd>Accept</kbd> for that name, then save. The copy fills missing values; it does not replace an assigned short label or family. Names marked <kbd>Keep in Other</kbd> keep that family. Different assigned families in these new curated checks require a separate explicit review decision; the prior punctuation-based automatic behavior is unchanged. After saving, confirm the short label and family under <i>All names</i>; Admin → Materials shows your saved values at once, while Browse, deep-dives, Ask and the next customer package show them after the next data publish (SIN-1043: the data refresh prepares the map from your saved values and every release carries it; the app no longer changes it). Names that first appear in a refresh arrive in Other and in the review list with that release. If saving is refused, read the message and correct or discard the pending edits; unrelated materials and distinct grades still cannot share a label. Two rules from the domain: a name in brackets after a material is its <i>origin</i> or its <i>acronym</i>, never a spelling variant to merge away; and animal by-product categories are ordered grades (category 1 worst, 3 cleanest) that must never be merged.</div>
<p><b>Ownership second opinions.</b> The Sinetti operator can recheck a bounded slice of pending, known-form ownership proposals once through the stored ChatGPT subscription login. The independent researcher sees the original form and parent, but no original sources, website or notes. The existing proposal note shows <b>First</b> and <b>Second</b> form/parent summaries and the outcome. Matching form and normalized parent need a new source domain and the existing source-trust checks before the row becomes provisional; Confirm/Revert stays the same. Disagreement, unknown, invalid or reused-source answers remain pending. Failed and interrupted attempts are also spent once. The original answer and complete second evidence remain in <code>data/company_group_proposals.json</code>; no new review control is added. The manual procedure is in <code>docs/company-groups-runbook.md</code>.</p>
<h3 id="material-names">How a material name travels</h3>
<div class="mn">
<p class="mnIntro">Every register spells materials its own way, in its own language. Before a name reaches a screen, Sinetti takes it through the same steps, in the same order, as the strip at the top of Admin → Materials, then reads one more thing from it. Each step has <b>one</b> answer source. When a chip or a filter looks wrong, find which step gave the wrong answer — that tells you where to fix it.</p>
<div class="legend">
<span><i class="sw" style="background:var(--topic-soft);border:1px solid var(--topic-line)"></i>You decide it on Admin → Materials</span>
<span><i class="sw" style="background:var(--code-soft);border:1px solid var(--code-line)"></i>Set in code — ask a developer</span>
</div>
<div class="flow">
<div class="col">
<h3>1 · Register names — where they come from</h3>
<div class="box src"><b>ISCC</b><small>register CSV + certificate PDF annex rows + audit reports</small></div>
<div class="box src"><b>REDcert</b><small>register + certificate PDF annex ticks</small></div>
<div class="box src"><b>INS</b><small>Italian free text, e.g. <code>Olio da cucina usato (UCO)</code></small></div>
<div class="box src"><b>2BSvs · RSB</b><small>register free text (not read from PDFs yet)</small></div>
<div class="box src"><b>LCFS · CFR · Oregon · BC · RenovaBio</b><small>pathway feedstock and fuel names</small></div>
</div>
<div class="col">
<h3>Steps 2–5, in order — one example traced</h3>
<div class="step code box"><span class="who">code</span>
<div class="q">2 · Spelling groups — are these spellings one material?</div>
<div class="ans">Registers write the same material several ways. Sinetti puts them in one <b>spelling group</b> and counts them as one material, keeping each original wording underneath.</div>
<div class="merge">
<div class="raws">
<code>Used cooking oil (UCO)</code>
<code>used cooking oil(UCO)</code>
<code>Olio da cucina usato (UCO)</code>
</div>
<div class="into">→</div>
<div class="one"><code>Used cooking oil (UCO)</code><small>spelling group · 3 spellings</small></div>
</div>
<ul class="how">
<li><b>Automatic, for every name:</b> capitals, punctuation and spacing are ignored (rows 1–2).</li>
<li><b>Curated list, 25 groups:</b> different <i>words</i> for the same material — Italian, acronym vs full name (row 3).</li>
<li><b>Never:</b> a different origin or ABP category in brackets. <code>Soapstock (Soybean)</code> stays apart from <code>Soapstock (Sunflower)</code>.</li>
</ul>
</div>
<div class="arrow">▼</div>
<div class="step box"><span class="who">Admin → Materials</span>
<div class="q">3 · Families — which shelf does it sit on?</div>
<div class="ans">Colour, icon, filter group. Unplaced names wait in <i>Other</i>. A fuel goes to its fuel family, never to the feedstock in its bracket.</div>
<div class="ex">Used cooking oil (UCO) → family UCO<br>HVO (Used cooking oil (UCO)) → family HVO</div>
</div>
<div class="arrow">▼</div>
<div class="step box"><span class="who">Admin → Materials</span>
<div class="q">4 · Short labels — what does the chip say?</div>
<div class="ans">Display only. Empty = the full register name is shown; the full name always stays on hover. Names that share a label show as one chip on a holder card.</div>
<div class="ex">Used cooking oil (UCO) → UCO</div>
</div>
<div class="arrow">▼</div>
<div class="step code box"><span class="who">code</span>
<div class="q">5 · Kind — product or feedstock?</div>
<div class="ans">Read from the words before the first bracket only (HVO, Biodiesel, Ethanol, SAF … = product).</div>
<div class="ex">UCO → feedstock · HVO (…) → product</div>
</div>
</div>
<div class="col">
<h3>Where users see the result</h3>
<div class="box surf"><b>Browse</b> cards, list, Filters<div class="tags"><span class="tag t1">3 family</span><span class="tag t2">4 label</span></div></div>
<div class="box surf"><b>Feedstocks / Fuels deep-dives</b> supply chain table, maps<div class="tags"><span class="tag t1">3 family</span><span class="tag t2">4 label</span></div></div>
<div class="box surf"><b>Holder page</b> materials, pathways, dossier export<div class="tags"><span class="tag t3">2 spelling</span><span class="tag t1">3 family</span><span class="tag t2">4 label</span></div></div>
<div class="box surf"><b>Geography deep-dive</b><div class="tags"><span class="tag t3">2 spelling</span><span class="tag t4">5 kind</span></div></div>
<div class="box surf"><b>Producers · Market monitoring · watchlist mail</b><div class="tags"><span class="tag t1">3 family</span><span class="tag t2">4 label</span></div></div>
<div class="box surf"><b>Daily digest · pathway change alerts</b><div class="tags"><span class="tag t3">2 spelling</span></div></div>
<div class="box surf"><b>Ask Sinetti</b><small>own word list (“animal fats” asks cat 1/2 or cat 3), then reads families</small><div class="tags"><span class="tag t1">3 family</span></div></div>
</div>
</div>
<h3 class="ladderTitle">How much each step groups</h3>
<div class="ladder">
<div class="rung"><b>16,532</b><span>1 · register wordings</span></div>
<div class="step2">→</div>
<div class="rung"><b>13,015</b><span>2 · spelling groups</span></div>
<div class="step2">→</div>
<div class="rung"><b>31</b><span>3 · built-in families<br>(+ your custom ones)</span></div>
<div class="step2">→</div>
<div class="rung"><b>2</b><span>5 · kinds<br>product · feedstock</span></div>
</div>
<div class="side">
<div class="rung rung--side"><b>1,121</b><span>4 · wordings with a short label</span></div>
<p><b>Short labels group too, but only on screen and only where a label exists.</b> Names that share a label show as one chip on a holder card and one line in its materials list: 630 different co-processed oil names all read <i>Copro oil</i>. Families, filters and deep-dive counts ignore labels, so a label never moves a holder in or out of a result. That is why it sits beside the ladder, not on it.</p>
</div>
<p class="ladderNote">Counts: material names on certificate pathway rows in the active release; labels from the local material map, 2026-10-08.</p>
<h3 class="ladderTitle">The same levels, one real example — each box sits inside the next</h3>
<div class="nestLegend">
<span><i class="sw k"></i>5 · Kind</span>
<span><i class="sw f"></i>3 · Family</span>
<span><i class="sw g"></i>2 · Spelling group</span>
<span><i class="sw w"></i>Register wording (× pathway rows)</span>
<span><i class="chipEx">chip</i>4 · Short label — what the chip says</span>
</div>
<div class="nest">
<div class="kind"><div class="kh">Feedstock</div>
<div class="fam"><div class="fh">Family <b>UCO</b> <small>284 wordings · 179 spelling groups</small></div>
<div class="grp"><div class="gh">Used cooking oil (UCO) entirely of veg. origin <span class="chip">UCO (veg.)</span><small>24 spellings · 3,324 rows</small></div>
<div class="w"><code>Used cooking oil (UCO) entirely of veg. origin</code><em>2,396</em></div>
<div class="w"><code>used cooking oil (UCO) entirely of veg. origin</code><em>428</em></div>
<div class="w"><code>Used Cooking Oil (UCO) entirely of veg. origin</code><em>333</em></div>
<div class="w"><code>Used cooking oil(UCO) entirely of veg. origin</code><em>31</em></div>
<div class="more">+ 20 more spellings</div>
</div>
<div class="grp"><div class="gh">Used cooking oil (UCO) <span class="chip">UCO</span><small>35 spellings · 2,248 rows</small></div>
<div class="w"><code>Used cooking oil (UCO)</code><em>1,327</em></div>
<div class="w"><code>UCO</code><em>294</em></div>
<div class="w"><code>UCO - Used cooking oil</code><em>55</em></div>
<div class="more">+ 32 more spellings</div>
</div>
<div class="grp"><div class="gh">Refined oil (Used cooking oil (UCO) entirely of veg. origin) <span class="chip">Refined oil (UCO (veg.))</span><small>11 spellings · 157 rows</small></div>
<div class="w"><code>Refined oil (Used cooking oil (UCO) entirely of veg. origin)</code><em>84</em></div>
<div class="more">+ 10 more spellings</div>
</div>
<div class="more">+ 176 more spelling groups</div>
</div>
<div class="more">+ other feedstock families (POME, Animal fat cat 3, …)</div>
</div>
<div class="kind prod"><div class="kh">Product</div>
<div class="fam"><div class="fh">Family <b>HVO</b> <small>443 wordings · 359 spelling groups</small></div>
<div class="grp"><div class="gh">HVO <span class="chip none">HVO</span><small>1 spelling · 276 rows</small></div>
<div class="w"><code>HVO</code><em>276</em></div>
</div>
<div class="grp"><div class="gh">HVO (Used cooking oil (UCO)) <span class="chip none">full name</span><small>3 spellings · 34 rows</small></div>
<div class="w"><code>HVO (Used cooking oil (UCO))</code><em>26</em></div>
<div class="w"><code>HVO (used cooking oil (UCO))</code><em>7</em></div>
<div class="w"><code>HVO (USED COOKING OIL (UCO)</code><em>1</em></div>
</div>
<div class="grp"><div class="gh">HVO (Food waste) <span class="chip none">full name</span><small>3 spellings · 41 rows</small></div>
<div class="w"><code>HVO (Food waste)</code><em>25</em></div>
<div class="w"><code>HVO (food waste)</code><em>15</em></div>
<div class="more">+ 1 more spelling</div>
</div>
<div class="more">+ 356 more spelling groups</div>
</div>
<div class="more">+ other fuel families (Biodiesel, Ethanol, SAF, …)</div>
</div>
</div>
<p class="ladderNote"><b>Read it inside-out.</b> A register wording lands in a spelling group (step 2); the group sits in a family (step 3); the family is a feedstock or a product (step 5). <b>HVO (Used cooking oil (UCO))</b> contains the words “used cooking oil” but sits in the HVO box on the product side — UCO is what the HVO was made from, not what it is. A UCO filter therefore does not return it. Wordings and row counts: certificate pathway rows in the active release; families from the local material map, 2026-10-08.</p>
<table>
<tr><th>Question</th><th>Answer kept in</th><th>Who changes it</th><th>Symptom when wrong</th></tr>
<tr><td>2 · Spelling group</td><td><code>data/models/material_aliases.json</code></td><td>Developer, by commit</td><td>Same material shown as two rows in Geography or a holder's pathways</td></tr>
<tr><td>3 · Family</td><td>Material map + family list (Admin → Materials)</td><td>Admin</td><td>Holder missing from a family filter, wrong chip colour, name stuck in Other</td></tr>
<tr><td>4 · Short label</td><td>Material map (Admin → Materials)</td><td>Admin</td><td>Chip shows a long or odd name, or two materials share one chip</td></tr>
<tr><td>5 · Kind (product or feedstock)</td><td>Code word list</td><td>Developer</td><td>Fuel counted as feedstock in Geography</td></tr>
</table>
<div class="rule"><b>Brackets are information, never noise.</b> <i>Soapstock (Soybean)</i> and <i>Soapstock (Sunflower)</i> are two materials. <i>Animal fats (category 1)</i> and <i>(category 3)</i> are different grades. No question above ever merges them.</div>
</div>
<p class="where">Counts and examples: certificate pathway rows and the local material map, 8 October 2026. Spelling groups and kind are shown read-only in Admin → Materials → All names (tick <i>Group spellings</i>; the Kind filter is next to Family).</p>
<div class="rule"><b>A fuel is filed under its fuel family, never under the feedstock in its bracket.</b> “HVO (UCO)” is HVO; “Biodiesel (UCO)” is Biodiesel. A UCO filter therefore returns UCO handlers, not the HVO and biodiesel producers that buy from them. Since 2026-09-24 the app enforces this at start-up: the first start after the deploy files every fuel-led name (biodiesel/FAME/UCOME, HVO/HEFA/SAF, ethanol, biomethane/bio-LNG, bio-LPG/naphtha/propane/butane, bio heating oil, biomethanol) under its fuel family and creates the three owner families <i>Bio-LPG / naphtha</i>, <i>Bio heating oil</i> and <i>Biomethanol</i> if missing; every later start only lifts fuel names still sitting in Other, so a family you set by hand is never overridden. Each move appears in the Materials change log. Rename or recolour the three families freely — the app creates them once and never rewrites them.</div>
<div class="rule"><b>Straw, husks, wood residues and similar lignocellulosic byproducts have their own family.</b> Since 2026-09-25 the app files the owner-approved list (Straw, Husks, Chaff, Grass, Wood chips, Wood chips from harvesting, Forestry processing/forest-biomass residues, Damaged trees, Fruit tree cuttings, Nut shells, Bean shells, Grape marc(s)/wine lees, Agricultural harvesting residues, Shells/husks: soy hulls, Non-food cellulosic materials from food/forage crops, Unused feed/fodder from ley) into <i>Agri & forestry residues</i> at start-up if they are still sitting in Other, and creates that family if missing. Every later start only lifts names still in Other, so a family you set by hand is never overridden. Names the owner marked uncertain at the time (an ambiguous "Sugar beet residues", "Darnel", "Intermediate crop") were deliberately left out — place those yourself if you agree with a reading.</div>
<div class="warn"><b>After a deploy that introduces a new built-in family, nothing is assigned to it</b> — production keeps its existing material map, and nothing errors. Run the seed-assignment script against production's state (dry run first): <code>python scripts/apply_seed_family_assignments.py</code>, then <code>--apply</code>. A dry run reporting zero changes is the proof it is done.</div>
<h3 id="company-names">How a company name travels</h3>
<p class="where"><b>Where it is decided:</b> Admin → Data Trust (<i>Entity identity review</i>, <i>Related companies review</i>) · the quarterly company-groups job · the rest is set in code and read-only. Real Neste example; counts as of 8 October 2026.</p>
<div class="mn">
<p class="mnIntro">The same company is printed in a dozen ways across the registers, and companies belong to families. Sinetti works through five questions in a fixed order. The first three decide whether two names are <b>one company</b>. The last two place <b>different companies</b> in a family. Each question has <b>one</b> answer source — when a card, a related list or a header looks wrong, find which question gave the wrong answer.</p>
<div class="legend">
<span><i class="sw" style="background:var(--topic-soft);border:1px solid var(--topic-line)"></i>You decide it, or can reverse it, in Admin</span>
<span><i class="sw" style="background:var(--mnc-soft);border:1px solid var(--mnc-line)"></i>Set in code — ask a developer</span>
</div>
<div class="flow">
<div class="col">
<h3>1 · Register names — where they come from</h3>
<div class="box src"><b>ISCC</b><small>name and address in one string: <code>Neste Oyj, Keilaranta 21, 02150 Espoo, Finland</code></small></div>
<div class="box src"><b>REDcert · INS · 2BSvs · RSB</b><small>name only, e.g. <code>NESTE Oyj</code></small></div>
<div class="box src"><b>LCFS · EPA · CFR · Oregon · BC · RenovaBio</b><small>name, usually with a structured address</small></div>
<div class="box src"><b>Research files</b><small>group, ownership and links we recorded from public sources</small></div>
</div>
<div class="col">
<h3>Steps 2–6, in order — Neste traced</h3>
<div class="step code box"><span class="who">code</span>
<div class="q">2 · Holder key — is it spelled the same?</div>
<div class="ans">The name before the first comma, lower case, letters and digits only. Registers that spell a name identically share a key, so the join across registers is already done here.</div>
<div class="chain">
<div class="node"><code>Neste Oyj, Keilaranta 21, …</code><code>NESTE Oyj</code><code>Neste Oyj</code></div>
<div class="link">→</div>
<div class="node"><b>nesteoyj</b><small>ISCC · INS · EPA · LCFS</small></div>
</div>
<ul class="how"><li><b>Never edited.</b> Watchlists, alerts and every override are keyed on it.</li></ul>
</div>
<div class="arrow">▼</div>
<div class="step box"><span class="who">Data Trust</span>
<div class="q">3 · Legal entity — are two different keys one legal person?</div>
<div class="ans">Same legal person, spelled differently by the registers. Joined into one entity that keeps every original name.</div>
<div class="chain">
<div class="node"><code>nesterenewablefuels</code><small>BC · “Neste Renewable Fuels”</small><code>nesterenewablefuelsoy</code><small>LCFS · “…Oy”</small></div>
<div class="link">→</div>
<div class="node"><b>Neste Renewable Fuels Oy</b><small>automatic · BC left the legal form out</small></div>
</div>
<div class="chain">
<div class="node"><code>nesteoilsingapore</code><small>BC · “Neste Oil Singapore”</small><code>nestesingaporepteltd</code><small>EPA · INS · ISCC · LCFS · Oregon</small></div>
<div class="link">→</div>
<div class="node"><b>Neste Singapore Pte Ltd</b><small>owner decision 22 Sep 2026 · earlier name, same Tuas plant · 6 registers</small></div>
</div>
<ul class="how">
<li><b>Automatic:</b> same name apart from the legal form one register leaves out or spells differently, or the same full legal name with no conflicting address.</li>
<li><b>You:</b> <i>Entity identity review</i> — <kbd>MERGE</kbd> or <kbd>DO NOT MERGE</kbd> for what the rules would not join on their own (a renamed company, the same plant under an old name). Takes effect at the next data rebuild.</li>
<li><b>Never:</b> a holding company with its operating company, or a parent with its subsidiary. Those are step 4.</li>
</ul>
</div>
<div class="arrow">▼</div>
<div class="step box"><span class="who">Data Trust</span>
<div class="q">4 · Related companies — different legal entities, one family?</div>
<div class="ans">Never merged. Found by a shared address and shared name stem, a shared street, or a holding company naming its operating company. Shown to customers only once confirmed.</div>
<div class="ex">Bioro NV ↔ Cargill NV — same street in Ghent · confirmed “Cargill — Ghent site”</div>
<ul class="how"><li><b>You:</b> <i>Related companies review</i> — <kbd>CONFIRM RELATED</kbd> (with a group label), <kbd>SAME ENTITY</kbd> or <kbd>REJECT</kbd>.</li></ul>
</div>
<div class="arrow">▼</div>
<div class="step box"><span class="who">quarterly job · reversible via API</span>
<div class="q">5 · Corporate group — more than 50 % under one parent?</div>
<div class="ans">Researched from public sources, with the source recorded. Members become related pairs automatically.</div>
<div class="ex">Group “Neste” · parent Neste Oyj · 21 holder keys → 19 legal entities</div>
<ul class="how">
<li><b>Not members:</b> joint ventures and minority stakes. They stay labelled links (box below).</li>
<li><b>You:</b> nothing to click each quarter. A wrong placement is reversed through the admin API — there is no screen for it yet (procedure <i>Review or reverse an autonomous company correction</i>) — and takes effect at the next rebuild.</li>
</ul>
</div>
<div class="arrow">▼</div>
<div class="step box"><span class="who">same job</span>
<div class="q">6 · Ownership line — listed, private, state-owned?</div>
<div class="ans">One fact per ultimate parent, shown under the address. A subsidiary reads “Listed group · Subsidiary of Neste Oyj”.</div>
<div class="ex">Neste Oyj → Listed · Nasdaq Helsinki · NESTE</div>
</div>
<div class="links"><b>Labelled links, never grouped:</b> joint venture · minority holder · affiliate · offtake · feedstock supplier · tolling.<br>Neste: <b>Martinez Renewables LLC — JV, 50 %</b> with Marathon Petroleum · <b>State of Finland — minority holder</b> (no register row, so listed under “Outside the registers”).</div>
</div>
<div class="col">
<h3>Where users see the result</h3>
<div class="box surf"><b>Browse cards</b> in the World view or with 2+ registers picked: one card per legal entity. With one register picked: one card per holder key.<div class="tags"><span class="tag t3">2 key</span><span class="tag t1">3 entity</span><span class="tag t2">4 related</span><span class="tag t4">5 group</span></div><small>“· N related” counts confirmed related companies in force today</small></div>
<div class="box surf"><b>Browse map</b><div class="tags"><span class="tag t3">2 key</span><span class="tag t1">3 entity</span></div></div>
<div class="box surf"><b>Holder page</b> name and aliases · ownership line · Related companies · Production units<div class="tags"><span class="tag t1">3 entity</span><span class="tag t2">4 related</span><span class="tag t4">5 group</span><span class="tag t6">6 ownership</span></div><small>Production units pools every plant of the group, joint ventures at the recorded share</small></div>
<div class="box surf"><b>Watchlist · alert mail</b><div class="tags"><span class="tag t3">2 key</span><span class="tag t1">3 entity</span></div><small>A watch follows the whole entity once it is confirmed across registers</small></div>
<div class="box surf"><b>Dossier export</b><div class="tags"><span class="tag t1">3 entity</span><span class="tag t2">4 related</span><span class="tag t4">5 group</span></div></div>
<div class="box surf"><b>Ask Sinetti</b><small>matches holder names across registers by name tiers</small><div class="tags"><span class="tag t1">3 entity</span></div></div>
<div class="box surf"><b>Research data switch</b> <small>Admin → Research → Coverage. Hidden removes research-confirmed related companies, the ownership line and Production units from customer screens. Your own pair decisions (step 4) and same-company joins (step 3) stay.</small></div>
</div>
</div>
<h3 class="ladderTitle">How much each step groups</h3>
<div class="ladder">
<div class="rung"><b>40,510</b><span>2 · holder keys<br>(after the spelling join)</span></div>
<div class="step2">→</div>
<div class="rung"><b>40,264</b><span>3 · legal entities<br>236 fuse 494 keys</span></div>
<div class="step2">→</div>
<div class="rung"><b>966</b><span>5 · corporate groups<br>holding 3,448 holder keys</span></div>
<div class="step2">→</div>
<div class="rung"><b>348</b><span>6 · ultimate parents with an ownership line<br>137 listed · 172 private · 18 state-owned · 18 cooperative · 3 unclear</span></div>
</div>
<div class="side">
<div class="rung rung--side"><b>625</b><span>labelled links<br>(JV, minority, …)</span></div>
<p><b>Related (step 4) and links sit beside the ladder, not on it.</b> A related pair or a link never merges two companies and never changes a count; it only adds a line on the holder page and a number on the card. Counts: entities from the active release on this desk; groups, links and ownership from the committed decision files, 8 October 2026.</p>
</div>
<h3 class="ladderTitle">The same levels, one real example — each box sits inside the next</h3>
<div class="nestLegend">
<span><i class="sw k"></i>5 · Corporate group</span>
<span><i class="sw f"></i>3 · Legal entity</span>
<span><i class="sw g"></i>2 · Holder key</span>
<span><i class="sw w"></i>Register wording</span>
</div>
<div class="nest" style="grid-template-columns:1fr">
<div class="kind"><div class="kh">Group “Neste” · parent Neste Oyj · Listed · Nasdaq Helsinki NESTE <span style="font-weight:400;letter-spacing:0;text-transform:none;color:var(--dim)"> · 21 holder keys · 19 legal entities · 7 registers</span></div>
<div class="nest" style="grid-template-columns:1fr 1fr">
<div class="fam"><div class="fh">Legal entity <b>NESTE Oyj</b><small>1 holder key · 36 register rows · confirmed across 4 registers</small></div>
<div class="grp"><div class="gh">nesteoyj<small>holder key</small></div>
<div class="w"><code>Neste Oyj, Keilaranta 21, 02150 Espoo, Finland</code><em>ISCC</em></div>
<div class="w"><code>Neste Oyj, Jalostamontie 36, 06850 Kulloo, Finland</code><em>ISCC</em></div>
<div class="w"><code>NESTE Oyj</code><em>INS</em></div>
<div class="w"><code>Neste Oyj</code><em>EPA · LCFS</em></div>
</div>
</div>
<div class="fam"><div class="fh">Legal entity <b>Neste Singapore Pte Ltd</b><small>2 holder keys fused · 67 register rows · 6 registers</small></div>
<div class="grp"><div class="gh">nestesingaporepteltd<small>holder key · 27 rows</small></div>
<div class="w"><code>Neste Singapore Pte Ltd</code><em>EPA · LCFS · Oregon</em></div>
<div class="w"><code>Neste Singapore PTE. LTD.</code><em>INS</em></div>
<div class="w"><code>Neste Singapore Pte Ltd, 1 Tuas South Lane …</code><em>ISCC</em></div>
</div>
<div class="grp"><div class="gh">nesteoilsingapore<small>holder key · 40 rows</small></div>
<div class="w"><code>Neste Oil Singapore</code><em>BC</em></div>
</div>
</div>
<div class="fam"><div class="fh">Legal entity <b>Neste Renewable Fuels Oy</b><small>2 holder keys fused · 10 register rows</small></div>
<div class="grp"><div class="gh">nesterenewablefuelsoy<small>holder key</small></div>
<div class="w"><code>Neste Renewable Fuels Oy</code><em>LCFS</em></div>
</div>
<div class="grp"><div class="gh">nesterenewablefuels<small>holder key</small></div>
<div class="w"><code>Neste Renewable Fuels</code><em>BC</em></div>
</div>
</div>
<div class="fam"><div class="fh">+ 16 more legal entities<small>Neste Netherlands B.V. · Neste Germany GmbH · Neste US Inc. · Neste Terminal Rotterdam · NEWPORT BIODIESEL INC · UAB Neste Lietuva · …</small></div>
<div class="more">each has its own holder key and register wordings</div>
</div>
</div>
<div class="links" style="margin-top:8px"><b>Outside the group, labelled:</b> Martinez Renewables LLC — JV, 50 % · State of Finland — minority holder</div>
</div>
</div>
<p class="ladderNote"><b>Read it inside-out.</b> A register wording lands in a holder key (step 2); keys that are one legal person share an entity (step 3); entities under one parent form a group (step 5). <b>Neste Oil Singapore</b> and <b>Neste Singapore Pte Ltd</b> are two keys but one company — so a watch on either follows both. <b>Neste Netherlands B.V.</b> is a different company in the same group — a watch on it does not follow Neste Oyj.</p>
<div class="cbbox">
<h3>Certification bodies run a separate, shorter chain</h3>
<p style="margin:0 0 8px;font-size:13.5px;color:var(--dim)">A CB is identified by <b>legal entity</b>, never by brand. The brand only groups entities for search and display; no number is ever computed at brand level.</p>
<div class="chain three">
<div class="node"><code>SGS Germany GmbH, Emstek, Germany</code><code>SGS Germany GmbH, Emstek, DE</code><small>238 distinct strings on certificates</small></div>
<div class="link">→</div>
<div class="node"><b>SGS Germany GmbH</b><small>CB legal entity · home country DE · 153 in the register</small></div>
<div class="link">→</div>
<div class="node"><b>SGS</b><small>brand · 15 in the register</small></div>
</div>
<p style="margin:0;font-size:13px;color:var(--dim)">Neste Oyj's ISCC certificates read <b>SGS Germany GmbH</b> (DE); its INS certificate reads <b>SGS ITALIA SPA</b> (IT): two CBs, one brand. <code>DEKRA Certification GmbH</code>, <code>…B.V.</code> and <code>…Sp. z o.o.</code> are three CBs. A bare “SGS” stays unmapped — an owner decision of 26 July 2026. <b>Set in code</b> (<code>data/cb_reputation.json</code>). Users see it in the holder dossier and under Market intelligence → Certification bodies; reputation grades are admin-only.</p>
</div>
<table>
<tr><th>Question</th><th>Answer kept in</th><th>Who changes it</th><th>Symptom when wrong</th></tr>
<tr><td>2 · Holder key</td><td>Computed from the name (<code>certlogic.holder_norm</code>)</td><td>Developer — changing it re-keys every watchlist</td><td>One company on two cards when a single register is picked</td></tr>
<tr><td>3 · Legal entity</td><td>Rules in code + owner decisions in <code>data/entity_identity_overrides.json</code> (92 joins, 4 separate)</td><td>Admin → Data Trust → Entity identity review; next rebuild</td><td>One company on two cards in the World view — or two companies on one card</td></tr>
<tr><td>4 · Related</td><td>Rules in code + the <code>decisions</code> in <code>data/related_company_overrides.json</code></td><td>Admin → Data Trust → Related companies review</td><td>Missing, or wrong, “· N related”</td></tr>
<tr><td>5 · Corporate group</td><td><code>groups</code> in <code>data/related_company_overrides.json</code> (966)</td><td>Quarterly job; owner. Reverse through the admin API (no screen yet)</td><td>A subsidiary missing from the related list; plants missing from Production units</td></tr>
<tr><td>6 · Ownership line</td><td><code>data/company_ownership.json</code></td><td>Same job; owner</td><td>Header shows no ownership line</td></tr>
<tr><td>Labelled links</td><td><code>relations</code> in <code>data/related_company_overrides.json</code> (625)</td><td>Same job; owner</td><td>A joint venture shown as a group company, or not shown</td></tr>
<tr><td>Certification body</td><td><code>data/cb_reputation.json</code></td><td>Developer, by commit</td><td>Two spellings of one CB counted twice</td></tr>
</table>
<div class="rule"><b>Same company, related and group are three different things.</b> Merge two names only on evidence of one legal person. A holding company over several operating companies stays related — merging it with each would chain them into one. A group needs more than 50 % under one parent: joint ventures and minority stakes stay labelled links. Never merge a parent with its subsidiary to tidy a list.</div>
</div>
</div>
<div class="d3">
<h3>Procedures</h3>
<details class="proc" id="p-company-resolutions"><summary>Review or reverse an autonomous company correction<span class="k">Admin API · operator report</span></summary><div class="body">
<p>Corrections to existing memberships and duplicate groups keep the more-than-50% ultimate-parent rule. Every new autonomous decision stores its sources, supporting facts, reasoning, actor, model, policy, time and complete before/after groups. Uncertain cases are saved as deferred. The journal does not add evidence retrospectively to older job decisions.</p>
<ol class="steps">
<li>The Sinetti admin uses an authenticated API session: <code>GET /api/admin/data-trust/company-resolutions?limit=50&offset=0</code> lists newest events first (maximum 200 per page); <code>GET /api/admin/data-trust/company-resolutions/<event_id></code> opens full evidence. There is no new history screen yet. The local operator can export all decisions and reversals with <code>python scripts/company_resolution.py --history --report history.md</code>.</li>
<li>To reverse an applied decision, the Sinetti admin sends <code>POST /api/admin/data-trust/company-resolutions/<event_id>/revert</code> with <code>{"rationale":"Reason for the correction"}</code>. The actor is the signed-in admin. Success returns <code>ok: true</code>, a linked reversal event and <code>applies: next_rebuild</code>. The original evidence stays intact.</li>
<li>A 409 means an affected group changed since the original decision; a 422 can mean a current Admin rejection prevents restoration. Review the latest evidence and prepare a fresh proposal. Do not overwrite later work or edit the journal. Repeating a successful reversal returns the same receipt.</li>
<li>The Sinetti operator preserves the changed code-owned override and history report in the normal commit/merge workflow, refreshes registry context with <code>scripts/company_registry_review.py</code>, then follows the company-layer rebuild and restart procedure. A hosted API reversal must also be preserved in the code-owned source before a later deploy. Customer links change on the next successful rebuild; a receipt alone does not prove merge or deployment.</li>
</ol>
<p class="where">Verified against the journal service, Admin routes and CLI, 27 September 2026. Proposal format, isolated-checkout apply, exact paths and recovery: <code>docs/company-groups-runbook.md</code> → Reviewing and reversing autonomous corrections. These commands make no research-provider calls.</p>
</div></details>
<details class="proc" id="p-new-materials"><summary>Weekly: review new material names<span class="k">Screen · 10 min</span></summary><div class="body">
<ol class="steps">
<li>Admin → Materials opens on <i>To do</i> and applies suggestions before you click. Read the note that says what it applied. What remains is names with no suggestion: pick a <i>Family</i>, add a <i>Short label</i> where a chip reads badly, then <kbd>Save</kbd> (<kbd>Review impact</kbd> first if families moved). The auto-apply log is <code>GET /api/setup/material-map/auto-log</code>.</li>
<li>For names that genuinely belong nowhere, <kbd>Keep in Other</kbd> removes them from the list. Since 2026-09-25 this choice is saved on the server (Admin → Materials "kept in Other" list), not in the browser — it follows you across devices and survives a cleared browser profile, and it also keeps that name out of the daily admin mail's material-backlog row.</li>
<li><i>All names</i> → <i>New since last review</i> shows names that appeared since you last looked; <kbd>Mark all reviewed</kbd> once you have. A start-up guard (2026-09-25) re-baselines this list once if it ever holds under 10% of the corpus — e.g. after a data reset — so a stale or empty seen-list never flags the whole corpus as "new" in one go.</li>
</ol></div></details>
<details class="proc" id="p-entity"><summary>Decide an entity-identity pair<span class="k">Screen · 2 min each</span></summary><div class="body">
<ol class="steps">
<li>Admin → Data Trust → <i>Entity identity review</i>. One table: both legal names, in-force state, flags, and why the pair was flagged.</li>
<li>Merge only when you would defend “same legal entity” to the customer: same registration, not merely same group or same street. Sister companies belong in <i>Related companies</i>, not here.</li>
<li>Write the <i>Decision reason</i> (what evidence), press <kbd>MERGE</kbd> or <kbd>DO NOT MERGE</kbd>. The grouping changes at the next data rebuild.</li>
</ol></div></details>
<details class="proc" id="p-related-card"><summary>Explain the card's related list to a customer<span class="k">Reference</span></summary><div class="body">
<p>On every Browse card, <kbd>· N related</kbd> opens one list of the holder's related companies. N is the same number as on the holder page. The list reads top to bottom in four groups, each sorted by region (South, Southeast and East Asia read as one "Asia") and then country:</p>
<ol class="steps">
<li><b>In force</b>: related companies with a certificate, pathway or registration in force today.</li>
<li><b>Nothing in force</b>: related companies whose register rows have all lapsed. Shown dimmed, still counted.</li>
<li><b>Outside the registers</b>: parties named in public sources (a shareholder, a joint-venture partner, a feedstock supplier) that hold nothing in any register. Counted, but not clickable, because there is no holder page; <kbd>source ↗</kbd> opens the research source. Hidden when the research data switch or a customer's plan turns research off.</li>
<li><b>Unconfirmed · not counted</b>: Sinetti's own guesses from register clues, such as two holders at the same numbered street address. Never part of N. To confirm or reject one, use Data Trust → <i>Related companies review</i>.</li>
</ol>
<p>The Materials panel shows materials only; it no longer lists related companies (2026-10-07).</p>
</div></details>
</div>
</section>
<section class="area topic--data" id="knowledge">
<h2><span class="topicIcon"><svg class="guideIcon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d='M4 4.5A2 2 0 0 1 6 3h13v16H6a2 2 0 0 0-2 2z'/><path d='M4 19.5A2 2 0 0 1 6 18h13'/></svg></span><span class="n">11</span>Knowledge and law</h2>
<p class="where"><b>Where:</b> Admin → Ask → Knowledge · Admin → Data ops → Sources · Admin → Ask → Regulatory monitor</p>
<div class="short"><b class="tag">In short</b>
<p>Ask Sinetti answers from two things: the register data, and a library of documents — scheme rules, EU and national law, guidance — that it can quote with a link. That library is built on the operator machine and <b>ships inside the app</b>, so a new document reaches customers only with the next deploy. ISCC and REDcert documents are checked automatically every week and regulation every four weeks; new or changed files are picked up and indexed without anyone adding them by hand. Separately, a daily monitor watches the laws behind Sinetti's country logic and opens a <b>review case</b> when an official text changes; it never changes any calculation by itself.</p>
</div>
<div class="d2">
<div class="tw"><table>
<tr><th style="width:190px">Screen</th><th>What it shows</th><th>Actions</th></tr>
<tr><td><b>Ask knowledge</b></td><td>Document library inventory: title, categories, chunk count, published version, download date, link to source or “private copy” (cited without a link). Availability in Ask also depends on source classification and the caller’s scheme access. Groups: ISCC, Canada, REDcert, Nabisy, Other.</td><td>None — read-only. ISCC and REDcert documents are refreshed by the weekly scheme-document refresh; other documents are added on the operator machine.</td></tr>
<tr><td><b>Sources</b></td><td>Every public endpoint Sinetti pulls from, with when Sinetti last pulled, when the source last published, and the schedule tag (<i>on schedule</i> / <i>overdue</i> / <i>manual</i>)</td><td>None — jump links</td></tr>
<tr><td><b>Regulatory monitor</b></td><td>Health per country, the review queue, and for each case the exact before/after text with the official source and a fingerprint</td><td><kbd>Check now</kbd>; record a decision; link an implementation</td></tr>
</table></div>
<p>A regulatory case moves <b>New → Reviewing → Action required / No action / Superseded</b>, and from <i>Action required</i> to <b>Implemented</b> once you link the revision that changed Sinetti. Every step needs a written reason; the evidence is immutable.</p>
<p><b>Dated watches (procedure verified 2026-09-26).</b> The same morning task also checks a short list of pages that are not due every day: a URL, the first day to look, and the text that counts as news. The list covers France and the other mandate countries (Germany, the Netherlands, Belgium, Italy, Spain, Sweden, and Finland). A hit is an ordinary review case. It does not change a mandate rate. The runbook section "Dated watches" names each page, the signal, and the window.</p>
<p><b>North American watch (procedure verified 2026-09-26).</b> The daily monitor checks California LCFS, EPA, Canada CFR, Oregon and BC registers, plus official rules and guidance indexes. It compares content before parsing and verifies downloads at least weekly. The legal action email carries legal-source review cases and source failures. A separate <b>Register monitor</b> email groups pathway/register value changes, data quality or extraction changes, uncertain differences, and register check failures. It names each register in plain words, describes each observation in full once, folds Sinetti-side row renames (the register did not change) and re-issued pathways into single blocks, counts large changes instead of listing them, and then keeps unpublished observations as one line each under “Still waiting for a data release” until the active data release was built after them. Parser enrichment is not automatically a publisher change. “Not yet in a data release” is not confirmation of live publication: verify the source, use <b>Data ops → Refresh now</b>, review the candidate, then follow the publication workflow. Rules/index alerts never change legal interpretations automatically.</p>
<p>Check <code><state>/refresh/north_american_monitor/latest.json</code> for source outcomes and <code>events.jsonl</code> for before/after evidence. A failed check is unknown, not unchanged; a receipt older than 30 hours is overdue. Keep the last good baseline during recovery. A British Columbia Laws line that only says the act or regulation is current to a new date is not a review case. Gateway timeouts (HTTP 502, 503 and 504) and a dropped connection are tried again up to three times inside that request's time budget. A source that failed yesterday is checked again the next morning, even when the failure was after 06:30. The daily legal action mail keeps naming a legal source whose latest check is still a failure, and an open legal case stays until marked. The separate register mail keeps an observed difference eligible while a refresh is pending and the latest check has not read the file unchanged. If a change is uncertain, compare the official source and full stored evidence before treating it as a business update. A 06:30 run that never starts is named by the 05:30 job monitor when the last receipt is missing or older than 30 hours. EUR-Lex discovery allows 90 seconds. A publisher that keeps failing stays a visible failure. Oregon's Secretary of State rules page rejects this host; leave that source visibly failed and retry on the normal schedule. The Oregon DEQ program index does not replace the rule text. An official https page with no law-allowlist probe is on the weekly check; a change opens a review case and does not write an optimizer value. Drafts, discovery feeds that are not yet proven, and sources with no official URL stay off that check. Re-register with <code>scripts/register_regulatory_monitor_task.ps1</code> after updating the operator checkout; the task action must include <code>--include-registers</code>. This task needs the operator machine online. Merged code is not cloud monitoring. The <a href="../law-layer-runbook.md#north-american-rules-and-register-watch">runbook</a> covers verification, retry and the separate cloud-authority option.</p>
<h3>Agreement coverage in Ask</h3>
<p>Ask applies the caller's scheme access before reading or counting evidence. This applies to signed-in callers, including administrators. The agreement must permit <b>every scheme covered by a mixed source</b>. Sources without a reliable scheme classification are excluded. An explicit request for an excluded scheme receives the line “X is not in your plan”; Ask can still answer the permitted part.</p>
<p>Until their sources are classified, Ask cannot provide material-label lookups, RED default values, trade codes, fuel conversion and specification facts, or market, mandate and GHG-value calculations through its reference tools. A document listed in Knowledge is therefore not necessarily available to that caller.</p>
<p>Ask also omits holder production units and Certified Sites reads because they include unclassified capacity evidence. This applies to every signed-in caller, including administrators with access to all schemes. The ordinary holder pages and Browse maps remain available.</p>
<p>For callers with limited scheme access, Ask also omits saved views, monitor sets and activity history whose mixed content cannot be reliably scoped, plus the shared briefing and product vocabulary. <b>Nothing is deleted.</b> Reduced coverage is intentional; an unavailable source does not mean the underlying information is absent.</p>
<p>Questions where Ask found nothing good enough to cite are logged on the server (the “miss log”). That list is your shopping list for documents worth adding.</p>
</div>
<div class="d3">
<h3>Procedures</h3>
<details class="proc" id="p-reg-case"><summary>Handle a regulatory review case<span class="k">Screen · 10 min</span></summary><div class="body">
<ol class="steps">
<li>Admin → Ask → Regulatory monitor. Check <i>Last successful check</i> is this morning; a stale time means the daily task did not finish.</li>
<li>Pick a case in the <i>Review queue</i>. Compare <i>Previous accepted observation</i> with <i>Current official observation</i>; open the <i>Official source</i>.</li>
<li>Write the <i>Admin decision reason</i> — the legal or market reasoning — and press <kbd>Reviewing</kbd>, <kbd>Action required</kbd>, <kbd>No action</kbd> or <kbd>Superseded</kbd>.</li>
<li>When the change has been built and merged: enter the <i>Revision</i> and an <i>Implementation note</i>, <kbd>Link revision</kbd>.</li>
<li>To re-check one country or instrument now: choose the <i>Check scope</i>, <kbd>Check now</kbd>. Overlapping requests reuse the run already in progress.</li>
</ol></div></details>
<details class="proc" id="p-law-refresh"><summary>Refresh the document library by hand<span class="k">Desk · 15 min · small embedding cost</span></summary><div class="body">
<ol class="steps">
<li>See what changed without downloading: <code>python scripts/refresh_law_docs.py --probe-only</code></li>
<li>Refresh (downloads only changed documents, updates the index incrementally): <code>python scripts/refresh_law_docs.py</code></li>
<li>Run the retrieval check. Never lower its threshold to make it pass: <code>python scripts/law_rag_smoke.py</code></li>
<li>Commit the changed index files (<code>knowledge/_manifest.json</code>, <code>knowledge/law_probe_state.json</code>, <code>rag_index.npy</code>, <code>rag_index.jsonl</code>, <code>rag_catalog.json</code>). The documents themselves stay out of git.</li>
<li>The new library reaches customers at the next deploy (<a href="#shipping">§13</a>).</li>
</ol>
<div class="note">Rebuilding the index calls a paid embedding service: cents for an incremental update, a few dollars for a full rebuild. Live evaluation of Ask answers is a separate, paid run that needs an explicit decision each time, with the scenario count and cost ceiling stated first.</div></div></details>
</div>
</section>
<section class="area topic--data" id="comms">
<h2><span class="topicIcon"><svg class="guideIcon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x='2.5' y='4.5' width='19' height='15' rx='2'/><path d='m3 7 9 6 9-6'/></svg></span><span class="n">12</span>Email and Ask safety</h2>
<p class="where"><b>Where:</b> Admin → Data ops → <i>Email previews</i> · Admin → Ask → Safety · Space secrets</p>
<div class="short"><b class="tag">In short</b>
<p>The app sends four kinds of email: invitations and password resets, watchlist alerts to members, a daily brief to you, and operational alerts from the monitors. On Hugging Face only one transport works — the mail provider's web API — because ordinary mail ports are blocked there. If it is not configured, the app does not fail: it quietly runs in <b>dry-run</b>, which looks like success and delivers nothing. Ask safety is a small inbox of questions that tripped the assistant's abuse rules.</p>
</div>
<div class="d2">
<div class="tw"><table>
<tr><th>Email</th><th>To</th><th>Sent when</th><th>Safeguards</th></tr>
<tr><td>Invite · password reset</td><td>The person</td><td>You invite; they ask for a reset</td><td>Links are single-use and expire (7 days · 1 hour). Reset links are never shown to an admin.</td></tr>
<tr><td>Watchlist alert / digest</td><td>Members who opted in</td><td>A watched holder changes (immediately or as a digest, the member's choice)</td><td>Re-checked at send time: account active, holder still on their watchlist, register still in their plan. One delivery per event.</td></tr>
<tr><td>Market daily summary</td><td>Members who ticked “Email me” on a Market list</td><td>After the nightly data refresh, on days with changes on their lists</td><td>One email per person per day: each change appears once and the person's matching lists are named at the top. Nothing on quiet days. Sent only to a <b>verified</b> address; a member proves an address in Settings → Notifications with a six-digit code mailed to it (10 minutes, one use). Invited accounts are verified already. Every send, and every failed send, is recorded and listed on <b>Admin → Customers → Email delivery</b>.</td></tr>
<tr><td>Admin daily brief</td><td>You</td><td>After the nightly</td><td>Previewable on Data ops. An error that kept coming back is named: where it happened, and the first line of the error. A server error is the exception type plus the route — the full exception text is not stored. One-off errors are listed the same way and do not by themselves make the brief say ATTENTION.</td></tr>
<tr><td>Operational alerts</td><td>You</td><td>A refresh or review is missing or failed; a regulatory case opened; a new access request</td><td>One alert, then one recovery — never a stream</td></tr>
</table></div>
<p>Settings the mail needs (Space secrets): <code>INFOMANIAK_API_TOKEN</code>, <code>SINETTI_INFOMANIAK_MAILBOX_UUID</code>, <code>SINETTI_EMAIL_FROM</code>, <code>SINETTI_PUBLIC_BASE_URL</code> (the address put into invite and reset links — wrong value, dead links), and <code>SINETTI_ADMIN_EMAIL</code> / <code>SINETTI_ACCESS_NOTIFY_EMAIL</code> for mail addressed to you.</p>
<div class="warn"><b>Dry-run is the trap.</b> With no transport configured, an invite reports “Invite created (email dry-run — copy the set-password link)”. Treat that phrase as an incident, not a convenience.</div>
<p><b>When a customer questions an alert</b> (“this certificate is not withdrawn”), check two sources before answering: the delivery record with the ordinary certificate row, <i>and</i> the register's own compliance status. The ordinary row keeps its original dates even after the scheme suspends or withdraws the certificate, so either source alone can mislead. A voluntary <b>termination</b> (the holder left while the certificate was still valid) is not a red flag and is not in the default Flags priority or the watchlist alert mail. The daily subscriber digest lists each one in its own section, under the red flags, and says the exit was voluntary and is not an enforcement action. It also names a new certificate for that holder when one starts within three days, or when the holder comes back within 90 days. That section is not counted in the red-flag subject. The admin overnight brief lists the same exits. INS, California LCFS, EPA, Canada CFR, Oregon, and BC land on the same Flags page and in the same daily digest; each weekly refresh still keeps its own staging log. The Saturday US mail is the operator brief only. <b>Withdrawn</b> is the authority cancellation. <b>Delisted</b> is Sinetti's own inference that a certificate disappeared between snapshots.</p>
<p>Watchlist digests draw the holder's country flag and the scope and material chips, and a reinstatement says which end-state the certificate came back from.</p>
<h3>Ask safety</h3>
<p><b>Question retention and customer purge.</b> Permanently deleting an organisation also deletes its Ask abuse incidents, export cohorts and organisation-tagged retrieval-miss entries through the guarded customer purge (<a href="#p-purge">section 4</a>). Other organisations' records remain. Abuse incidents expire 180 days after creation, including dismissed incidents; repeated hits do not extend that deadline. A hit after the deadline starts a new incident with its own 180-day lifetime. Alert deduplication stays within the same organisation. Export cohorts remain usable for one hour. A cleanup worker runs when the app starts and hourly thereafter: it deletes expired cohorts, expired abuse incidents and retrieval misses at 90 days. Miss entries retain up to 500 characters of question text for diagnosis and carry the organisation resolved by the server.</p>
<p><b>Where a hit came from.</b> The abuse rules read the typed question and also the page context the browser sends and the earlier turns it sends back. A hit outside the question shows its source as a prefix on the rule name, <code>page_context:</code> or <code>history:</code>, in the Safety inbox and in the alert email. A high-severity hit in any of the three is refused the same way and a watch hit is only recorded. Document passages, tool results and page context also reach the model inside labelled data blocks that it is told never to obey; that needs no admin action.</p>
<p>Older retrieval misses without an organisation tag cannot safely be attributed to a customer: an organisation purge leaves those rows for their 90-day expiry. Rows with missing or invalid dates require operator review and are preserved. If cleanup reports <code>ask_retention_failed</code> in server logs, fix access to the named store; the next hourly pass retries it. Before treating a customer purge as complete, confirm all three Ask categories completed in its deletion receipt. A failed category requires the existing preview/Resume deletion procedure; never clear a shared log manually.</p>
<p>Questions that match abuse rules, or sessions that hit the tool-call ceiling, open an incident. Alert emails never contain the question; the full text is readable only on this screen. There is one action: read, optionally note, <kbd>Dismiss</kbd>. Repeated incidents from one member are a conversation with the customer, and a reason to consider <kbd>Disable</kbd> (<a href="#lifecycle">§4</a>).</p>
</div>
</section>
<div data-toc-group="The app — shipping and running it"></div>
<section class="area topic--app" id="shipping">
<h2><span class="topicIcon"><svg class="guideIcon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d='M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z'/><path d='M14 2v6h6M8 13h8M8 17h8M8 9h2'/></svg></span><span class="n">13</span>Shipping a release</h2>
<p class="where"><b>Where:</b> operator machine — <code>scripts\release.ps1</code> · the deploy desk <code>C:\Users\azizl\sinetti-deploy</code></p>
<div class="short"><b class="tag">In short</b>
<p>A release is one exact commit, tested whole, approved by a named person for a stated reason, and only then published. One command drives it and can be re-run at any time: each run does the next safe step and stops. The long test run happens in the background, so nothing depends on a terminal staying open. Publishing replaces the app's code only — never the data, never customer state — and if production does not come up healthy on the new version, the previous version is restored automatically and the attempt is written down.</p>
</div>
<div class="d2">
<figure>
<svg viewBox="0 0 1000 330" role="img" aria-label="Release workflow: scope check, preflight, pin the deploy desk, run the release gate, approve, dry run, go live, publish. Publishing uploads, restarts, waits for the version marker and data readiness, runs a signed-in smoke test, and rolls back automatically on any failure.">
<rect class="bx" x="20" y="40" width="105" height="60" rx="8"/><text class="t" x="72" y="66">Scope check</text><text class="s" x="72" y="84">no mandates</text>
<rect class="bx" x="147" y="40" width="105" height="60" rx="8"/><text class="t" x="199" y="66">Preflight</text><text class="s" x="199" y="84">fast checks</text>
<rect class="bx" x="274" y="40" width="105" height="60" rx="8"/><text class="t" x="326" y="66">Pin desk</text><text class="s" x="326" y="84">exact commit</text>
<rect class="bx" x="401" y="40" width="105" height="60" rx="8"/><text class="t" x="453" y="66">Release gate</text><text class="s" x="453" y="84">background</text>
<rect class="gate" x="528" y="52" width="84" height="36" rx="18"/><text class="gt" x="570" y="75">APPROVE</text>
<rect class="bx" x="634" y="40" width="105" height="60" rx="8"/><text class="t" x="686" y="66">Dry run</text><text class="s" x="686" y="84">touches nothing</text>
<rect class="gate" x="761" y="52" width="84" height="36" rx="18"/><text class="gt" x="803" y="75">LIVE</text>
<rect class="app" x="867" y="40" width="105" height="60" rx="8"/><text class="t" x="919" y="66">Publish</text><text class="s" x="919" y="84">to production</text>
<path class="ar" d="M125 70H145" marker-end="url(#ah)"/><path class="ar" d="M252 70H272" marker-end="url(#ah)"/><path class="ar" d="M379 70H399" marker-end="url(#ah)"/><path class="ar" d="M506 70H526" marker-end="url(#ah)"/><path class="ar" d="M612 70H632" marker-end="url(#ah)"/><path class="ar" d="M739 70H759" marker-end="url(#ah)"/><path class="ar" d="M845 70H865" marker-end="url(#ah)"/>
<text class="al" x="263" y="124">first run does these four, then returns at once</text>
<text class="al" x="453" y="142">re-run to poll · 45–90 min</text>
<path class="ard" d="M919 100V150H90V178" marker-end="url(#ah)"/><text class="al" x="700" y="144">what Publish does</text>
<rect class="bx2" x="20" y="180" width="140" height="50" rx="8"/><text class="t" x="90" y="210">Upload · 1 commit</text>
<rect class="bx2" x="182" y="180" width="140" height="50" rx="8"/><text class="t" x="252" y="210">Restart app</text>
<rect class="bx2" x="344" y="180" width="140" height="50" rx="8"/><text class="t" x="414" y="202">New version seen</text><text class="s" x="414" y="218">≈ 8 min · max 15</text>
<rect class="bx2" x="506" y="180" width="140" height="50" rx="8"/><text class="t" x="576" y="210">Data ready</text>
<rect class="bx2" x="668" y="180" width="140" height="50" rx="8"/><text class="t" x="738" y="210">Signed-in smoke</text>
<rect class="bx2" x="830" y="180" width="140" height="50" rx="8"/><text class="t" x="900" y="210">Approval used</text>
<path class="ar" d="M160 205H180" marker-end="url(#ah)"/><path class="ar" d="M322 205H342" marker-end="url(#ah)"/><path class="ar" d="M484 205H504" marker-end="url(#ah)"/><path class="ar" d="M646 205H666" marker-end="url(#ah)"/><path class="ar" d="M808 205H828" marker-end="url(#ah)"/>
<rect class="bad" x="300" y="274" width="400" height="40" rx="8"/><text class="t" x="500" y="299">any failure → previous version restored + receipt</text>
<path class="ard" d="M500 232V272" marker-end="url(#ah)"/>
</svg>
<figcaption>Two human decisions, asked for separately: <b>Approve</b> records who and why and proves the release with a dry run; <b>Live</b> publishes. Passing <code>-Live</code> without an approval does nothing. The outcome of every live attempt is one of four words — <code>published</code>, <code>refused</code>, <code>rolled_back</code>, <code>failed</code> — written to a receipt, and the receipt, not the terminal, is the truth.</figcaption>
</figure>
<h3>What the release gate runs</h3>
<div class="tw"><table>
<tr><th>#</th><th>Stage</th><th>Proves</th></tr>
<tr><td>1</td><td>Repository hygiene</td><td>No whitespace damage or conflict markers</td></tr>
<tr><td>2</td><td>Runtime manifest</td><td>Every file the app imports at runtime is in the shipment; scheduled-job scripts too</td></tr>
<tr><td>3</td><td>Identity and offboarding safety acceptance</td><td>Login, MFA, session revocation, offboarding, purge still hold — the reason rollback has a boundary</td></tr>
<tr><td>4</td><td>Python tests</td><td>The full backend suite — the long one</td></tr>
<tr><td>5–7</td><td>Frontend typecheck · tests · production build</td><td>The interface compiles, passes, and builds</td></tr>
<tr><td>8</td><td>Browser smoke</td><td>A real browser logs in and walks the built app</td></tr>
</table></div>
<p>The gate refuses to start on a dirty folder, refuses if another gate is running, and fails if the commit or folder changes underneath it. A stage whose inputs are byte-identical to an earlier green run may be inherited instead of re-run; the status shows which stages <i>ran</i> and which were <i>inherited</i>.</p>
<h3>Rules that bite</h3>
<ul>
<li><b>Once a commit is pinned, never chase <code>main</code>.</b> New work landing meanwhile is for the next release. Change the candidate only if you name another commit yourself, or to repair this exact candidate.</li>
<li><b>The approval is single-use and is spent before publication.</b> A live attempt that fails and rolls back has still used it; the retry needs a new approval id. This is what stops a replay.</li>
<li><b><code>-Exclude consumption,mandates</code> is an assertion, not a filter.</b> If the candidate touches those areas the workflow refuses before pinning anything. The fix is a candidate without those commits — never dropping the flag. Independently, the consumption code is left out of the shipment.</li>
<li><b>Publishing ships code and the document library, not data.</b> A data change goes live through <a href="#releases">§8</a>; a new built-in material family needs its seed step (<a href="#curation">§10</a>).</li>
<li><b>Never roll back past a used security control</b> (<a href="#lifecycle">§4</a>).</li>
<li><b>The footer release mark.</b> Every user sees the code release and the data date (for example <code>v13 · data 23 Oct</code>) in the left column of the app footer; on a phone the version sits left and the data date right, either side of the centred links. Nothing shows on a desk or dev server. For an admin the mark also shows the data publish number (<code>D155</code>: the live data release’s position among all publishes, since several can land on one day) and is a button: it opens a small panel with the code release and commit, the data publish number and revision, and when each register was last read (Zurich time), with “Copy for support”. The old CalVer names (<code>2026.10.13</code>) display as their running number (<code>v13</code>). The name comes from <code>-ReleaseName</code> at publish; tag the published commit <code>v</code> plus that name afterwards, and <code>python scripts/deploy_state.py</code> warns if they differ.</li>
<li>Never run the gate inside a chat or an awaited shell — if the window dies, orphaned test processes remain. Always detached, then poll.</li>
<li><b>The 03:00 nightly suite is the gate.</b> It runs <code>scripts/release_check.py --no-resume --keep-going</code> in <code>..\sinetti-nightly</code>, so one red stage does not hide the later ones, and a green stage still leaves a proof. A night another gate already holds the lock is <code>skipped</code>, not a failure and not a pass. The 05:30 mail includes a “PC daily test run” section when the newest verdict is failed or older than 48 hours. Runbook: <code>docs/nightly-suite-runbook.md</code>.</li>
<li><b>A reboot mid-gate is resumed, not abandoned.</b> Task “Sinetti PC 15-min resume interrupted gate” runs <code>scripts\resume_interrupted_gate.ps1</code> every 15 minutes and catches up after the machine comes back. Two resumes are allowed; a third mid-stage kill stops as <code>gate_failed</code>. Re-register with <code>scripts\register_resume_interrupted_gate_task.ps1</code> after changing the launcher. The task has no “run at logon” trigger.</li>
<li><b>Secret and dependency scan.</b> Before a pilot release, run <code>python scripts/security_check.py</code> as in <code>docs/security/security-scanning-runbook.md</code>. The scan covers every tracked file in an isolated snapshot of the pinned commit. A pass that only looked at the latest diff is not evidence. Offline mode is ineligible for release.</li>
</ul>
<h3>What publishing needs on the operator machine</h3>
<p>In the deploy desk's <code>.env</code>: a Hugging Face write token (<code>HF_TOKEN</code>), <code>SINETTI_DATA_REPO_ID</code> (so the record that an approval was used survives a rollback), a login for the smoke test (<code>SINETTI_SMOKE_USERNAME</code> / <code>SINETTI_SMOKE_PASSWORD</code>), and the map key for production builds (<code>VITE_MAPTILER_KEY_HF</code>). Any of these missing → refusal before anything is built.</p>
</div>
<div class="d3">
<h3>Procedures</h3>
<details class="proc" id="p-release"><summary>Ship a release with the resumable command<span class="k">Desk · 1–2 h elapsed, 10 min of attention</span></summary><div class="body">
<ol class="steps">
<li><b>Choose the commit</b> and get its full 40-character id (<code>git rev-parse main</code>). Work from a folder checked out at exactly that commit.</li>
<li><b>Start.</b> This verifies scope, runs fast checks, claims and pins the deploy desk, starts the gate in the background, and returns.
<pre><code>powershell -ExecutionPolicy Bypass -File scripts\release.ps1 `
-Sha <40-char-commit-id> -Baseline main -Exclude consumption,mandates</code></pre>
<div class="expect">phase <code>gate_running</code>. A refusal prints <code>REFUSING: …</code> and changes nothing.</div></li>
<li><b>Poll</b> whenever you like — either repeat the same command or, to look without advancing:
<pre><code>powershell -ExecutionPolicy Bypass -File scripts\release.ps1 -Sha <40-char-commit-id> -Status</code></pre>
<div class="expect">eventually <code>gate result: passed</code> and phase <code>awaiting_approval</code>. <code>gate_failed</code> means nothing was published; read the log under <code>.worktrees\_gate-logs\</code>.</div></li>
<li><b>Approve.</b> This writes and commits the approval on the deploy desk (that file and nothing else), runs the pre-release Trust Review decision as <i>Proceed</i>, then performs a dry run.
<pre><code>powershell -ExecutionPolicy Bypass -File scripts\release.ps1 -Sha <40-char-commit-id> `
-Exclude consumption,mandates -ApprovalId release-20260919-01 -Reviewer <your-admin-id> `
-Reason "What this release is for, in plain words" -Approve</code></pre>
<div class="expect">phase <code>dry_run_passed</code>.</div></li>
<li><b>Go live</b> — same line plus <code>-Live</code> and the release name <code>-ReleaseName</code> — the next running number, for example <code>14</code> (no leading <code>v</code>; without it the command refuses):
<pre><code>powershell -ExecutionPolicy Bypass -File scripts\release.ps1 -Sha <40-char-commit-id> `
-Exclude consumption,mandates -ApprovalId release-20260919-01 -Reviewer <your-admin-id> `
-Reason "What this release is for, in plain words" -Approve -Live -ReleaseName 14</code></pre>
<div class="expect">after about 10–20 minutes, phase <code>published</code> and a line <code>deploy receipt: …</code>.</div></li>
<li><b>Verify yourself:</b> open the app, check <code>/api/ready</code> is <code>ready</code> with the expected data revision, click through Browse and one Admin screen.</li>
<li>If the phase stays <code>publishing</code>, the outcome is <b>ambiguous</b> — the command could not match a receipt to this approval. Do not retry blindly: open the newest file in the deploy desk's <code>data\refresh\deploy_provenance\</code> and read <code>outcome</code> and <code>observed_oid</code> first.</li>
</ol></div></details>
<details class="proc" id="p-release-receipt"><summary>Read a deploy receipt<span class="k">Desk · 2 min</span></summary><div class="body">
<div class="tw"><table>
<tr><th>Field</th><th>Meaning</th></tr>
<tr><td><code>intended_sha</code></td><td>The commit you meant to publish</td></tr>
<tr><td><code>observed_oid</code></td><td>What production was independently seen to be running afterwards. Empty only when observation failed — and then the outcome can never be <code>published</code>.</td></tr>
<tr><td><code>outcome</code></td><td><code>published</code> · <code>refused</code> (stopped before anything external) · <code>rolled_back</code> (went out, failed a check, previous version restored) · <code>failed</code> (needs investigation)</td></tr>
<tr><td><code>release_check_receipt_sha256</code></td><td>Fingerprint of the gate result this publication relied on</td></tr>
<tr><td><code>original_error</code>, <code>space_run_log_tail</code></td><td>Why it failed, captured before the restart destroyed the log</td></tr>
</table></div></div></details>
<details class="proc" id="p-release-manual"><summary>The same release, step by step (for diagnosis)<span class="k">Desk</span></summary><div class="body">
<pre><code>powershell -ExecutionPolicy Bypass -File scripts\pin_deploy_desk.ps1 -Sha <commit> -Provision -NpmCi
powershell -ExecutionPolicy Bypass -File scripts\run_release_check_detached.ps1 -Worktree C:\Users\azizl\sinetti-deploy -ForceKillOrphans
powershell -ExecutionPolicy Bypass -File scripts\poll_release_check.ps1 -Worktree C:\Users\azizl\sinetti-deploy -Tail 30</code></pre>
<p>Poll exit codes: <code>0</code> still running · <code>1</code> <b>passed</b> · <code>2</code> failed · <code>3</code> no gate found for this desk. (Yes — 1 is the good one.)</p></div></details>
</div>
</section>
<section class="area topic--app" id="health">
<h2><span class="topicIcon"><svg class="guideIcon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d='M3 12h4l3 8 4-16 3 8h4'/></svg></span><span class="n">14</span>Health, monitoring and alerts</h2>
<p class="where"><b>Where:</b> Admin → System health (<code>/admin/system-health</code>) · <code>/api/health</code> · <code>/api/ready</code> · your inbox · external uptime service</p>
<div class="short"><b class="tag">In short</b>
<p>Two public addresses tell you how production is. <code>/api/health</code> only says the process is alive. <code>/api/ready</code> says whether it is <i>fit to serve</i>: data valid, storage writable, admin identity configured — and answers HTTP 503 when not. Watchers that can email you: an outside uptime service probing <code>/api/health</code>, the job monitor (a missed refresh or review, a repeated application error, the daily workload), and the regulatory monitor (a law changed). The application-error mail depends on the daily local sample; a burst earlier in the day can be missed until an opt-in catch-up is run.</p>
</div>
<div class="d2">
<h3>Using System health</h3>
<p>Open <b>Admin → System health</b> to inspect the app that is running now. The page reads <code>/api/ready</code> when opened and again only when you choose <b>Check again</b>; there is no continuous refresh. A degraded HTTP 503 still carries the readiness details. Serving data, writable persistence and administrator identity determine the overall readiness result. Entity links, corpus loading and frontend build information are useful diagnostics but do not decide that result. Expand the saved-data stores to inspect every returned check; the list opens itself when a store reports a problem. Treat a check as Healthy only when it explicitly passes. Unknown means the response lacks reliable evidence; Unavailable means the request failed. An unavailable frontend modification time does not prove the interface is fresh. After a failed check, the previous result is not current evidence.</p>
<p><b>Running versions</b> shows the revision reported by this running app and the active data release. An absent code revision reads Unknown; bundled data without a revision has no recorded release ID. Copy the code revision only when one is known. <b>Open Data ops</b> takes you to the serving-data and release workflow; System health itself does not promote or roll back data.</p>
<p><b>Recent errors</b> is a separate 30-day history. Filter locally between app-server and browser reports, then expand a group for its recorded type, count, first and last seen, and safe support reference. An error recorded yesterday does not prove a current fault or its cause; green readiness does not prove the error was resolved. An empty history means the ledger was read and no groups were found. If the error history is Unavailable, do not interpret it as zero errors. Readiness and error history load independently, so either can remain useful when the other cannot be reached. The administrator-only metadata endpoint <code>/api/admin/ops/system-health</code> returns safe grouped identifiers and the running revision; it excludes raw event text, customer paths, stack traces and credentials. A ledger-read failure returns <code>ok: false</code> with a safe message and <code>errors: null</code>, while a successful empty read returns <code>ok: true</code> with <code>errors: []</code>.</p>
<h3>Reading <code>/api/ready</code></h3>
<div class="tw"><table>
<tr><th>Block</th><th>Healthy</th><th>Unhealthy → likely cause</th></tr>
<tr><td><code>status</code></td><td><code>ready</code> (HTTP 200)</td><td><code>degraded</code> (HTTP 503) — one of the next three is false</td></tr>
<tr><td><code>data</code></td><td><code>ok: true</code> — the serving data passed validation</td><td><code>validation: critical</code> + reason → the served database failed validation. <code>release.source: bundled</code> identifies the bundled fallback; review the release reason and <a href="#releases">§8</a> separately, even if data validation passed</td></tr>
<tr><td><code>persistence</code></td><td>every check <code>ok</code></td><td><code>unavailable</code> → storage not mounted or file missing. <code>schema incomplete</code> → a migration did not finish. <code>contended</code> → busy; only a fault if it persists</td></tr>
<tr><td><code>identity</code></td><td><code>ok: true</code></td><td><code>administrative_identity_degraded</code> → administrator sign-in configuration is not operational (<a href="#logins">§2</a>)</td></tr>
<tr><td><code>frontend</code></td><td><code>stale: false</code> with a known bundle and usable modification times</td><td>Informational only; never makes the app degraded. <code>mtime_unavailable</code> means freshness was not established. On the operator machine, compare the reported folder and bundle with the desk being checked</td></tr>
</table></div>
<h3>Who watches what</h3>
<div class="tw"><table>
<tr><th>Watcher</th><th>Runs</th><th>Emails you when</th><th>Does not cover</th></tr>
<tr><td>External uptime probe<br><span class="where">UptimeRobot, free plan, five minutes</span></td><td>Outside Hugging Face</td><td><code>/api/health</code> stops answering 200 “ok”</td><td>A live app serving wrong or fallback data. Simulated DOWN and UP emails were received on 25 September 2026. It does not prove an application-error email.</td></tr>
<tr><td>Job monitor</td><td>Operator machine, daily 05:30</td><td>A register's refresh or its review is missing past its allowance (ISCC/REDcert, and 2BSvs/RSB after activation: 1 day + 6 h · INS: 7 days + 12 h), or ended unusable; a PDF queue grows for several nights; a burst of application errors; the regulatory monitor receipt is missing or older than 30 hours</td><td>Anything when the machine is off</td></tr>
<tr><td>Regulatory monitor</td><td>Operator machine, daily 06:30</td><td>An official text changed, including an official https page that has no law-allowlist probe, and the open case stays until it is marked, with the previous and current lines; a source whose latest check is still a failure, until a later check succeeds; weekly health on Mondays. The separate Register monitor email covers register observations and check failures with its own receipt</td><td>A run that never starts. The 05:30 job monitor names that</td></tr>
<tr><td>Nightly test suite</td><td>Operator machine, daily 03:00</td><td>Does not send its own mail. The 05:30 job monitor names a failed verdict, or one older than 48 hours</td><td>A <code>skipped</code> night (another gate was running) is neutral</td></tr>
</table></div>
<p>The job monitor sends <b>one</b> alert per incident and <b>one</b> recovery message when a genuinely newer good run arrives — never a stream. Do not close an incident by hand: fix the job and let the next good run close it. A mail titled “PDF queue is still growing” means the nightly parse cap is not shrinking that scheme’s queue. The register refresh can still have succeeded. The mail names the last night the depth fell, how many certificate PDFs that night parsed, and what is still waiting (new documents, an older parser version, earlier failures, and ISCC audit templates the parser now recognises).</p>
<p>Its 05:30 slot is deliberate: the evidence it reads is the copy mirrored to the machine at 03:00, after the nightly data refresh has finished, so it normally shows the same night. When that mirror run was skipped or the machine was off, the copy is the <i>previous</i> night's, which stays inside its allowance only until about 09:00 — keep the task between roughly 04:45 and 09:00 or a skipped mirror raises a false “missing”. If a reminder disagrees with the Hugging Face jobs page, start “Sinetti PC daily HF sync” by hand before believing it. Since 21 September 2026 the monitor first pulls a fresh copy itself (about 15 seconds), so it normally reads the same night even when the mirror task ran early or was skipped.</p>
<p>Since 23 September 2026 the monitor mails you <b>every day</b>. The mail opens with <b>Workload to clear</b>: how many certificate PDFs are still to parse per scheme (ISCC, REDcert, INS) and how many ISCC audit reports, each split into parser work, review and not downloaded, with what changed since the previous count. Since 25 September 2026 a fourth row, <b>Material names awaiting a family</b>, counts the register material names still sitting in Other on Admin → Materials that carry certificates (split into 5+ certificates and 1–4), with how many arrived and how many you cleared since the previous mail — every refresh brings new names, and they wait there until you place them. Since 28 September 2026 a fifth row, <b>Companies awaiting group research</b>, counts the certificate holders the corporate-group research has not reached yet (split into those beside a production plant, researched first, and the rest), counted fresh each morning with the job's own selection. It should fall while the quarterly drain runs; a count that only grows means the drain is not running (log <code>data/refresh/company_group_drain.log</code>). On a day with nothing failing the mail is the “Daily admin summary” and the subject carries the two totals; on a failing day the usual “Still failing” list follows the workload. The morning a check first fails, that check’s own alert is the only mail — the daily summary does not also go out and call the desk healthy.</p>
<p>Since 12 October 2026 the mail carries one line, <b>Certificate PDFs</b>, while any in-force certificate is neither read nor an accepted exception (counts per scheme, how many need a decision, accounted share). It reports the published data after the previous morning OCR run and disappears when everything is accounted.</p>
<p>Since 25 September 2026 the mail also says when <b>sinetti.ch serves a late register</b>. Each register is judged against its own schedule — ISCC and REDcert every night (2BSvs and RSB too once the nightly job carries them; until then they are not judged); INS, the US registers (LCFS, EPA), Canada CFR, Oregon and BC every week — and counts as late one day after its refresh was due. When last night's refresh already published newer data that is not live yet, the mail says so and gives the command to run on your PC: <code>python scripts\promote_release_on_space.py --list</code>. It also says when newer data is ready even if nothing is late yet, with a command pinned to that email's exact full dataset commit: <code>python scripts\promote_release_on_space.py --hub-revision <full commit> --restart-space</code>. If the commit cannot be verified, the mail offers review only. When the deployed code cannot serve that release because it needs a newer data-plane version, the headline reads <b>Newer data needs a code deploy first</b>, the mail names both versions and offers review only: deploy newer code, then promote. And it names any Hugging Face cloud job that has been running for 6 hours or more, because every promotion refuses while another cloud job runs. Nothing late, nothing waiting and no long job: no section.</p>
<p>A Trust Review mail lists its five checks (Freshness, Status evidence, Identity, Coverage, Publication) as <b>passed</b>, <b>could not run</b> or <b>found a problem</b>. “Could not complete (data unaffected)” in the subject means a check could not run — a tool problem, nothing wrong found in the data. “Found a blocking problem” means the new data failed a check: do not promote that candidate. ISCC and REDcert share one nightly run, so one cause sends one mail for both.</p>
<p><b>Application errors.</b> Unhandled server errors and authenticated client error reports are recorded in an internal ledger. System health displays safe groups from its most recent 30 days; individual events remain available through the administrator API. The daily local Trust monitor opens an incident and attempts an operator email when one server error repeats at least three times, or one client error repeats at least five times, within its preceding 15-minute sample. Earlier bursts can be missed. A verified-snapshot catch-up (<code>--catch-up-errors</code> on <code>scripts/monitor_trust_jobs.py</code>, with the three inputs the runbook names) is opt-in and is not a scheduled task. Dry-run changes nothing. A catch-up incident stays open until the verified snapshot covers a complete quiet 15-minute interval after the last event; a quiet daily sample cannot close it. A failed delivery can be retried with <code>--retry-failed-error-delivery</code> on a live catch-up invocation. The receipt names that file's size and modification time (`ledger_bytes`, `ledger_mtime`) and not the rows inside it. On 25 September 2026 five hosted client reports were accepted. Infomaniak accepted both the emergency mail (21:10 UTC) and the recovery mail (21:28 UTC), and the operator confirmed both messages were in the inbox. The live daily monitor was not the sender. See <code>docs/security/evidence/2026-09-25-restore-offmachine-and-error-mail.md</code> and <code>docs/data-trust-runbook.md</code>.</p>
</div>
<div class="d3">
<h3>Procedures</h3>
<p><b>PDF monitor liveness is separately opt-in.</b> While healthy, the PC monitor detects failures on its next daily execution, with a maximum 24-hour sweep interval. The independent hosted preflight rejects a missing, invalid or more-than-26-hour-old completed PC heartbeat. With the existing daily hosted cadence, detection is at most <b>50 hours from the last completed heartbeat while the hosted job runs</b>. Both hosts unavailable has <b>no bounded alert delivery</b>; evidence is unknown on recovery and the streak resets. These are not immediate-alert guarantees. Use <a href="#p-pdf-live-coverage">the coverage procedure</a> to inspect receipt, export/readback and genuine hosted-check evidence.</p>
<details class="proc" id="p-alert"><summary>You received an alert email — now what<span class="k">10 min</span></summary><div class="body">
<ol class="steps">
<li><b>“Down” from the uptime service:</b> open <code>/api/ready</code>. No answer at all → open the Space page on Hugging Face: is it <i>Running</i>, <i>Building</i>, or in error? Restart it. Still failing → read the Space logs; if this began right after a deploy, the deploy should already have rolled back — read its receipt (<a href="#p-release-receipt">§13</a>).</li>
<li><b>“Missing/failed run” from the job monitor:</b> check the job's true status (<a href="#p-check-nightly">§7</a>). Fix the cause; the next good run sends the recovery mail.</li>
<li><b>“PDF queue is still growing”:</b> the register refresh may still have succeeded. Read the reason: it says how many PDFs last night parsed and what is still waiting (new documents, an older parser version, earlier failures, ISCC audit templates). The incident closes when a later night shows the depth fell. The same text appears on the next daily reminder; the first alert is not sent again.</li>
<li><b><code>/api/ready</code> degraded on <code>data</code>:</b> restart once — self-heal steps back to a good release at start. If it returns as <code>bundled</code>, promote a known-good revision (<a href="#p-promote-prod">§8</a>).</li>
<li><b>Degraded on <code>persistence</code>:</b> confirm the storage bucket is still attached to the Space and <code>SINETTI_STATE_DIR</code> points at it.</li>
<li>Write down what happened and when. Security-relevant events follow <code>docs/security/incident-response-runbook.md</code>.</li>
</ol></div></details>
<details class="proc" id="p-monitor-dry"><summary>Ask the job monitor what it thinks, without sending anything<span class="k">Desk · 1 min</span></summary><div class="body">
<pre><code>$env:APP_BACKEND = 'local'
python scripts\monitor_trust_jobs.py --state-dir <state folder> --dry-run</code></pre>
<p>Exit <code>0</code> healthy · <code>1</code> open incidents · <code>2</code> the monitor or its mail transport is itself broken.</p></div></details>
</div>
</section>
<section class="area topic--app" id="state">
<h2><span class="topicIcon"><svg class="guideIcon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d='M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z'/><path d='M17 21v-8H7v8'/><path d='M7 3v5h8'/></svg></span><span class="n">15</span>State, backup and recovery</h2>
<p class="where"><b>Where:</b> production storage bucket · operator machine backup folder</p>
<p class="where">Verified against the Windows task “Sinetti PC daily customer-state backup”, the isolated restore receipt, and dataset <code>Couscouskabyle/sinetti-customer-state-backups</code>, 25 September 2026. The task is registered for 06:15. It ran once at 00:09 on 26 September 2026 (result 0), stamp <code>20260925T220919Z</code>. The 06:15 trigger is still ahead. Stamp <code>20260925T205902Z</code> is a manual copy of the local mirror. That stamp was restored into an isolated folder and uploaded once. Stamp <code>20260925T220919Z</code> was restored into a separate isolated folder the same night (RPO 5 minutes, RTO 1 minute for that copy). The daily task does not upload, and the production bucket still has no backup series.</p>
<div class="short"><b class="tag">In short</b>
<p>Almost everything in Sinetti can be rebuilt: code from git, register data by refreshing, releases by promoting. What cannot be rebuilt is <b>state</b>: customers' watchlists and saved views, accounts and organisations, audit trails, your curation decisions — and the day-by-day history of register changes, which no register publishes and which is the core of the product's value. State lives in one storage bucket attached to production. Backups are a command you schedule on the operator machine; a restore only counts once you have rehearsed it.</p>
</div>
<div class="d2">
<div class="tw"><table>
<tr><th style="width:230px">Thing</th><th>If lost</th><th>Protected by</th></tr>
<tr><td>App code</td><td>Rebuild from git</td><td>Local repository; private GitHub mirror of <code>main</code>; the Space repository</td></tr>
<tr><td>Register data and releases</td><td>Refresh and promote again; two previous releases retained</td><td>Dataset repository history; self-heal; bundled fallback</td></tr>
<tr><td><b>Main state database</b> (<code>intel.db</code>): accounts, organisations, plans, watchlists, alerts, change history, meters, audits</td><td><b>Gone</b></td><td>Customer-state backup (required member)</td></tr>
<tr><td><b>Saved views</b></td><td><b>Gone</b></td><td>Customer-state backup (required member)</td></tr>
<tr><td><b>Customer administrator authority</b> (<code>customer-admin.json</code>)</td><td><b>Gone</b></td><td>Customer-state backup (optional inventory member; required and identity-checked for customer-edition restore)</td></tr>
<tr><td>Admin app settings (<code>settings.json</code>: certificate expiry window)</td><td>Back to defaults</td><td>Customer-state backup (optional member; the customer edition keeps it in <code>/state</code>)</td></tr>
<tr><td>Findings, Ask-safety incidents, regulatory cases, identity and related-company decisions, timeline sets</td><td>Gone</td><td>Customer-state backup (optional members — recorded as missing if absent)</td></tr>
<tr><td>Ask retrieval-miss questions (<code>ask_rag_misses.jsonl</code>)</td><td>Gone</td><td>New stamps include optional <code>ask_rag_misses.jsonl.enc</code>; requires the separately retained backup key to recover</td></tr>
<tr><td>Refresh history ledgers (<code>change_events.jsonl</code> and siblings)</td><td><b>Gone — and irreplaceable</b></td><td><span class="pill gap">Gap</span> not in the backup set yet</td></tr>
<tr><td>Geocoded coordinates, parsed-PDF archives</td><td>Expensive to rebuild (money and weeks)</td><td>Files on the operator machine; keep a copy off it</td></tr>
<tr><td>Secrets</td><td>Regenerate, never restore</td><td>Password manager</td></tr>
</table></div>
<p>Production state is reached through <code>SINETTI_STATE_DIR</code> (<code>/data/sinetti</code>), a storage bucket mounted into the Space and into the nightly job. If that variable is unset, the app writes state inside its own container and <b>loses it at every deploy</b> — silently. The bucket can only be written from inside Hugging Face; the operator machine receives a mirror through the daily local sync.</p>
<div class="warn"><span class="pill gap">Gap</span> <b>What is still missing:</b> a repeating off-machine copy (the 06:15 task stays on this PC); a backup of the production bucket itself; a named second recovery operator; a rebuild-from-nothing procedure if the Hugging Face account were lost. The local restore drill recorded RPO 8 minutes and RTO 1 minute for stamp <code>20260925T205902Z</code>. A later restore of stamp <code>20260925T220919Z</code> recorded RPO 5 minutes and RTO 1 minute. Those figures are those drills, not hosted targets. <b>As of 2026-09-25 21:31 UTC:</b> the 06:15 task ran once at 00:09 on 26 September 2026 (result 0), stamp <code>20260925T220919Z</code>. The 06:15 trigger remains. Stamp <code>20260925T205902Z</code> is a manual copy of the local mirror (<code>intel.db</code> 7,278,592 bytes). An isolated restore of that stamp passed, and that same stamp was uploaded once to the private dataset <code>Couscouskabyle/sinetti-customer-state-backups</code>. The hosted file listed the same afternoon was 4,866,048 bytes. OneDrive is not signed in (<code>0x8004de25</code>), so the OneDrive folder is not the off-machine copy. Detail: <code>docs/security/evidence/2026-09-25-restore-offmachine-and-error-mail.md</code>.</div>
</div>
<div class="d3">
<h3>Procedures</h3>
<details class="proc" id="p-backup"><summary>Back up customer state<span class="k">Desk · 2 min · before anything risky</span></summary><div class="body">
<pre><code>$env:APP_BACKEND = 'local'
$env:SINETTI_BACKUP_DIR = 'C:\Users\azizl\sinetti-backups\customer-state'
.\.venv\Scripts\python.exe scripts\backup_customer_state.py --git-sha (git rev-parse --short HEAD)</code></pre>
<p class="expect">the path of a new time-stamped folder holding the files and a <code>MANIFEST.json</code> of checksums. A missing <i>required</i> file aborts the backup. Older folders beyond the newest 14 successful stamps are pruned.</p>
<p><b>Ask miss-log protection (code and isolated tests verified 9 October 2026):</b> new stamps encrypt only the optional question-log member with AES-256-GCM. The Sinetti operator supplies <code>SINETTI_ASK_MISS_BACKUP_KEY</code>, a dedicated random 32-byte key encoded as base64, to the backup and restore processes. Keep the key separately in approved secret custody, available to the recovery operator. Never put it in stamps, Git, receipts or logs, or reuse an API key or session secret. Sinetti does not create or print a key; this change has not configured the live scheduled task. The existing operator-controlled production mirror retains the private plaintext source.</p>
<p>A present log, including an empty one, with missing or invalid key configuration aborts the backup. An absent log needs no key. Supply the retained operator key and retry; do not delete the source log to bypass failure. Historical stamps without the optional member remain readable. Historical stamps are never rewritten by customer purge: live questions are erased through the existing customer process, while backup generations expire under the newest-14 retention policy.</p>
<p>The folder must not be the live state directory <code>C:\Users\azizl\sinetti\data</code>. This PC has no D: drive. The daily task “Sinetti PC daily customer-state backup” (06:15, <code>scripts/register_customer_state_backup_task.ps1</code>) is registered for that folder. It ran once at 00:09 on 26 September 2026 (result 0), stamp <code>20260925T220919Z</code>. The 06:15 trigger is still ahead. The first stamp in the folder was written by hand. The 00:09 run added <code>20260925T220919Z</code>, and that stamp was restored into an isolated folder. It was not uploaded. The user environment variable <code>SINETTI_BACKUP_DIR</code> names it; a program that is already running does not see a newly set variable until it is started again. Stamp <code>20260925T205902Z</code> is a copy of the local mirror. On 25 September 2026 that one stamp was uploaded to the private dataset <code>Couscouskabyle/sinetti-customer-state-backups</code>. The daily task does not repeat that upload. The 30 July 2026 stamp under the older AppData folder is a different series.</p></div></details>
<details class="proc" id="p-restore"><summary>Rehearse a restore (the drill)<span class="k">Desk · 15 min · at least before each new customer</span></summary><div class="body">
<ol class="steps">
<li>Pick a recent backup folder; open its <code>MANIFEST.json</code>.</li>
<li>If it contains <code>ask_rag_misses.jsonl.enc</code>, supply the separately retained <code>SINETTI_ASK_MISS_BACKUP_KEY</code> to the restore process. Ciphertext checksums and log authentication are checked before any target writes. A wrong/missing key, damaged ciphertext or invalid envelope refuses recovery without changing the target. Check key custody and select an intact stamp; do not edit the manifest to bypass a failure. Success recovers <code>ask_rag_misses.jsonl</code>. Older generations can recover erased questions: keep the drill isolated and reapply relevant erasure before returning restored state to service.</li>
<li>Restore into an <b>empty, separate</b> folder — never over live state:
<pre><code>.\.venv\Scripts\python.exe scripts\restore_customer_state.py `
--manifest "$env:SINETTI_BACKUP_DIR\<stamp>\MANIFEST.json" --target-dir C:\Users\azizl\sinetti-restore-drills\<date></code></pre></li>
<li>Confirm exit 0 and that the checksums of <code>intel.db</code> and <code>saved_views.json</code> match the manifest.</li>
<li>Note how long it took and how old the backup was. File a receipt under <code>data\refresh\ops_drills\</code> (template: <code>docs/ops-drill-receipt.example.json</code>).</li>
</ol>
<div class="warn">Restoring over live state is refused unless you pass <code>--i-understand-overwrite</code> <b>and</b> the target is on an explicit allow-list. If you ever need it for real, stop the app first and take a fresh backup of the damaged state before overwriting it. If the target folder already holds a database, the restore removes its <code>-wal</code> and <code>-shm</code> files first so an earlier write cannot replay onto the snapshot.</div></div></details>
<details class="proc" id="p-acceptance"><summary>Run the two-customer acceptance gate<span class="k">Desk · 10 min · before each real customer</span></summary><div class="body">
<pre><code>$env:APP_BACKEND = 'local'
.\.venv\Scripts\python.exe scripts\bilateral_acceptance_gate.py --operator $env:USERNAME --git-sha (git rev-parse --short HEAD)</code></pre>
<p>It creates two throwaway organisations in a temporary folder — it never touches live state — and proves twelve things: isolation between them, plan entitlements, rate and export limits with audit, release identity, admin MFA, session revocation, offboarding and reactivation, lifecycle receipts, guarded permanent deletion, a restore, the shape of a deploy receipt, and the ownership trail.</p>
<p class="expect">a <code>PASS</code> line per check, exit 0, and the path of a receipt. Anything else: do not onboard.</p></div></details>
</div>
</section>
<div data-toc-group="Reference"></div>
<section class="area topic--neutral" id="menu">
<h2><span class="topicIcon"><svg class="guideIcon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d='M4 6h16M4 12h16M4 18h16'/></svg></span><span class="n">16</span>The Admin menu, screen by screen</h2>
<div class="short"><b class="tag">In short</b>
<p>Everything admin lives in one place: click your avatar, and the account menu shows an <b>Admin</b> column to the right of your personal items (Alerts, Activity, Settings, Language, Sign out). Members never see it; typing an admin address sends them quietly to Browse. Ten entries. Related pages share one entry and switch with a row of tabs under the page title; each tab keeps its own address, so older links and bookmarks still open the right page.</p>
</div>
<div class="d2">
<div class="tw"><table>
<tr><th style="width:170px">Menu entry</th><th>What it is for</th><th style="width:110px">Edits?</th><th style="width:70px">See</th></tr>
<tr><td>System health</td><td>Current readiness, running versions and safe groups of recent errors at <code>/admin/system-health</code>; <b>Check again</b> refreshes the evidence</td><td>No</td><td><a href="#health">§14</a></td></tr>
<tr><td>Environment</td><td>Assistant model routing, “expiring soon” window, which keys are configured, whether live register fetch is available, map point colours by scope</td><td>Yes</td><td><a href="#curation">§10</a></td></tr>
<tr><td>Materials</td><td>To-do list, families and how names aggregate, short labels, spelling groups and kind (read-only)</td><td>Yes</td><td><a href="#curation">§10</a></td></tr>
<tr><td><i>Data</i> · Data ops</td><td>Tabs: <b>Refresh</b> (morning briefing, next-12-month parse inflow, queue history, refresh log, geocoding budget, email previews) · <b>Sources</b> (every source endpoint with freshness and schedule tags)</td><td>No</td><td><a href="#trust">§9</a> · <a href="#knowledge">§11</a></td></tr>
<tr><td><i>Data</i> · Data Trust</td><td>PDF coverage arithmetic, identity and related-company queues, Trust Review findings. Tabs: <b>Overview · Formats · Admin QA · User reports</b></td><td>Yes</td><td><a href="#trust">§9</a></td></tr>
<tr><td><i>Data</i> · Research</td><td>Tabs: <b>Coverage</b> (the research data switch — show research to customers, or certificate data only — and the researched facilities not yet matched to a holder) · <b>Site footprint</b> (China-pilot site classification review)</td><td>Research data switch; Site footprint</td><td><a href="#curation">§10</a></td></tr>
<tr><td><i>Data</i> · Ask</td><td>Tabs: <b>Knowledge</b> (the document library Ask can cite) · <b>Safety</b> (assistant abuse incidents) · <b>Regulatory monitor</b> (law-change review cases)</td><td>Dismiss, decide cases</td><td><a href="#knowledge">§11</a> · <a href="#comms">§12</a></td></tr>
<tr><td>Customers</td><td>Tabs: <b>Organisations</b> (organisations with their people, how each joined and last seen; the Everyone view; needs-a-decision box; data access limits, history, suspend or offboard) · <b>Plans</b> (plans side by side, default AI caps) · <b>Access</b> (invites, requests, users, lifecycle) · <b>Market templates</b> · <b>Legal</b> (draft Data Processing Agreement, privacy policy, terms and the counsel packet, read from <code>docs/legal/</code>; Download .md to send; edit the files in git, never on the page) · <b>Email delivery</b> (what the daily job sent and to whom, masked; read-only). The badge counts pending access requests.</td><td>Yes</td><td><a href="#customers">§3</a>–<a href="#limits">5</a></td></tr>
<tr><td>Admin guide</td><td>This guide, inside the app</td><td>No</td><td>—</td></tr>
<tr><td>Product</td><td>“How it works” with live audited figures, the sales crib, a <b>Slide deck</b> link (<code>/api/admin/sales-deck/</code>), and an editable private valuation note</td><td>Note only</td><td>—</td></tr>
</table></div>
<h3>What else differs for an admin</h3>
<ul>
<li><b>Freshness wording.</b> You see a banner only when a register is late against its own schedule (ISCC and REDcert nightly, plus 2BSvs and RSB after activation; INS, LCFS, EPA, CFR, Oregon and BC weekly): nightly “Late” at two days old and “Stale” at seven; weekly “Late” at eight days and “Stale” at fourteen. The banner names late registers and any safely recommended newer release waiting to go live. Nothing is shown while everything is on time. Members see only “Data as of <i>date</i>”.</li>
<li><b>Holder cards</b> carry an extra <kbd>Also..</kbd> control (similar counterparties) that members do not have.</li>
<li><b>Settings → Security</b> adds the <i>Administrator verification</i> block.</li>
<li><b>You do not see</b> the guided tour or the alerts nudge, and you bypass every meter — so your own experience is <i>not</i> the customer's. Always verify customer-facing changes as a non-admin member.</li>
<li>On the operator machine only, <kbd>Customer preview</kbd> in the account menu hides all admin chrome until you press <kbd>Back to admin view</kbd>.</li>
</ul>
<p>Errors on admin screens are deliberately plain (“… is temporarily unavailable”). Each carries a trace id that finds the matching line in the server log.</p>
<p><b>Support bundle.</b> Data ops → <b>Download support bundle</b> card, <code>/api/admin/ops/support-bundle</code> (or <code>python scripts/support_bundle.py --out file.json</code>) downloads a content-free summary: versions, served release, names of settings present, AI route, private-mode summary, and error counts by route and exception class. No user ids, holder names, searches or error text. Private Cloud customers send it when asking for help — see <code>docs/enterprise/private-cloud-gcp/support-bundle.md</code>.</p>
</div>
</section>
<section class="area topic--neutral" id="settings">
<h2><span class="topicIcon"><svg class="guideIcon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d='M4 21v-7M4 10V3M12 21v-9M12 8V3M20 21v-5M20 12V3M1 14h6M9 8h6M17 16h6'/></svg></span><span class="n">17</span>Settings and secrets</h2>
<div class="short"><b class="tag">In short</b>
<p>Configuration lives in three places: <b>Space secrets</b> (production), the <b>nightly job's registration</b> (frozen when registered), and the operator machine's <b>.env</b> file (never committed). This section lists names and purposes only — values belong in a password manager. On handover, secrets are regenerated, not copied.</p>
</div>
<div class="d2">
<h3>Production — Space secrets and variables</h3>
<div class="tw"><table>
<tr><th>Name</th><th>Purpose</th><th>If missing or wrong</th></tr>
<tr><td><code>SINETTI_SESSION_SECRET</code></td><td>Signs login sessions</td><td>Nobody can log in. Changing it logs everyone out.</td></tr>
<tr><td><code>SINETTI_USERS</code></td><td>Owner-provisioned accounts (hashes)</td><td>Nobody from the roster can log in; a duplicate id voids the whole list</td></tr>
<tr><td><code>SINETTI_MFA_ENCRYPTION_KEY</code></td><td>Protected stored authenticator secrets (32 random bytes, base64). Emailed-code sign-in does not use it.</td><td>On an authenticator-era build, a missing key refused admin login and degraded <code>/api/ready</code>. On a build that reports marker <code>aed958ce</code>, login uses email. Never reuse the session secret. Do not delete the key until an administrator has signed in with an emailed code on that build.</td></tr>
<tr><td><code>SINETTI_STATE_DIR</code></td><td>Where persistent state lives (the mounted bucket)</td><td>State resets at every deploy — silently</td></tr>
<tr><td><code>SINETTI_DATA_REPO_ID</code></td><td>The private dataset repository for candidates</td><td>App serves only bundled data</td></tr>
<tr><td><code>ANTHROPIC_API_KEY</code></td><td>Ask Sinetti, narrative, research</td><td>Assistant unavailable</td></tr>
<tr><td><code>SINETTI_PRIVATE_MODE</code> · <code>SINETTI_EGRESS_ALLOW</code> · <code>SINETTI_RELEASE_PUBKEYS</code></td><td>Private Cloud only: refuse outbound connections except approved hosts; extra trusted release-signing keys. See <code>docs/enterprise/private-cloud-gcp/egress-acceptance.md</code> and <code>offline-releases.md</code></td><td>Never set on this Space: private mode would stop the data pull and mail.</td></tr>
<tr><td><code>SINETTI_AI_PROVIDER</code></td><td>Where AI requests go: <code>anthropic</code> (default, this Space), or a customer's own <code>vertex</code> / <code>bedrock</code> / <code>azure</code>, or <code>off</code>. Private Cloud only changes it — see <code>docs/enterprise/private-cloud-gcp/ai-routing.md</code></td><td>Unset = Anthropic. A misspelled value turns AI off rather than guessing.</td></tr>
<tr><td><code>SINETTI_AZURE_AI_ENDPOINT</code> · <code>SINETTI_AI_MODEL_MAP</code></td><td>For <code>azure</code>, the customer platform team sets the customer's Microsoft Foundry resource base URL (<code>https://<resource>.services.ai.azure.com/anthropic</code>) and maps Sinetti model IDs to deployment names. The team builds with <code>ENTERPRISE_AI=1</code> and grants the app's managed/workload identity invocation permission through Microsoft Entra; no Foundry API key is stored. The Sinetti admin verifies the configured workflows through customer acceptance. This is an AI route; an Azure deployment reference is not built.</td><td>A missing or invalid endpoint leaves AI unavailable. Correct the endpoint, model map, dependencies or identity permissions and repeat acceptance. Configuration status does not prove a live invocation; none was run for this implementation.</td></tr>
<tr><td><code>INFOMANIAK_API_TOKEN</code> · <code>SINETTI_INFOMANIAK_MAILBOX_UUID</code> · <code>SINETTI_EMAIL_FROM</code></td><td>Outgoing mail</td><td>Dry-run: looks sent, is not</td></tr>
<tr><td><code>SINETTI_PUBLIC_BASE_URL</code></td><td>Address written into invite and reset links</td><td>Dead links in emails</td></tr>
<tr><td><code>SINETTI_ADMIN_EMAIL</code> · <code>SINETTI_ACCESS_NOTIFY_EMAIL</code></td><td>Where operational mail goes. After the emailed-code release, <code>SINETTI_ADMIN_EMAIL</code> is also the last-resort destination for an administrator's sign-in code</td><td>You are not told. With no address on the account either, administrator sign-in cannot send a code</td></tr>
<tr><td><code>SINETTI_AUTO_ACTIVATE_NIGHTS</code></td><td>Automatic data promotion after N clean nights</td><td>Unset or 0 = manual promotion only</td></tr>
<tr><td><code>WEB_CONCURRENCY</code></td><td>Number of app workers (shipped as 2)</td><td>1 = one slow request blocks everyone</td></tr>
</table></div>
<div class="warn"><b>Never set on production:</b> <code>SINETTI_DEV_AUTH_BYPASS</code> and <code>SINETTI_LOCAL_ADMIN_MFA_BYPASS</code>. Both switch protection off for local development and are built to be inert on Hugging Face — do not test that.</div>
<h3>Operator machine — <code>.env</code></h3>
<div class="tw"><table>
<tr><th>Name</th><th>Purpose</th></tr>
<tr><td><code>HF_TOKEN</code> (and <code>HF_JOB_TOKEN</code>, <code>HF_CREATE_TOKEN</code>)</td><td>Publish, roll back, launch and register cloud jobs. A “bucket does not exist” error usually means the token lacks scope, not that the bucket is gone.</td></tr>
<tr><td><code>SINETTI_SMOKE_USERNAME</code> · <code>SINETTI_SMOKE_PASSWORD</code></td><td>The login the post-deploy smoke test uses</td></tr>
<tr><td><code>VITE_MAPTILER_KEY</code> · <code>VITE_MAPTILER_KEY_HF</code></td><td>Satellite map imagery, baked in at build time (local · production)</td></tr>
<tr><td><code>GOOGLE_MAPS_KEY</code></td><td>Geocoding, passed to the nightly job at registration</td></tr>
<tr><td><code>INS_PDF_NEW_MAX_PDFS</code> · <code>INS_PDF_CRITERIA_MAX_PDFS</code> · <code>INS_PDF_SITE_MAX_PDFS</code></td><td>Weekly INS PDF caps. The runner default is zero (never). <code>register_ins_weekly_task.ps1</code> raises a missing or lower value to 1,200, the same drain budget as the nightly certificate-PDF cap. Uncached GSE refetch stays off.</td></tr>
<tr><td><code>INS_PDF_SITE_REEXTRACT</code></td><td>Lets the weekly INS refresh re-read certificates that already have sites when the site reader has improved, within the site cap. It replaces stored sites only when the new reading keeps everything the old one said. Off unless set to 1.</td></tr>
<tr><td><code>SINETTI_PDF_SOURCE_AUDIT_ENABLED</code> · <code>SINETTI_PDF_COVERAGE_MONITOR_ENABLED</code></td><td>Separate source-audit and production/reciprocal-monitor opt-ins, both off unless explicitly set to 1. Audit caps separately default to zero. No Environment-screen controls; local merge does not change live configuration. See <a href="#pdf-live-coverage">verified live coverage</a>.</td></tr>
<tr><td><code>SINETTI_PDF_COVERAGE_INPUT_REQUEST</code> · <code>SINETTI_PDF_COVERAGE_INPUT_REQUEST_SHA256</code></td><td>Independent inventory/evidence request and exact byte-hash pin. Source-only loading permits acquisition before Read proof; production coverage still requires the full strong evidence chain. Missing/invalid proof remains unknown.</td></tr>
<tr><td><code>SINETTI_PDF_SOURCE_AUDIT_MAX_CHECKS</code> · <code>SINETTI_PDF_SOURCE_AUDIT_ISCC_MAX</code> · <code>SINETTI_PDF_SOURCE_AUDIT_REDCERT_MAX</code> · <code>SINETTI_PDF_SOURCE_AUDIT_INS_MAX</code></td><td>Separate all-unit source-check caps, each default zero; existing processing caps/runtime are retained. A cap is not evidence of measured capacity.</td></tr>
<tr><td><code>SINETTI_PDF_COVERAGE_DRY_RUN</code></td><td>When set to 1, withholds the new monitor runtime effects and acceptance. The PC's legacy <code>--now</code> also cannot seed acceptance.</td></tr>
<tr><td><code>SINETTI_BACKUP_DIR</code></td><td>Where backups go; also required before permanent deletion is allowed</td></tr>
<tr><td><code>SINETTI_ASK_MISS_BACKUP_KEY</code></td><td>Dedicated base64 32-byte Ask miss-log backup key; retain separately for recovery</td></tr>
<tr><td><code>SINETTI_JOB_AUTO_ACTIVATE</code></td><td>Lets a local refresh promote its own clean result on this machine only</td></tr>
<tr><td><code>SINETTI_DEV_AUTH_BYPASS</code></td><td>Skips login locally (you appear as “Admin (dev)”)</td></tr>
</table></div>
</div>
</section>
<section class="area topic--neutral" id="machine">
<h2><span class="topicIcon"><svg class="guideIcon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x='3' y='4' width='18' height='16' rx='1'/><path d='M3 10h18M3 15h18M9 4v16'/></svg></span><span class="n">18</span>The operator machine</h2>
<div class="short"><b class="tag">In short</b>
<p>One Windows PC holds the only copy of the repository (it has no remote by design), three special folders — the working folder, the “latest” serving desk and the deploy desk — and the weekly scheduled tasks. Two cleanup scripts exist because improvised cleanup has destroyed live work twice. Both do nothing until you add a flag.</p>
</div>
<div class="d2">
<div class="tw"><table>
<tr><th>Folder</th><th>Role</th><th>Rule</th></tr>
<tr><td><code>C:\Users\azizl\sinetti</code></td><td>The live working folder; scheduled tasks run from here</td><td>Merges never happen here; it only fast-forwards</td></tr>
<tr><td><code>C:\Users\azizl\sinetti-latest</code></td><td>Serves a clean copy of <code>main</code> on port 8511 for you to look at</td><td>No work in progress; reset to <code>main</code> on every open</td></tr>
<tr><td><code>C:\Users\azizl\sinetti-deploy</code></td><td>The deploy desk, pinned to the release commit</td><td>“Detached HEAD” there is the pin, not a mistake. Never delete it.</td></tr>
<tr><td><code>sinetti-*</code>, <code>.worktrees\*</code></td><td>Temporary work desks</td><td>May share Python and node folders by link — delete links with <code>rmdir</code>, never recursively</td></tr>
</table></div>
</div>
<div class="d3">
<h3>Procedures</h3>
<details class="proc" id="p-open-latest"><summary>Look at the latest app locally<span class="k">Desk · 1–10 min</span></summary><div class="body">
<pre><code>powershell -ExecutionPolicy Bypass -File scripts\open_latest_main.ps1
powershell -ExecutionPolicy Bypass -File scripts\open_latest_main.ps1 -Path /browse</code></pre>
<p>Reuses the running server when it already serves the current <code>main</code>; otherwise rebuilds and restarts, then opens your browser. It serves this machine's active data release and state. If it refuses because the desk is dirty, it says so and changes nothing.</p>
<p>A restart does not take the app down. Port 8511 is a small front door that passes every request to one of two server ports, 8512 or 8513. The new version starts on the idle one while the old one keeps answering; the front door switches over only once the new one is ready, and the old one stops a few seconds later. The scheduled task <b>Sinetti PC 5-min keep latest main</b> does the same in the background whenever <code>main</code> moves and nobody is using the app. Each server keeps its own copy of the web pages, so rebuilding them for the next version never affects the one you are using.</p></div></details>
<details class="proc" id="p-rebuild-venv"><summary>Rebuild a desk's Python environment off python.org<span class="k">Desk · 15 min</span></summary><div class="body">
<p>The Microsoft Store Python breaks the release gate (Playwright cannot see its browser, and <code>python -I</code> tests fail). Rebuild one desk at a time, only when no gate or PC daily test run is using it, and only with the python.org interpreter — never the venv you are about to replace. The old venv is kept outside the checkout. Full steps: <code>docs/runbook.md</code>.</p>
<ol class="steps">
<li>Confirm <code>python.exe -c "import sys; print(sys.base_prefix)"</code> does not contain <code>WindowsApps</code>.</li>
<li>Preview, then apply. Deploy desk first, then the live folder:
<pre><code><python.org python> scripts\rebuild_venv.py --target C:\Users\azizl\sinetti-deploy
<python.org python> scripts\rebuild_venv.py --target C:\Users\azizl\sinetti-deploy --apply</code></pre></li>
</ol></div></details>
<details class="proc" id="p-zombies"><summary>Clean up stuck processes and dead work folders<span class="k">Desk · 5 min</span></summary><div class="body">
<pre><code>powershell -ExecutionPolicy Bypass -File scripts\kill_zombies.ps1 # lists only
powershell -ExecutionPolicy Bypass -File scripts\kill_zombies.ps1 -Kill
powershell -ExecutionPolicy Bypass -File scripts\cleanup_dead_worktrees.ps1 # lists only
powershell -ExecutionPolicy Bypass -File scripts\cleanup_dead_worktrees.ps1 -Apply</code></pre>
<ul>
<li>The first never touches the app on ports 8511–8513 (the front door and its two servers), anything with a live connection, or the refresh, gate, deploy and promotion jobs. Every kill is logged to <code>%LOCALAPPDATA%\Sinetti\kill-zombies.log</code>.</li>
<li>The second removes only folders whose work is fully in <code>main</code>, clean, and idle for 12 hours; never the deploy, latest, release or nightly desks; removals go to a trash folder first.</li>
<li><b>Never</b> kill processes by matching “sinetti” in a path, and never bulk-delete work folders by hand.</li>
</ul></div></details>
</div>
</section>
<section class="area topic--neutral" id="broken">
<h2><span class="topicIcon"><svg class="guideIcon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d='M12 3 2 20h20z'/><path d='M12 10v4'/><path d='M12 17h.01'/></svg></span><span class="n">19</span>When something breaks</h2>
<div class="short"><b class="tag">In short</b>
<p>Start from the symptom. Most surprises in Sinetti are one of three things: a job that silently did not run, a setting that was never set, or data that was never promoted.</p>
</div>
<div class="d2">
<div class="tw"><table>
<tr><th>Symptom</th><th>Most likely</th><th>Go to</th></tr>
<tr><td>Nobody can log in</td><td>Session secret or user list missing/invalid; duplicate <code>user_id</code></td><td><a href="#logins">§2</a></td></tr>
<tr><td>Admins cannot log in; members can</td><td>On production today: MFA key missing or changed. After the emailed-code deploy: the code was not delivered (no address, or mail dry-run)</td><td><a href="#logins">§2</a></td></tr>
<tr><td>One member cannot log in: “seats are in use”</td><td>More members than seats; they were added last</td><td><a href="#customers">§3</a></td></tr>
<tr><td>One customer cannot log in: “access is paused”</td><td>Organisation suspended, offboarded, or agreement end date passed</td><td><a href="#lifecycle">§4</a></td></tr>
<tr><td>Member sees “not included in your plan”</td><td>Feature not ticked in their plan</td><td><a href="#customers">§3</a></td></tr>
<tr><td>Member sees “We couldn't complete this action”</td><td>Rate or export limit reached — or the meter's storage is unavailable</td><td><a href="#limits">§5</a></td></tr>
<tr><td>Ask says “allowance used”</td><td>Monthly AI cap reached</td><td><a href="#limits">§5</a></td></tr>
<tr><td>New member has a tiny AI allowance</td><td>Added after the plan was assigned; budgets never stamped</td><td><a href="#p-change-org">§3</a></td></tr>
<tr><td>Invite “sent” but never arrives</td><td>Mail in dry-run</td><td><a href="#comms">§12</a></td></tr>
<tr><td>Data looks days old although nightly “ran”</td><td>Job died without a receipt; or candidates exist but none was promoted</td><td><a href="#refresh">§7</a>, <a href="#releases">§8</a></td></tr>
<tr><td><code>/api/ready</code> says <code>source: bundled</code></td><td>Active pointer or releases broken; app fell back</td><td><a href="#releases">§8</a></td></tr>
<tr><td>Customer data vanished after a deploy</td><td><code>SINETTI_STATE_DIR</code> unset or bucket detached</td><td><a href="#state">§15</a></td></tr>
<tr><td>New INS certificates never get pathways</td><td>INS PDF caps unset (zero)</td><td><a href="#refresh">§7</a></td></tr>
<tr><td>PDF queues never shrink</td><td>Nightly caps re-registered at zero</td><td><a href="#p-register-nightly">§7</a></td></tr>
<tr><td>New material family shows nothing</td><td>Seed assignment not run after deploy</td><td><a href="#curation">§10</a></td></tr>
<tr><td>Ask does not know a document you added</td><td>Library not deployed yet</td><td><a href="#knowledge">§11</a></td></tr>
<tr><td>Release refuses: “excluded release paths changed”</td><td>Candidate contains mandate or consumption work — working as intended</td><td><a href="#shipping">§13</a></td></tr>
<tr><td>Release stuck in <code>publishing</code></td><td>Ambiguous outcome; read the receipt before anything else</td><td><a href="#p-release">§13</a></td></tr>
<tr><td>Retry of a failed publish refused</td><td>Approval already spent; use a new approval id</td><td><a href="#shipping">§13</a></td></tr>
<tr><td>False “missing run” email every morning</td><td>Job monitor task moved out of its 03:05–09:00 slot</td><td><a href="#health">§14</a></td></tr>
<tr><td>Local app shows an old interface</td><td>Another folder's server is answering on that port</td><td><a href="#p-open-latest">§18</a></td></tr>
</table></div>
</div>
</section>
<section class="area topic--neutral" id="gaps">
<h2><span class="topicIcon"><svg class="guideIcon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx='12' cy='12' r='9'/><path d='M12 11v6'/><path d='M12 7h.01'/></svg></span><span class="n">20</span>Known gaps</h2>
<div class="short"><b class="tag">In short</b>
<p>Things the product does not do today, collected so that nobody plans around a capability that is not there. Documentation mismatches found on 19 September 2026 were closed in the same sitting as the freshness rule.</p>
</div>
<div class="d2">
<h3>Product</h3>
<ul>
<li>An invited member cannot be made an admin from the app; invites always create non-admin accounts.</li>
<li>No screen returns a hand-capped member to plan-controlled AI budgets.</li>
<li>Members added to an organisation after plan assignment receive no AI budget until the plan is re-saved.</li>
<li>System health shows grouped recent application errors; individual ledger events remain available through the administrator API.</li>
<li>No screens for the certification-body register or the INS place corrections — both are data files maintained through development work.</li>
<li>The site-footprint “rescan” exists on the server without a button.</li>
<li>Plan changes made by script are validated for feature names only, not register or AI-budget names.</li>
</ul>
<h3>Operations</h3>
<ul>
<li>Backups, as of 2026-09-25 21:31 UTC: the daily 06:15 task ran once at 00:09 on 26 September 2026 (result 0), stamp <code>20260925T220919Z</code>. The 06:15 trigger remains. Stamp <code>20260925T205902Z</code> is a manual local-mirror copy. An isolated restore of it passed, and that stamp was uploaded once to private dataset <code>Couscouskabyle/sinetti-customer-state-backups</code>. Stamp <code>20260925T220919Z</code> was restored into a separate isolated folder (RPO 5 minutes, RTO 1 minute). The production bucket itself still has no backup series. The daily task does not upload. Change-history ledgers stay outside the backup set. The local restore drill recorded RPO 8 minutes and RTO 1 minute. No hosted recovery targets, no named second operator, and no rebuild-from-nothing procedure.</li>
<li>UptimeRobot DOWN and UP test emails were received on 25 September 2026. The same day the operator confirmed both application-error drill messages in the inbox (emergency 21:10 UTC, recovery 21:28 UTC). The live daily monitor was not the sender, so OPS-02 stays in progress on that limit.</li>
<li>Retention and deletion periods, the data-processing agreement and the supplier transfer register are open legal items (PRIV-01, PRIV-02, SUP-01).</li>
<li>No registration script in the repository for the CFR weekly task.</li>
<li>The monthly law-refresh task was last noted exiting with an error and an empty log when run by the scheduler (August 2026); check its history.</li>
<li>MFA key rotation has no tooling. It applies while production still stores authenticator factors.</li>
</ul>
<h3>Documentation</h3>
<ul>
<li>An older installation may still hold the Phase-3 default of 120 requests per 5 minutes. The code default is 2 000; read Admin → Customers → Organisations → <i>Data access limits</i>, not a design spec.</li>
</ul>
</div>
</section>
<section class="area topic--neutral" id="glossary">
<h2><span class="topicIcon"><svg class="guideIcon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d='M4 4.5A2 2 0 0 1 6 3h13v16H6a2 2 0 0 0-2 2z'/><path d='M4 19.5A2 2 0 0 1 6 18h13'/></svg></span><span class="n">21</span>Glossary</h2>
<div class="tw"><table>
<tr><th style="width:190px">Term</th><th>Meaning</th></tr>
<tr><td>Active pointer</td><td>The one-line file naming which release the app serves</td></tr>
<tr><td>Candidate</td><td>A complete release published by a refresh job, not yet served</td></tr>
<tr><td>Deploy desk</td><td>The folder pinned to the exact commit being released</td></tr>
<tr><td>Disposition</td><td>Your recorded decision on a finding or a review pair, with its reason</td></tr>
<tr><td>Dry-run</td><td>Goes through the motions and changes nothing. For email: nothing was delivered.</td></tr>
<tr><td>Entitlement</td><td>What a member may do right now, derived from organisation and plan</td></tr>
<tr><td>Finding</td><td>One Trust Review observation about a candidate; advisory</td></tr>
<tr><td>Gate (release gate)</td><td>The full automated test run a commit must pass before approval</td></tr>
<tr><td>Heartbeat</td><td>Evidence that a scheduled job finished usefully; what the job monitor looks for</td></tr>
<tr><td>Operator machine</td><td>The Windows PC holding the repository, desks and scheduled tasks</td></tr>
<tr><td>Organisation</td><td>A customer under a bilateral agreement</td></tr>
<tr><td>Owner organisation</td><td>The built-in organisation with all features and registers; for you only</td></tr>
<tr><td>Promote</td><td>Move the active pointer to a reviewed release</td></tr>
<tr><td>Receipt</td><td>A small file a job writes when it <i>finishes</i>. Its absence or age is the signal; its content alone proves nothing.</td></tr>
<tr><td>Release (data)</td><td>The served database plus its manifest, stored under a revision id and never changed</td></tr>
<tr><td>Release (app)</td><td>One commit of code, gated, approved and published</td></tr>
<tr><td>Roster</td><td>The owner-provisioned accounts kept in a Space secret</td></tr>
<tr><td>Space</td><td>The Hugging Face container that runs the production app</td></tr>
<tr><td>State</td><td>Everything customers and you created inside the app; lives in the storage bucket</td></tr>
<tr><td>Step-up</td><td>An emailed six-digit code entered within the last five minutes, required for sensitive actions. On production, until that release is deployed, this is still an authenticator code</td></tr>
<tr><td>Trust Review</td><td>The automatic reviewer of each candidate</td></tr>
</table></div>
<p class="where" style="margin-top:18px">Sources: the runbooks under <code>docs/</code> (auth, billing, bilateral-acceptance, daily-refresh, ins-local-refresh, data-plane, data-trust, law-layer, ops-harden, local-hf-sync, nightly-suite, security/), <code>docs/OWNER-HANDOVER.md</code>, and the code they describe. This guide is the map; those are the territory.</p>
</section>
</main>
</div>
<script src="../../frontend/public/admin-guide/guide.js"></script>
</body>
</html>
<html lang="en" data-depth="2">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Sinetti Admin Guide</title>
<style>
/* Field Journal palette — values mirror tokens.py / frontend/src/theme.css */
:root{
--bg:#E9DFC6; --panel:#FBF7EC; --raised:#FFFCF4; --hair:#C9B98A; --hair-soft:#D6C8A8;
--ink:#23271C; --dim:#574F3B; --muted:#655A3E; --accent:#3E6B4F; --warm:#B5613C;
--danger-bg:#F7E0D7; --danger-ink:#7A241C; --danger-border:#E0A992; --surface:#F0ECE0;
--serif:"Newsreader Variable",Georgia,"Times New Roman",serif;
--sans:"Inter Variable",system-ui,-apple-system,"Segoe UI",sans-serif;
--mono:ui-monospace,"Cascadia Mono",Consolas,monospace;
--toc-w:270px; --bar-h:52px;
}
*{box-sizing:border-box}
html{scroll-behavior:smooth;scroll-padding-top:calc(var(--bar-h) + 16px)}
body{margin:0;background:var(--bg);color:var(--ink);font:400 15.5px/1.6 var(--sans)}
a{color:var(--accent)} a:hover{color:var(--warm)}
code{font:0.86em var(--mono);background:var(--surface);border:1px solid var(--hair-soft);border-radius:4px;padding:0 4px}
pre{font:12.5px/1.55 var(--mono);background:#2A2E22;color:#F0E9D6;border-radius:8px;padding:12px 14px;overflow-x:auto;margin:10px 0}
pre code{background:none;border:0;padding:0;color:inherit;font-size:inherit}
/* top bar */
.bar{position:sticky;top:0;z-index:20;height:var(--bar-h);display:flex;align-items:center;gap:18px;
padding:0 20px;background:var(--ink);color:#F0E9D6}
.bar__brand{font:600 1.1rem var(--serif);letter-spacing:.01em;white-space:nowrap}
.bar__brand small{font:400 .78rem var(--sans);opacity:.7;margin-left:8px}
.depth{display:flex;margin-left:auto;border:1px solid #6d6a55;border-radius:999px;overflow:hidden}
.depth button{all:unset;cursor:pointer;padding:5px 14px;font:500 .8rem var(--sans);color:#d9d2ba;white-space:nowrap}
.depth button[aria-pressed="true"]{background:#F0E9D6;color:var(--ink)}
.depth button:focus-visible{outline:2px solid var(--warm);outline-offset:-2px}
.bar__tools button{all:unset;cursor:pointer;font:.78rem var(--sans);color:#d9d2ba;padding:4px 8px;border-radius:6px}
.bar__tools button:hover{background:#3a3e30}
/* layout */
.shell{display:grid;grid-template-columns:var(--toc-w) minmax(0,1fr);max-width:1320px;margin:0 auto}
nav.toc{position:sticky;top:var(--bar-h);align-self:start;height:calc(100vh - var(--bar-h));overflow:auto;
padding:22px 14px 40px 20px;border-right:1px solid var(--hair);font-size:.84rem}
nav.toc h4{font:600 .7rem var(--sans);letter-spacing:.09em;text-transform:uppercase;color:var(--muted);margin:18px 0 6px}
nav.toc a{display:flex;gap:7px;align-items:start;padding:5px 8px;border-radius:5px;color:var(--ink);text-decoration:none;line-height:1.35}
nav.toc a:hover{background:var(--panel)}
nav.toc a.on{background:var(--topic-soft);box-shadow:inset 3px 0 var(--topic);color:var(--ink)}
nav.toc .guideIcon{width:17px;height:17px;color:var(--topic);margin-top:1px}
main{padding:34px 44px 120px;min-width:0}
/* type */
h1{font:600 2.3rem/1.15 var(--serif);margin:0 0 6px}
h2{font:600 1.65rem/1.2 var(--serif);margin:0 0 4px;display:flex;align-items:center;gap:10px}
h2 .n{font:600 .85rem var(--sans);color:var(--topic);padding:0}
h3{font:600 1.12rem/1.3 var(--serif);margin:26px 0 6px}
h4{font:600 .92rem var(--sans);margin:18px 0 4px}
p{margin:8px 0} ul,ol{margin:8px 0;padding-left:22px} li{margin:3px 0}
.lede{font:400 1.08rem/1.55 var(--serif);color:var(--dim);max-width:70ch}
.where{font:.8rem var(--sans);color:var(--muted);margin:0 0 10px}
.where b{color:var(--ink);font-weight:600}
section.area{background:var(--panel);border:1px solid var(--hair);border-radius:12px;padding:26px 30px;margin:26px 0;
box-shadow:0 2px 8px rgba(70,50,20,.06)}
section.plain{margin:26px 0}
.topic--neutral{--topic:#3E6B4F;--topic-soft:#E6EFE8;--topic-line:#B9CFBF}
.topic--people{--topic:#71507B;--topic-soft:#EFE7F3;--topic-line:#D2C1DB}
.topic--data{--topic:#276C68;--topic-soft:#E4F1ED;--topic-line:#B5D3C9}
.topic--app{--topic:#385F8B;--topic-soft:#E6EDF6;--topic-line:#BACCE0}
section.area{border-top:3px solid var(--topic);background:linear-gradient(var(--topic-soft),var(--panel) 98px)}
.guideIcon{flex:none;vertical-align:middle}
.guideIcon .iconWash{opacity:0}
.topicIcon,.guideTitleIcon{display:inline-flex;align-items:center;justify-content:center;flex:none;border:1px solid var(--topic-line);background:var(--topic-soft);color:var(--topic)}
.topicIcon{width:38px;height:38px;border-radius:10px}
.guideTitleIcon{width:48px;height:48px;margin-right:12px;border-radius:12px}
.guideTitleIcon svg{width:26px;height:26px}
/* "in short" — the summary layer */
.short{background:var(--raised);border-left:4px solid var(--accent);border-radius:0 8px 8px 0;padding:10px 16px;margin:12px 0 4px}
.short b.tag{display:block;font:600 .68rem var(--sans);letter-spacing:.1em;text-transform:uppercase;color:var(--accent);margin-bottom:2px}
.short p{margin:4px 0}
section.area .short{border-left-color:var(--topic)}
section.area .short b.tag{color:var(--topic)}
/* callouts */
.note,.warn,.rule{border-radius:8px;padding:9px 14px;margin:12px 0;font-size:.92rem}
.note{background:#E7EFF7;border:1px solid #BECDDF;color:#274765}
.warn{background:var(--danger-bg);border:1px solid var(--danger-border);color:var(--danger-ink)}
.rule{background:#E6EFE8;border:1px solid #B9CFBF;color:#294B34}
.note b:first-child,.warn b:first-child,.rule b:first-child{font-weight:600}
/* tables */
.tw{overflow-x:auto;margin:12px 0}
table{border-collapse:collapse;width:100%;font-size:.88rem;background:var(--raised)}
th,td{border:1px solid var(--hair-soft);padding:6px 10px;text-align:left;vertical-align:top}
th{background:var(--surface);font:600 .74rem var(--sans);letter-spacing:.04em;text-transform:uppercase;color:var(--dim)}
/* procedures — the detail layer */
details.proc{border:1px solid var(--hair);border-radius:9px;background:var(--raised);margin:10px 0}
details.proc>summary{cursor:pointer;list-style:none;padding:10px 14px;font:600 .95rem var(--sans);display:flex;gap:10px;align-items:center}
details.proc>summary::-webkit-details-marker{display:none}
details.proc>summary::before{content:"▸";color:var(--warm);transition:transform .15s}
details.proc[open]>summary::before{transform:rotate(90deg)}
details.proc>summary .k{margin-left:auto;font:500 .7rem var(--sans);color:var(--muted);text-transform:uppercase;letter-spacing:.06em}