DATA_COLLECTION_PIPELINE.html 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537
  1. <!doctype html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="utf-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1">
  6. <title>Field-to-Model: Correction Data Pipeline</title>
  7. <style>
  8. :root {
  9. --bg: #F2F1E7;
  10. --bg-alt: #E7E4D5;
  11. --panel-line: rgba(27,36,30,0.16);
  12. --ink: #1B241E;
  13. --ink-dim: rgba(27,36,30,0.64);
  14. --ink-faint: rgba(27,36,30,0.42);
  15. --line: rgba(27,36,30,0.14);
  16. --accent: #B9711F;
  17. --accent-soft: rgba(185,113,31,0.12);
  18. --accent-ink: #FBF7EE;
  19. --green: #1F7A5C;
  20. --green-soft: rgba(31,122,92,0.10);
  21. --red: #A3402E;
  22. --red-soft: rgba(163,64,46,0.10);
  23. --mono: ui-monospace, "SF Mono", "Cascadia Code", "Roboto Mono", Consolas, "Liberation Mono", monospace;
  24. --serif: Georgia, "Iowan Old Style", "Palatino Linotype", "Book Antiqua", serif;
  25. --shadow: 0 1px 2px rgba(27,36,30,0.06), 0 8px 24px rgba(27,36,30,0.05);
  26. }
  27. @media (prefers-color-scheme: dark) {
  28. :root {
  29. --bg: #101B16;
  30. --bg-alt: #17251E;
  31. --panel-line: rgba(233,231,220,0.14);
  32. --ink: #E9E7DC;
  33. --ink-dim: rgba(233,231,220,0.66);
  34. --ink-faint: rgba(233,231,220,0.42);
  35. --line: rgba(233,231,220,0.13);
  36. --accent: #DDA35C;
  37. --accent-soft: rgba(221,163,92,0.14);
  38. --accent-ink: #17130C;
  39. --green: #55C29A;
  40. --green-soft: rgba(85,194,154,0.12);
  41. --red: #D97F6C;
  42. --red-soft: rgba(217,127,108,0.12);
  43. --shadow: 0 1px 2px rgba(0,0,0,0.3), 0 8px 28px rgba(0,0,0,0.28);
  44. }
  45. }
  46. :root[data-theme="dark"] {
  47. --bg: #101B16; --bg-alt: #17251E; --panel-line: rgba(233,231,220,0.14);
  48. --ink: #E9E7DC; --ink-dim: rgba(233,231,220,0.66); --ink-faint: rgba(233,231,220,0.42);
  49. --line: rgba(233,231,220,0.13); --accent: #DDA35C; --accent-soft: rgba(221,163,92,0.14);
  50. --accent-ink: #17130C; --green: #55C29A; --green-soft: rgba(85,194,154,0.12);
  51. --red: #D97F6C; --red-soft: rgba(217,127,108,0.12);
  52. --shadow: 0 1px 2px rgba(0,0,0,0.3), 0 8px 28px rgba(0,0,0,0.28);
  53. }
  54. :root[data-theme="light"] {
  55. --bg: #F2F1E7; --bg-alt: #E7E4D5; --panel-line: rgba(27,36,30,0.16);
  56. --ink: #1B241E; --ink-dim: rgba(27,36,30,0.64); --ink-faint: rgba(27,36,30,0.42);
  57. --line: rgba(27,36,30,0.14); --accent: #B9711F; --accent-soft: rgba(185,113,31,0.12);
  58. --accent-ink: #FBF7EE; --green: #1F7A5C; --green-soft: rgba(31,122,92,0.10);
  59. --red: #A3402E; --red-soft: rgba(163,64,46,0.10);
  60. --shadow: 0 1px 2px rgba(27,36,30,0.06), 0 8px 24px rgba(27,36,30,0.05);
  61. }
  62. * { box-sizing: border-box; }
  63. html, body { margin: 0; padding: 0; }
  64. body {
  65. background: var(--bg);
  66. color: var(--ink);
  67. font-family: var(--serif);
  68. font-size: 17px;
  69. line-height: 1.65;
  70. -webkit-font-smoothing: antialiased;
  71. }
  72. a { color: var(--accent); }
  73. a:focus-visible, button:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
  74. .wrap { max-width: 800px; margin: 0 auto; padding: 56px 24px 96px; }
  75. .eyebrow {
  76. font-family: var(--mono);
  77. font-size: 12px;
  78. letter-spacing: 0.11em;
  79. text-transform: uppercase;
  80. color: var(--accent);
  81. display: inline-flex;
  82. align-items: center;
  83. gap: 8px;
  84. margin-bottom: 18px;
  85. }
  86. .eyebrow::before {
  87. content: "";
  88. width: 7px; height: 7px;
  89. background: var(--accent);
  90. border-radius: 50%;
  91. display: inline-block;
  92. }
  93. h1 {
  94. font-family: var(--mono);
  95. font-weight: 600;
  96. font-size: clamp(28px, 4.4vw, 38px);
  97. line-height: 1.18;
  98. letter-spacing: -0.01em;
  99. text-wrap: balance;
  100. margin: 0 0 18px;
  101. color: var(--ink);
  102. }
  103. .subtitle {
  104. font-family: var(--serif);
  105. font-style: italic;
  106. font-size: 19px;
  107. color: var(--ink-dim);
  108. max-width: 62ch;
  109. text-wrap: balance;
  110. margin: 0 0 8px;
  111. }
  112. .meta-row {
  113. display: flex;
  114. flex-wrap: wrap;
  115. gap: 18px;
  116. margin-top: 28px;
  117. padding-top: 20px;
  118. border-top: 1px solid var(--line);
  119. font-family: var(--mono);
  120. font-size: 12px;
  121. color: var(--ink-faint);
  122. }
  123. .meta-row span b { color: var(--ink-dim); font-weight: 600; }
  124. nav.toc {
  125. margin-top: 32px;
  126. padding: 18px 20px;
  127. border: 1px solid var(--line);
  128. border-radius: 10px;
  129. background: var(--bg-alt);
  130. }
  131. nav.toc p {
  132. font-family: var(--mono);
  133. font-size: 11px;
  134. letter-spacing: 0.09em;
  135. text-transform: uppercase;
  136. color: var(--ink-faint);
  137. margin: 0 0 10px;
  138. }
  139. nav.toc ol {
  140. margin: 0; padding: 0; list-style: none;
  141. columns: 2;
  142. column-gap: 24px;
  143. font-family: var(--mono);
  144. font-size: 13px;
  145. }
  146. nav.toc li { break-inside: avoid; margin-bottom: 8px; }
  147. nav.toc a { text-decoration: none; color: var(--ink-dim); }
  148. nav.toc a:hover { color: var(--accent); }
  149. nav.toc a .n { color: var(--ink-faint); margin-right: 6px; }
  150. section { margin-top: 64px; scroll-margin-top: 24px; }
  151. .sec-head {
  152. display: flex;
  153. align-items: baseline;
  154. gap: 12px;
  155. border-bottom: 1px solid var(--line);
  156. padding-bottom: 10px;
  157. margin-bottom: 22px;
  158. }
  159. .sec-head .n {
  160. font-family: var(--mono);
  161. font-size: 13px;
  162. color: var(--accent);
  163. letter-spacing: 0.04em;
  164. }
  165. .sec-head h2 {
  166. font-family: var(--mono);
  167. font-size: 13px;
  168. letter-spacing: 0.1em;
  169. text-transform: uppercase;
  170. font-weight: 600;
  171. color: var(--ink);
  172. margin: 0;
  173. }
  174. p { margin: 0 0 16px; color: var(--ink); }
  175. p.lead { font-size: 18px; color: var(--ink-dim); }
  176. strong { color: var(--ink); }
  177. ul.principles, ul.risks { list-style: none; margin: 0; padding: 0; display: grid; gap: 12px; }
  178. ul.principles li, ul.risks li {
  179. border: 1px solid var(--line);
  180. background: var(--bg-alt);
  181. border-radius: 10px;
  182. padding: 14px 16px;
  183. display: flex;
  184. gap: 12px;
  185. align-items: flex-start;
  186. }
  187. ul.principles li::before { content: "✓"; flex: 0 0 auto; width: 20px; color: var(--green); font-family: var(--mono); font-weight: 700; }
  188. ul.risks li::before { content: "!"; flex: 0 0 auto; width: 20px; color: var(--red); font-family: var(--mono); font-weight: 700; text-align: center; }
  189. ul.principles li .txt, ul.risks li .txt { flex: 1 1 auto; min-width: 0; }
  190. ul.principles li .t, ul.risks li .t { font-weight: 700; color: var(--ink); display: block; margin-bottom: 3px; }
  191. ul.principles li .d, ul.risks li .d { color: var(--ink-dim); font-size: 15.5px; }
  192. .panel {
  193. border: 1px solid var(--panel-line);
  194. border-radius: 12px;
  195. background: var(--bg-alt);
  196. box-shadow: var(--shadow);
  197. margin: 24px 0;
  198. overflow: hidden;
  199. }
  200. .panel .bar {
  201. display: flex;
  202. align-items: center;
  203. gap: 8px;
  204. padding: 10px 16px;
  205. border-bottom: 1px solid var(--panel-line);
  206. font-family: var(--mono);
  207. font-size: 11.5px;
  208. letter-spacing: 0.06em;
  209. text-transform: uppercase;
  210. color: var(--ink-faint);
  211. }
  212. .panel .bar .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--ink-faint); opacity: 0.5; }
  213. .panel .body { padding: 18px; overflow-x: auto; }
  214. .panel .body .mermaid { margin: 0; display: flex; justify-content: center; min-width: 560px; }
  215. table {
  216. width: 100%;
  217. border-collapse: collapse;
  218. font-size: 15px;
  219. }
  220. .table-scroll { overflow-x: auto; border: 1px solid var(--line); border-radius: 10px; }
  221. table thead th {
  222. font-family: var(--mono);
  223. font-size: 11px;
  224. letter-spacing: 0.07em;
  225. text-transform: uppercase;
  226. color: var(--ink-faint);
  227. text-align: left;
  228. padding: 12px 16px;
  229. background: var(--bg-alt);
  230. border-bottom: 1px solid var(--line);
  231. white-space: nowrap;
  232. }
  233. table tbody td {
  234. padding: 13px 16px;
  235. border-bottom: 1px solid var(--line);
  236. vertical-align: top;
  237. color: var(--ink-dim);
  238. }
  239. table tbody tr:last-child td { border-bottom: none; }
  240. table tbody td.field { font-family: var(--mono); font-size: 13.5px; color: var(--ink); white-space: nowrap; }
  241. table code { font-family: var(--mono); font-size: 0.92em; background: var(--accent-soft); color: var(--accent); padding: 1px 6px; border-radius: 4px; }
  242. .pill {
  243. display: inline-block;
  244. font-family: var(--mono);
  245. font-size: 10.5px;
  246. letter-spacing: 0.05em;
  247. text-transform: uppercase;
  248. padding: 3px 9px;
  249. border-radius: 100px;
  250. font-weight: 600;
  251. }
  252. .pill.open { background: var(--accent-soft); color: var(--accent); }
  253. .pill.decided { background: var(--green-soft); color: var(--green); }
  254. .boundary {
  255. border: 1px dashed var(--panel-line);
  256. border-radius: 12px;
  257. padding: 20px 22px;
  258. background: var(--bg-alt);
  259. }
  260. .boundary .t {
  261. font-family: var(--mono);
  262. font-size: 11px;
  263. letter-spacing: 0.08em;
  264. text-transform: uppercase;
  265. color: var(--ink-faint);
  266. margin-bottom: 8px;
  267. }
  268. footer {
  269. margin-top: 80px;
  270. padding-top: 24px;
  271. border-top: 1px solid var(--line);
  272. font-family: var(--mono);
  273. font-size: 12px;
  274. color: var(--ink-faint);
  275. display: flex;
  276. justify-content: space-between;
  277. flex-wrap: wrap;
  278. gap: 8px;
  279. }
  280. @media (max-width: 640px) {
  281. nav.toc ol { columns: 1; }
  282. .wrap { padding: 40px 18px 72px; }
  283. }
  284. </style>
  285. </head>
  286. <body>
  287. <div class="wrap">
  288. <div class="eyebrow">Internal discussion draft — no implementation started</div>
  289. <h1>Field-to-Model: a correction&#8209;data pipeline for PalmOilAI</h1>
  290. <p class="subtitle">Every grade a harvester corrects in the field is a labeled training example. Right now it never leaves the phone. This document proposes how it gets to us — quietly, reliably, and without asking the harvester to do anything.</p>
  291. <div class="meta-row">
  292. <span><b>Scope</b> — mobile app → dedicated ingestion server</span>
  293. <span><b>Status</b> — brainstorm, pending decisions marked below</span>
  294. <span><b>Owner</b> — palm_oil_mobile</span>
  295. </div>
  296. <nav class="toc" aria-label="Table of contents">
  297. <p>Contents</p>
  298. <ol>
  299. <li><a href="#context"><span class="n">01</span>Why this exists</a></li>
  300. <li><a href="#principles"><span class="n">02</span>Agreed principles</a></li>
  301. <li><a href="#architecture"><span class="n">03</span>System architecture</a></li>
  302. <li><a href="#flow"><span class="n">04</span>Sync flow, step by step</a></li>
  303. <li><a href="#data"><span class="n">05</span>What actually gets sent</a></li>
  304. <li><a href="#decisions"><span class="n">06</span>Open decisions</a></li>
  305. <li><a href="#risks"><span class="n">07</span>Risks &amp; mitigations</a></li>
  306. <li><a href="#boundary"><span class="n">08</span>Out of scope</a></li>
  307. </ol>
  308. </nav>
  309. <section id="context">
  310. <div class="sec-head"><span class="n">01</span><h2>Why this exists</h2></div>
  311. <p class="lead">The app already lets a harvester correct the AI's grading — change a class, remove a false detection, or draw a box around a bunch the model missed. That correction is never destructive: the AI's original call is kept, and the human's edit sits alongside it.</p>
  312. <p>That pairing — <em>what the model said</em> next to <em>what was actually true</em> — is exactly the labeled data needed to train the next generation of the model. Today it's trapped in a local SQLite database on each harvester's phone and goes nowhere. This document proposes a pipeline to collect it centrally, without adding a single step to the harvester's workflow.</p>
  313. </section>
  314. <section id="principles">
  315. <div class="sec-head"><span class="n">02</span><h2>Agreed principles</h2></div>
  316. <p>Three calls have already been made in discussion; everything downstream in this document follows from them.</p>
  317. <ul class="principles">
  318. <li>
  319. <div class="txt">
  320. <span class="t">A dedicated, standalone ingestion server</span>
  321. <span class="d">Not bolted onto <code>server-desktop</code> or any operational service. If it's slow or down, nothing a harvester relies on in the field is affected — the data simply queues on-device until it's reachable again.</span>
  322. </div>
  323. </li>
  324. <li>
  325. <div class="txt">
  326. <span class="t">Fully silent, background sync</span>
  327. <span class="d">No "sync now" button, no progress bar, no prompt. The harvester's job is to scan and correct; getting that data to us is entirely the app's problem, running only when connectivity allows.</span>
  328. </div>
  329. </li>
  330. <li>
  331. <div class="txt">
  332. <span class="t">The app tracks its own upload state</span>
  333. <span class="d">Every record knows locally whether it's been sent. No handshake with the server is needed to know what's still pending — a local flag is the source of truth, which makes the whole thing resumable after any interruption.</span>
  334. </div>
  335. </li>
  336. </ul>
  337. </section>
  338. <section id="architecture">
  339. <div class="sec-head"><span class="n">03</span><h2>System architecture</h2></div>
  340. <p>The phone is the only place data is created. Everything after that is designed so a slow or offline server never blocks a harvester from scanning.</p>
  341. <div class="panel">
  342. <div class="bar"><span class="dot"></span>figure 1 — data path from bunch to model</div>
  343. <div class="body">
  344. <pre class="mermaid">
  345. flowchart LR
  346. classDef device fill:#00000000,stroke-width:1px;
  347. classDef server fill:#00000000,stroke-width:1px;
  348. classDef future stroke-dasharray: 4 3;
  349. H(["Harvester"]) --> P["Phone: scan + correct"]
  350. P --> L[("Local SQLite\nscans + corrections\n+ upload state")]
  351. L -- "background sync\nWi-Fi / connectivity trigger" --> S["Dedicated Ingestion Server"]
  352. S --> R[("Raw Store\nimages + AI result + correction")]
  353. R --> T["Retraining Pipeline\n(offline, periodic — future work)"]
  354. T --> M["New Model Version"]
  355. M -.-> P
  356. class P,L device
  357. class S,R server
  358. class T,M future
  359. </pre>
  360. </div>
  361. </div>
  362. <p>The dashed link back to the phone is deliberately out of scope for this document — how a new model version reaches devices is a separate rollout question for later.</p>
  363. </section>
  364. <section id="flow">
  365. <div class="sec-head"><span class="n">04</span><h2>Sync flow, step by step</h2></div>
  366. <p>The harvester only ever sees the left-most lane. Everything else happens whenever the phone next has connectivity, with no user-visible state.</p>
  367. <div class="panel">
  368. <div class="bar"><span class="dot"></span>figure 2 — from a correction to an uploaded record</div>
  369. <div class="body">
  370. <pre class="mermaid">
  371. sequenceDiagram
  372. participant H as Harvester
  373. participant App as App
  374. participant DB as Local DB
  375. participant Sync as Background Sync
  376. participant Srv as Ingestion Server
  377. H->>App: Scan bunch, correct a grade
  378. App->>DB: Save record (uploadedAt = none)
  379. Note right of DB: Sits queued.<br/>Nothing for the harvester to do.
  380. loop Whenever the OS wakes it
  381. Sync->>DB: Which records are unsynced?
  382. alt connectivity available
  383. Sync->>Srv: Upload batch (image + AI result + correction)
  384. Srv-->>Sync: Acknowledged
  385. Sync->>DB: Mark uploadedAt = now
  386. else no connectivity
  387. Sync->>Sync: Do nothing, try again next wake
  388. end
  389. end
  390. </pre>
  391. </div>
  392. </div>
  393. <p>One edge case worth naming: a harvester can correct a scan <em>after</em> it already uploaded — reviewing a batch a few days later, say. So "synced" needs to track the correction separately from the original scan, or a late correction silently never makes it off the device.</p>
  394. </section>
  395. <section id="data">
  396. <div class="sec-head"><span class="n">05</span><h2>What actually gets sent</h2></div>
  397. <p>Per scan, the payload pairs the model's original call with whatever the harvester changed it to — that pairing is the entire point of the exercise.</p>
  398. <div class="table-scroll">
  399. <table>
  400. <thead>
  401. <tr><th>Field</th><th>Where it comes from</th><th>Why it matters for training</th></tr>
  402. </thead>
  403. <tbody>
  404. <tr><td class="field">image</td><td>Camera capture</td><td>The actual training input</td></tr>
  405. <tr><td class="field">ai_detections[]</td><td>On-device inference</td><td>The model's original boxes, classes, confidence — what it got right or wrong</td></tr>
  406. <tr><td class="field">human_correction[]</td><td>Harvester review</td><td>The corrected boxes/classes, or a removed false positive — the actual label</td></tr>
  407. <tr><td class="field">model_version</td><td>App build</td><td>Which model produced the AI call, so improvement can be measured generation over generation</td></tr>
  408. <tr><td class="field">captured_at</td><td>App</td><td>Chronological + seasonal context for the dataset</td></tr>
  409. <tr><td class="field">device_id</td><td>App</td><td>Coarse debugging / dedup, not tied to a personal identity</td></tr>
  410. <tr><td class="field">uploaded_at</td><td>Sync engine, local only</td><td>Never sent — this is the idempotency marker that makes sync resumable</td></tr>
  411. </tbody>
  412. </table>
  413. </div>
  414. </section>
  415. <section id="decisions">
  416. <div class="sec-head"><span class="n">06</span><h2>Open decisions</h2></div>
  417. <p>Three things still need a call before this can move from brainstorm to plan.</p>
  418. <div class="table-scroll">
  419. <table>
  420. <thead>
  421. <tr><th>Question</th><th>Leaning</th><th>Trade-off</th><th></th></tr>
  422. </thead>
  423. <tbody>
  424. <tr>
  425. <td class="field">Wi-Fi only, or any connection?</td>
  426. <td>Wi-Fi only to start</td>
  427. <td>Slower data arrival, but no surprise mobile-data cost for a harvester who never asked for this</td>
  428. <td><span class="pill open">Open</span></td>
  429. </tr>
  430. <tr>
  431. <td class="field">Upload every scan, or only corrected ones?</td>
  432. <td>Every reviewed scan</td>
  433. <td>More storage, but "the AI was right, no correction needed" is itself a useful training signal, not noise</td>
  434. <td><span class="pill open">Open</span></td>
  435. </tr>
  436. <tr>
  437. <td class="field">Silent upload, or an opt-in toggle?</td>
  438. <td>Depends on device ownership</td>
  439. <td>Opt-in adds friction and drop-off; silent upload only sits right if these are company-managed devices</td>
  440. <td><span class="pill open">Open</span></td>
  441. </tr>
  442. <tr>
  443. <td class="field">Dedicated, standalone server</td>
  444. <td>Decided</td>
  445. <td>Keeps ingestion downtime from ever touching the app's core detection features</td>
  446. <td><span class="pill decided">Decided</span></td>
  447. </tr>
  448. <tr>
  449. <td class="field">Fully background, no harvester-facing sync UI</td>
  450. <td>Decided</td>
  451. <td>Zero added burden on the harvester's workflow</td>
  452. <td><span class="pill decided">Decided</span></td>
  453. </tr>
  454. </tbody>
  455. </table>
  456. </div>
  457. </section>
  458. <section id="risks">
  459. <div class="sec-head"><span class="n">07</span><h2>Risks &amp; mitigations</h2></div>
  460. <ul class="risks">
  461. <li>
  462. <div class="txt">
  463. <span class="t">Ingestion server is down or slow</span>
  464. <span class="d">No effect on scanning or correcting — records simply stay queued locally and upload on the next successful sync. This is the entire reason it's a separate server.</span>
  465. </div>
  466. </li>
  467. <li>
  468. <div class="txt">
  469. <span class="t">Bandwidth cost on a harvester's personal data plan</span>
  470. <span class="d">Addressed by the Wi-Fi-only decision above, once confirmed.</span>
  471. </div>
  472. </li>
  473. <li>
  474. <div class="txt">
  475. <span class="t">A correction made after the scan already synced gets missed</span>
  476. <span class="d">Track correction-sync state separately from scan-sync state, so an edit made days later still gets picked up on the next background pass.</span>
  477. </div>
  478. </li>
  479. <li>
  480. <div class="txt">
  481. <span class="t">Raw storage grows indefinitely</span>
  482. <span class="d">Not a phone-side concern, but worth flagging for the server design — periodic archival into a proper training dataset, rather than an ever-growing inbox.</span>
  483. </div>
  484. </li>
  485. </ul>
  486. </section>
  487. <section id="boundary">
  488. <div class="sec-head"><span class="n">08</span><h2>Out of scope</h2></div>
  489. <div class="boundary">
  490. <div class="t">Where this document stops</div>
  491. <p style="margin-bottom:0;">The app's responsibility ends at <strong>reliably getting corrected data to the server</strong>. Aggregating uploads into a training dataset, running retraining, evaluating a new model, and distributing it back to devices are separate initiatives — worth planning, but deliberately not decided here.</p>
  492. </div>
  493. </section>
  494. <footer>
  495. <span>Field-to-Model — correction data pipeline</span>
  496. <span>Discussion draft, no code changes made</span>
  497. </footer>
  498. </div>
  499. <script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
  500. <script>
  501. var prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
  502. mermaid.initialize({ startOnLoad: true, theme: prefersDark ? 'dark' : 'default' });
  503. </script>
  504. </body>
  505. </html>