Output:
Chunk ID: 385698
Wall time: 0.0002 seconds
Process exited with code 2
Original token count: 113423
Output:
Total output lines: 192

rg: pyproject.toml: No such file or directory (os error 2)
rg: setup.cfg: No such file or directory (os error 2)
rg: tox.ini: No such file or directory (os error 2)
./tests/test_apple_health_analytics_v2.py:10:import pytest
./tests/test_apple_health_analytics_v2.py:70:@pytest.fixture
./tests/test_apple_health_analytics_v2.py:229:@pytest.mark.parametrize(
./tests/test_apple_health_analytics_v2.py:323:        assert normalized_value == pytest.approx(expected_value)
./tests/test_apple_health_analytics_v2.py:420:    assert quality["coverage_ratio"] == pytest.approx(2 / 3, abs=0.001)
./tests/test_dashboard_v5_capture_sprint5c.py:15:import pytest
./tests/test_dashboard_v5_capture_sprint5c.py:100:        with pytest.raises(urllib.error.HTTPError) as scheme_rejected:
./tests/test_dashboard_v5_capture_sprint5c.py:104:        with pytest.raises(urllib.error.HTTPError) as redirect:
./tests/test_dashboard_v5_capture_sprint5c.py:128:        with pytest.raises(urllib.error.HTTPError) as rejected:
./tests/test_dashboard_v5_capture_sprint5c.py:147:        with pytest.raises(urllib.error.HTTPError) as unavailable:
./tests/test_dashboard_v5_capture_sprint5c.py:174:    with pytest.raises(ValueError, match="shape"):
./tests/test_dashboard_v5_capture_sprint5c.py:179:    with pytest.raises(ValueError, match="single value"):
./tests/test_dashboard_v5_capture_sprint5c.py:184:    with pytest.raises(ValueError, match="return target"):
./tests/test_dashboard_v5_capture_sprint5c.py:203:    with pytest.raises(module.QueueFullError):
./tests/test_dashboard_v5_capture_sprint5c.py:215:    with pytest.raises(ValueError, match="permissions"):
./tests/test_dashboard_v5_capture_sprint5c.py:336:    with pytest.raises(RuntimeError, match="below /tmp"):
./tests/test_multimodal_correlations_v1.py:10:import pytest
./tests/test_multimodal_correlations_v1.py:84:@pytest.fixture
./tests/test_multimodal_correlations_v1.py:352:@pytest.mark.parametrize("duplicate_value", ["unbekannt", "keine (0)"])
./tests/test_multimodal_correlations_v1.py:646:    with pytest.raises(ValueError, match="predictor is not allowlisted"):
./tests/test_multimodal_correlations_v1.py:653:@pytest.mark.parametrize(
./tests/test_multimodal_correlations_v1.py:690:    with pytest.raises(ValueError):
./tests/test_multimodal_correlations_v1.py:701:    with pytest.raises(TypeError):
./tests/test_multimodal_correlations_v1.py:703:    with pytest.raises(SystemExit):
./tests/test_health_dashboard_v5.py:14:import pytest
./tests/test_health_dashboard_v5.py:227:    with pytest.raises(ValueError):
./tests/test_health_dashboard_v5.py:232:    with pytest.raises(ValueError):
./tests/test_health_dashboard_v5.py:239:    with pytest.raises(ValueError):
./tests/test_health_dashboard_v5.py:248:    with pytest.raises(ValueError, match="lab provenance"):
./tests/test_health_dashboard_v5.py:257:    with pytest.raises(ValueError, match="allowlisted lab"):
./tests/test_health_dashboard_v5.py:267:        with pytest.raises(ValueError, match="non-numeric verified lab"):
./tests/test_health_dashboard_v5.py:276:    with pytest.raises(ValueError, match="non-numeric lab reference"):
./tests/test_health_dashboard_v5.py:292:    with pytest.raises(ValueError):
./tests/test_health_dashboard_v5.py:297:    with pytest.raises(ValueError):
./tests/test_health_dashboard_v5.py:302:    with pytest.raises(ValueError):
./tests/test_health_dashboard_v5.py:307:    with pytest.raises(ValueError):
./tests/test_health_dashboard_v5.py:326:def test_generator_cli_requires_database_before_opening_any_database(monkeypatch: pytest.MonkeyPatch) -> None:
./tests/test_health_dashboard_v5.py:336:    with pytest.raises(SystemExit) as error:
./tests/test_health_dashboard_v5.py:342:def test_worker_passes_explicit_database_to_v5_regeneration(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
./tests/test_health_dashboard_v5.py:431:        with pytest_http_error(404):
./tests/test_health_dashboard_v5.py:446:    with pytest.raises(ValueError, match="too many items"):
./tests/test_health_dashboard_v5.py:469:    with pytest.raises(ValueError, match="serialized size budget"):
./tests/test_health_dashboard_v5.py:473:class pytest_http_error:
./tests/test_dashboard_v5_sprint6d_day_calendar.py:11:import pytest
./tests/test_dashboard_v5_sprint6d_day_calendar.py:174:    with pytest.raises(APIError) as invalid:
./tests/test_dashboard_v5_sprint6d_day_calendar.py:177:    with pytest.raises(APIError) as query:
./tests/test_dashboard_v5_sprint6d_day_calendar.py:180:    with pytest.raises(APIError) as future:
./tests/test_dashboard_v5_sprint6d_day_calendar.py:202:    with pytest.raises(APIError) as too_large:
./tests/test_dashboard_v5_sprint6d_day_calendar.py:215:        with pytest.raises(APIError):
./tests/test_dashboard_v5_sprint6d_day_calendar.py:217:    with pytest.raises(APIError) as future:
./tests/test_dashboard_v5_sprint6b_api.py:17:import pytest
./tests/test_dashboard_v5_sprint6b_api.py:253:    with pytest.raises(APIError) as panel:
./tests/test_dashboard_v5_sprint6b_api.py:256:    with pytest.raises(APIError) as too_wide:
./tests/test_dashboard_v5_sprint6b_api.py:359:        with pytest.raises(APIError) as error:
./tests/test_dashboard_v5_sprint6b_api.py:397:    with pytest.raises(APIError, match="row_limit_exceeded"):
./tests/test_dashboard_v5_sprint6b_api.py:403:    with pytest.raises(APIError, match="row_limit_exceeded"):
./tests/test_dashboard_v5_sprint6b_api.py:418:    with pytest.raises(APIError, match="source_inventory_limit_exceeded"):
./tests/test_dashboard_v5_sprint6b_api.py:468:    with pytest.raises(APIError, match="range_too_large"):
./tests/test_dashboard_v5_sprint6b_api.py:470:    with pytest.raises(APIError, match="future_date_not_allowed"):
./tests/test_dashboard_v5_sprint6b_api.py:472:    with pytest.raises(APIError, match="future_range_not_allowed"):
./tests/test_dashboard_v5_sprint6b_api.py:506:    with pytest.raises(APIError, match="data_unavailable"):
./tests/test_dashboard_v5_sprint6b_api.py:591:        with pytest.raises(urllib.error.HTTPError) as rejected_host:
./tests/test_dashboard_v5_sprint6b_api.py:595:        with pytest.raises(urllib.error.HTTPError) as missing:
./tests/test_dashboard_v5_sprint6b_api.py:603:        with pytest.raises(urllib.error.HTTPError) as denied:
./tests/test_dashboard_v5_sprint6b_api.py:623:        with pytest.raises(urllib.error.HTTPError) as cross_origin:
./tests/test_dashboard_v5_sprint6b_api.py:634:        with pytest.raises(urllib.error.HTTPError) as cross_site_error:
./tests/test_dashboard_v5_sprint6b_api.py:647:        with pytest.raises(urllib.error.HTTPError) as no_legacy_cross_link:
./tests/test_dashboard_v5_sprint6b_api.py:654:        with pytest.raises(urllib.error.HTTPError) as query_secret:
./tests/test_dashboard_v5_sprint6b_api.py:661:        with pytest.raises(urllib.error.HTTPError) as read_only:
./tests/test_dashboard_v5_sprint6b_api.py:669:        with pytest.raises(urllib.error.HTTPError) as unknown_method:
./tests/test_dashboard_v5_sprint6b_api.py:703:            with pytest.raises(urllib.error.HTTPError) as oversized:
./tests/test_dashboard_v5_sprint6b_api.py:713:            with pytest.raises(urllib.error.HTTPError) as internal:
./tests/test_dashboard_v5_sprint6b_api.py:723:            with pytest.raises(urllib.error.HTTPError) as serialization:
./tests/test_dashboard_v5_sprint6b_api.py:732:        with pytest.raises(urllib.error.HTTPError) as insecure:
./tests/test_dashboard_v5_sprint6b_api.py:739:        with pytest.raises(urllib.error.HTTPError) as symlinked:
./tests/test_dashboard_v5_sprint6b_api.py:771:        with pytest.raises(urllib.error.HTTPError) as disabled:
./tests/test_dashboard_v5_sprint6a.py:12:import pytest
./tests/test_dashboard_v5_sprint6a.py:166:    with pytest.raises(ValueError, match="must stay below /tmp"):
./tests/test_dashboard_v5_sprint6a.py:176:    with pytest.raises(ValueError, match="synthetic fixture marker"):
./tests/test_dashboard_v5_sprint6a.py:189:    with pytest.raises(sqlite3.OperationalError, match="readonly"):
./tests/test_dashboard_v5_sprint6a.py:195:    with pytest.raises(ValueError, match="must not be a symlink"):
./tests/test_dashboard_v5_sprint6a.py:202:    with pytest.raises(ValueError, match="must stay below /tmp"):
./tests/test_dashboard_v5_sprint6a.py:208:    with pytest.raises(FileExistsError, match="must not already exist"):
./tests/test_dashboard_v5_sprint6a.py:216:    with pytest.raises(FileExistsError, match="must not already exist"):
./tests/test_dashboard_v5_sprint6a.py:224:    with pytest.raises(ValueError, match="symlink"):
./tests/test_dashboard_v5_sprint6a.py:232:    with pytest.raises(ValueError, match="mode 0700"):
./tests/test_dashboard_v5_sprint6a.py:249:    with pytest.raises(ValueError, match="changed after validation"):
./tests/test_health_system_migrate_sprint2.py:5:import pytest
./tests/test_health_system_migrate_sprint2.py:10:@pytest.fixture
./tests/test_dashboard_v5_sprint6e_record.py:8:import pytest
./tests/test_dashboard_v5_sprint6e_record.py:94:    with pytest.raises(APIError):
./tests/test_dashboard_v5_sprint6e_record.py:97:    with pytest.raises(APIError):
./tests/test_dashboard_v5_sprint6e_record.py:130:    with pytest.raises(APIError) as detail_error:
./tests/test_dashboard_v5_sprint6e_record.py:134:        with pytest.raises(APIError) as error:
./tests/test_dashboard_v5_sprint6e_record.py:215:        with pytest.raises(APIError) as error:
./tests/test_dashboard_v5_sprint6e_record.py:286:    with pytest.raises(APIError) as error:
./tests/test_dashboard_v5_sprint6e4a_recovery.py:8:import pytest
./tests/test_dashboard_v5_sprint6e4a_recovery.py:62:        with pytest.raises(APIError):
./tests/test_dashboard_v5_sprint6e4a_recovery.py:71:def test_render_profile_assets_and_single_explorer_contract(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
./tests/test_dashboard_v5_sprint6e4a_recovery.py:109:def test_server_allowlists_only_local_new_assets(monkeypatch: pytest.MonkeyPatch, tmp_path: Path):
./tests/test_dashboard_v5_sprint6e4b.py:3:# ruff: noqa: E402
./tests/test_dashboard_v5_sprint6e4b.py:11:import pytest
./tests/test_dashboard_v5_sprint6e4b.py:61:    with pytest.raises(APIError) as pending:
./tests/test_sprint0_safety.py:12:import pytest
./tests/test_sprint0_safety.py:44:    with pytest.raises(RuntimeError, match="Missing gog keyring secret"):
./tests/test_sprint0_safety.py:73:        with pytest.raises(RuntimeError, match="synthetic parse failure"):
./tests/test_sprint0_safety.py:96:        with pytest.raises(OSError, match="synthetic archive failure"):
./tests/test_sprint0_safety.py:129:    with pytest.raises(sqlite3.OperationalError, match="synthetic final commit failure"):
./tests/test_sprint0_safety.py:152:        with pytest.raises(sqlite3.IntegrityError):
./tests/test_sprint0_safety.py:240:        with pytest.raises(RuntimeError, match="empty"):
./tests/test_sprint0_safety.py:244:                with pytest.raises(RuntimeError, match="empty"):
./tests/test_sprint0_safety.py:266:        with pytest.raises(RuntimeError, match="Unsafe permissions"):
./tests/test_health_symptom_quick_add.py:8:import pytest
./tests/test_health_symptom_quick_add.py:14:@pytest.fixture
./tests/test_health_symptom_quick_add.py:95:    with pytest.raises(RuntimeError, match="Legacy-Zeilen"):
./tests/test_health_symptom_quick_add.py:134:    with pytest.raises(ValueError, match="abweichende Datenbank"):
./tests/test_health_symptom_quick_add.py:150:    with pytest.raises(SystemExit) as error:
./tests/test_health_symptom_quick_add.py:156:@pytest.mark.parametrize(
./tests/test_health_symptom_quick_add.py:180:    with pytest.raises(ValueError):
./tests/test_dashboard_v5_explorer_sprint5b.py:8:import pytest
./tests/test_dashboard_v5_explorer_sprint5b.py:50:    with pytest.raises(ValueError, match="Explorer registry differs"):
./tests/test_dashboard_v5_explorer_sprint5b.py:55:    with pytest.raises(ValueError, match="metric registry differs"):
./tests/test_dashboard_v5_sprint6b1_integration.py:15:import pytest
./tests/test_dashboard_v5_sprint6b1_integration.py:64:        with pytest.raises(urllib.error.HTTPError) as no_session:
./tests/test_dashboard_v5_sprint6b1_integration.py:74:        with pytest.raises(urllib.error.HTTPError) as missing:
./tests/test_dashboard_v5_sprint6b1_integration.py:105:        with pytest.raises(urllib.error.HTTPError) as blocked:
./tests/test_dashboard_v5_sprint6b_catalog.py:171:def test_static_snapshot_remains_the_small_print_fallback_not_the_large_api_catalog():
./tests/test_dashboard_v5_sprint6g_nutrition.py:9:import pytest
./tests/test_dashboard_v5_sprint6g_nutrition.py:33:@pytest.fixture()
./tests/test_dashboard_v5_sprint6g_nutrition.py:59:    fixture_db: Path, monkeypatch: pytest.MonkeyPatch
./tests/test_dashboard_v5_sprint6g_nutrition.py:90:    fixture_db: Path, monkeypatch: pytest.MonkeyPatch
./tests/test_dashboard_v5_sprint6g_nutrition.py:181:def test_composite_and_ignore_do_not_alter_mappings(fixture_db: Path, monkeypatch: pytest.MonkeyPatch) -> None:
./tests/test_dashboard_v5_sprint6g_nutrition.py:200:    with pytest.raises(ValueError):
./tests/test_dashboard_v5_sprint6g_nutrition.py:214:def test_mapping_submission_requires_one_time_csrf_and_exact_shape(monkeypatch: pytest.MonkeyPatch) -> None:
./tests/test_dashboard_v5_sprint6g_nutrition.py:240:    with pytest.raises(ValueError):
./tests/test_health_dashboard_v4.py:16:import pytest
./tests/test_health_dashboard_v4.py:35:@pytest.fixture
./tests/test_health_dashboard_v4.py:269:        with pytest.raises(urllib.error.HTTPError) as rejected_host:
./tests/test_health_dashboard_v4.py:278:            with pytest.raises(urllib.error.HTTPError) as rejected_method:
./tests/test_health_dashboard_v4.py:367:        with pytest.raises(urllib.error.HTTPError) as redirect:
./tests/test_health_dashboard_v4.py:385:        with pytest.raises(urllib.error.HTTPError) as replay:
./tests/test_health_dashboard_v4.py:410:    with pytest.raises(ValueError):
./tests/test_health_dashboard_v4.py:414:    with pytest.raises(ValueError):
./tests/test_health_dashboard_v4.py:418:    with pytest.raises(ValueError):
./tests/test_health_dashboard_v4.py:484:    with pytest.raises(ValueError, match="duplicate JSON key"):
./tests/test_health_dashboard_v4.py:497:    with pytest.raises(ValueError, match="duplicate JSON key"):
./tests/test_sprint4b_correlation_contract.py:7:import pytest
./tests/test_sprint4b_correlation_contract.py:49:        with pytest.raises(ValueError):
./tests/test_sprint4b_correlation_contract.py:103:    with pytest.raises(ValueError):
./deploy/systemd/health-dashboard.service:2:Description=Health Dashboard static server
./scripts/cron/weekly_health_drive_backup.py:39:    ".git", "__pycache__", ".pytest_cache", ".mypy_cache",
./scripts/health/health_dashboard_v4.py:894:@media (max-width: 700px){{body{{margin:8px;padding-bottom:76px;font-size:14px}}h1{{font-size:1.3rem}}h2{{font-size:1.1rem}}.card,.chart-card,.day-card{{border-radius:12px;margin-bottom:10px}}.grid{{grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}}.num{{font-size:22px}}.day-summary{{grid-template-columns:18px 1fr auto;grid-template-areas:'arrow date kcal' 'arrow pill load' 'arrow items items';gap:4px 8px;padding:12px}}.day-summary:before{{grid-area:arrow}}.day-date{{grid-area:date}}.day-summary .pill{{grid-area:pill;justify-self:start}}.day-kcal{{grid-area:kcal}}.day-load{{grid-area:load}}.day-items{{grid-area:items}}.day-detail{{padding:10px}}.meal-card{{padding:10px}}.food-row{{grid-template-columns:22px minmax(0,1fr) 54px;grid-template-areas:'h name kcal' '. name amount' '. macro macro';gap:2px 6px}}.food-amount,.food-kcal,.food-macro{{text-align:right}}.chart-card{{overflow:hidden}}table{{font-size:12px;max-width:100%}}td,th{{padding:5px}}canvas{{max-height:260px}}.button{{width:100%;text-align:center}}.top-actions{{position:static}}.bottom-nav{{display:flex}}}}
./THIRD_PARTY_NOTICES.md:12:Copyright (c) 2021 Adam Shaw.
./docs/sprint5b-dashboard-v5-explorer.md:68:- 130 Python- und 16 Playwright-Tests bestanden; Ruff, Compile, JavaScript-Syntax und Diff-Check grün,
./docs/sprint6e4b-record-calendar-day-ui.md:146:- Ruff für alle geänderten Python-Dateien, Python-/Node-/Bash-Syntax und `git diff --check`: bestanden;
./docs/sprint5c-dashboard-v5-mobile-capture.md:80:- Ruff, Python-Kompilierung, JavaScript-Syntax, systemd-Verifikation und `git diff --check`: **bestanden**,
./docs/sprint6e4a-preview-recovery.md:118:- Ruff, Python-/Node-/Bash-Syntax und `git diff --check`: grün.
./docs/sprint6b-metric-catalog-read-api.md:278:- Ruff, `py_compile`, `git diff --check`, JSON-Beispielvalidierung und deterministische Ausführung aller sechs Endpunkte bestanden.
./docs/sprint0-safety.md:56:python3 -m pytest tests/test_sprint0_safety.py -q
./docs/sprint6e1-health-record-hardening.md:46:- Ruff, Python-/Node-/Bash-Syntax und `git diff --check`: bestanden.
./docs/sprint5-dashboard-v5-desktop-first.md:80:- 126 Python- und 13 Playwright-Tests bestanden; Ruff, Python-Kompilierung, JavaScript-Syntax und Diff-Check grün,
./docs/architecture.md:75:- HTTP-Server: `~/projects/Jarvis/scripts/health-dashboard-static-server.py`
./docs/sprint6a-dashboard-v5-foundation-echarts.md:78:- gezieltes Ruff für alle geänderten Python-Dateien: bestanden,
./docs/sprint6a-dashboard-v5-foundation-echarts.md:90:Das globale `ruff check .` bleibt wegen 98 bereits vorhandener Legacy-Befunde außerhalb des Sprint-6A-Diffs kein aussagekräftiger Release-Gate. Der vollständige Python-Testlauf, Compile und die gezielte Prüfung aller geänderten Python-Dateien sind grün.
./scripts/health/assets/health-assets/FULLCALENDAR-6.1.21-LICENSE.md:3:Copyright (c) 2021 Adam Shaw
./scripts/health/assets/health-assets/FULLCALENDAR-6.1.21-LICENSE.md:13:The above copyright notice and this permission notice shall be
./scripts/health/assets/health-assets/FULLCALENDAR-6.1.21-LICENSE.md:19:NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
./scripts/health/assets/health-assets/dashboard-v5.css:433:@media (max-width: 560px) { .record-kpi-grid,.record-overview-groups,.day-kpi-grid { grid-template-columns: 1fr; }.record-filter-compact { grid-template-columns: 1fr; }.record-tabs { top: 60px; margin-inline: -8px; overflow: visible; flex-wrap: wrap; border-radius: 0; }.record-tabs button { flex: 1 1 30%; }.document-entry { grid-template-columns: 1fr; }.document-actions { grid-column: 1; }.record-viewer-tools { position: static; grid-template-columns: 1fr 1fr; }.record-viewer-tools input,.record-viewer-tools span { grid-column: 1/-1; }.calendar-toolbar { justify-content: center; }.calendar-toolbar strong { order: -1; width: 100%; }.calendar-month-picker { width: 100%; justify-content: center; }.calendar-grid { padding: 3px; }.fc { font-size: .82rem; }.fc .fc-daygrid-day { min-height: 72px; }.fc .fc-daygrid-event { width: auto; max-width: 100%; min-height: 18px; overflow: hidden; color: #111827; border-radius: 4px; padding: 1px 3px; white-space: nowrap; text-overflow: ellipsis; }.fc .fc-daygrid-event::before { color: #235b68; font-size: .8rem; }.day-metric-card { grid-template-columns: 1fr; }.day-mini-chart { height: 130px; }.day-toolbar > * { flex: 1 1 140px; } }
./scripts/health/assets/health-assets/ECHARTS-6.1.0-LICENSE-D3.txt:1:Copyright 2010-2016 Mike Bostock
./scripts/health/assets/health-assets/ECHARTS-6.1.0-LICENSE-D3.txt:7:* Redistributions of source code must retain the above copyright notice, this
./scripts/health/assets/health-assets/ECHARTS-6.1.0-LICENSE-D3.txt:10:* Redistributions in binary form must reproduce the above copyright notice,
./scripts/health/assets/health-assets/ECHARTS-6.1.0-LICENSE-D3.txt:18:THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
./scripts/health/assets/health-assets/ECHARTS-6.1.0-LICENSE-D3.txt:21:DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
./scripts/health/assets/health-assets/fullcalendar-6.1.21.min.js:1:(()=>{var jt,A,G…103423 tokens truncated…borderJoinStyle,lineWidth:(h.width+h.height)/4,strokeStyle:l.borderColor,pointStyle:s||l.pointStyle,rotation:l.rotation,textAlign:n||l.textAlign,borderRadius:a&&(r||l.borderRadius),datasetIndex:t.index}}),this)}},title:{color:t=>t.chart.options.color,display:!1,position:"center",text:""}},descriptors:{_scriptable:t=>!t.startsWith("on"),labels:{_scriptable:t=>!["generateLabels","filter","sort"].includes(t)}}};class wa extends $s{constructor(t){super(),this.chart=t.chart,this.options=t.options,this.ctx=t.ctx,this._padding=void 0,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.width=void 0,this.height=void 0,this.position=void 0,this.weight=void 0,this.fullSize=void 0}update(t,e){const i=this.options;if(this.left=0,this.top=0,!i.display)return void(this.width=this.height=this.right=this.bottom=0);this.width=this.right=t,this.height=this.bottom=e;const s=n(i.text)?i.text.length:1;this._padding=ki(i.padding);const o=s*Si(i.font).lineHeight+this._padding.height;this.isHorizontal()?this.height=o:this.width=o}isHorizontal(){const t=this.options.position;return"top"===t||"bottom"===t}_drawArgs(t){const{top:e,left:i,bottom:s,right:n,options:o}=this,a=o.align;let r,l,h,c=0;return this.isHorizontal()?(l=ft(a,i,n),h=e+t,r=n-i):("left"===o.position?(l=i+t,h=ft(a,s,e),c=-.5*C):(l=n-t,h=ft(a,e,s),c=.5*C),r=s-e),{titleX:l,titleY:h,maxWidth:r,rotation:c}}draw(){const t=this.ctx,e=this.options;if(!e.display)return;const i=Si(e.font),s=i.lineHeight/2+this._padding.top,{titleX:n,titleY:o,maxWidth:a,rotation:r}=this._drawArgs(s);Ne(t,e.text,0,0,i,{color:e.color,maxWidth:a,rotation:r,textAlign:ut(e.align),textBaseline:"middle",translation:[n,o]})}}var ka={id:"title",_element:wa,start(t,e,i){!function(t,e){const i=new wa({ctx:t.ctx,options:e,chart:t});ls.configure(t,i,e),ls.addBox(t,i),t.titleBlock=i}(t,i)},stop(t){const e=t.titleBlock;ls.removeBox(t,e),delete t.titleBlock},beforeUpdate(t,e,i){const s=t.titleBlock;ls.configure(t,s,i),s.options=i},defaults:{align:"center",display:!1,font:{weight:"bold"},fullSize:!0,padding:10,position:"top",text:"",weight:2e3},defaultRoutes:{color:"color"},descriptors:{_scriptable:!0,_indexable:!1}};const Sa=new WeakMap;var Pa={id:"subtitle",start(t,e,i){const s=new wa({ctx:t.ctx,options:i,chart:t});ls.configure(t,s,i),ls.addBox(t,s),Sa.set(t,s)},stop(t){ls.removeBox(t,Sa.get(t)),Sa.delete(t)},beforeUpdate(t,e,i){const s=Sa.get(t);ls.configure(t,s,i),s.options=i},defaults:{align:"center",display:!1,font:{weight:"normal"},fullSize:!0,padding:0,position:"top",text:"",weight:1500},defaultRoutes:{color:"color"},descriptors:{_scriptable:!0,_indexable:!1}};const Da={average(t){if(!t.length)return!1;let e,i,s=new Set,n=0,o=0;for(e=0,i=t.length;e<i;++e){const i=t[e].element;if(i&&i.hasValue()){const t=i.tooltipPosition();s.add(t.x),n+=t.y,++o}}if(0===o||0===s.size)return!1;return{x:[...s].reduce(((t,e)=>t+e))/s.size,y:n/o}},nearest(t,e){if(!t.length)return!1;let i,s,n,o=e.x,a=e.y,r=Number.POSITIVE_INFINITY;for(i=0,s=t.length;i<s;++i){const s=t[i].element;if(s&&s.hasValue()){const t=q(e,s.getCenterPoint());t<r&&(r=t,n=s)}}if(n){const t=n.tooltipPosition();o=t.x,a=t.y}return{x:o,y:a}}};function Ca(t,e){return e&&(n(e)?Array.prototype.push.apply(t,e):t.push(e)),t}function Oa(t){return("string"==typeof t||t instanceof String)&&t.indexOf("\n")>-1?t.split("\n"):t}function Aa(t,e){const{element:i,datasetIndex:s,index:n}=e,o=t.getDatasetMeta(s).controller,{label:a,value:r}=o.getLabelAndValue(n);return{chart:t,label:a,parsed:o.getParsed(n),raw:t.data.datasets[s].data[n],formattedValue:r,dataset:o.getDataset(),dataIndex:n,datasetIndex:s,element:i}}function Ta(t,e){const i=t.chart.ctx,{body:s,footer:n,title:o}=t,{boxWidth:a,boxHeight:r}=e,l=Si(e.bodyFont),h=Si(e.titleFont),c=Si(e.footerFont),d=o.length,f=n.length,g=s.length,p=ki(e.padding);let m=p.height,x=0,b=s.reduce(((t,e)=>t+e.before.length+e.lines.length+e.after.length),0);if(b+=t.beforeBody.length+t.afterBody.length,d&&(m+=d*h.lineHeight+(d-1)*e.titleSpacing+e.titleMarginBottom),b){m+=g*(e.displayColors?Math.max(r,l.lineHeight):l.lineHeight)+(b-g)*l.lineHeight+(b-1)*e.bodySpacing}f&&(m+=e.footerMarginTop+f*c.lineHeight+(f-1)*e.footerSpacing);let _=0;const y=function(t){x=Math.max(x,i.measureText(t).width+_)};return i.save(),i.font=h.string,u(t.title,y),i.font=l.string,u(t.beforeBody.concat(t.afterBody),y),_=e.displayColors?a+2+e.boxPadding:0,u(s,(t=>{u(t.before,y),u(t.lines,y),u(t.after,y)})),_=0,i.font=c.string,u(t.footer,y),i.restore(),x+=p.width,{width:x,height:m}}function La(t,e,i,s){const{x:n,width:o}=i,{width:a,chartArea:{left:r,right:l}}=t;let h="center";return"center"===s?h=n<=(r+l)/2?"left":"right":n<=o/2?h="left":n>=a-o/2&&(h="right"),function(t,e,i,s){const{x:n,width:o}=s,a=i.caretSize+i.caretPadding;return"left"===t&&n+o+a>e.width||"right"===t&&n-o-a<0||void 0}(h,t,e,i)&&(h="center"),h}function Ea(t,e,i){const s=i.yAlign||e.yAlign||function(t,e){const{y:i,height:s}=e;return i<s/2?"top":i>t.height-s/2?"bottom":"center"}(t,i);return{xAlign:i.xAlign||e.xAlign||La(t,e,i,s),yAlign:s}}function Ra(t,e,i,s){const{caretSize:n,caretPadding:o,cornerRadius:a}=t,{xAlign:r,yAlign:l}=i,h=n+o,{topLeft:c,topRight:d,bottomLeft:u,bottomRight:f}=wi(a);let g=function(t,e){let{x:i,width:s}=t;return"right"===e?i-=s:"center"===e&&(i-=s/2),i}(e,r);const p=function(t,e,i){let{y:s,height:n}=t;return"top"===e?s+=i:s-="bottom"===e?n+i:n/2,s}(e,l,h);return"center"===l?"left"===r?g+=h:"right"===r&&(g-=h):"left"===r?g-=Math.max(c,u)+n:"right"===r&&(g+=Math.max(d,f)+n),{x:Z(g,0,s.width-e.width),y:Z(p,0,s.height-e.height)}}function Ia(t,e,i){const s=ki(i.padding);return"center"===e?t.x+t.width/2:"right"===e?t.x+t.width-s.right:t.x+s.left}function za(t){return Ca([],Oa(t))}function Fa(t,e){const i=e&&e.dataset&&e.dataset.tooltip&&e.dataset.tooltip.callbacks;return i?t.override(i):t}const Va={beforeTitle:e,title(t){if(t.length>0){const e=t[0],i=e.chart.data.labels,s=i?i.length:0;if(this&&this.options&&"dataset"===this.options.mode)return e.dataset.label||"";if(e.label)return e.label;if(s>0&&e.dataIndex<s)return i[e.dataIndex]}return""},afterTitle:e,beforeBody:e,beforeLabel:e,label(t){if(this&&this.options&&"dataset"===this.options.mode)return t.label+": "+t.formattedValue||t.formattedValue;let e=t.dataset.label||"";e&&(e+=": ");const i=t.formattedValue;return s(i)||(e+=i),e},labelColor(t){const e=t.chart.getDatasetMeta(t.datasetIndex).controller.getStyle(t.dataIndex);return{borderColor:e.borderColor,backgroundColor:e.backgroundColor,borderWidth:e.borderWidth,borderDash:e.borderDash,borderDashOffset:e.borderDashOffset,borderRadius:0}},labelTextColor(){return this.options.bodyColor},labelPointStyle(t){const e=t.chart.getDatasetMeta(t.datasetIndex).controller.getStyle(t.dataIndex);return{pointStyle:e.pointStyle,rotation:e.rotation}},afterLabel:e,afterBody:e,beforeFooter:e,footer:e,afterFooter:e};function Ba(t,e,i,s){const n=t[e].call(i,s);return void 0===n?Va[e].call(i,s):n}class Wa extends $s{static positioners=Da;constructor(t){super(),this.opacity=0,this._active=[],this._eventPosition=void 0,this._size=void 0,this._cachedAnimations=void 0,this._tooltipItems=[],this.$animations=void 0,this.$context=void 0,this.chart=t.chart,this.options=t.options,this.dataPoints=void 0,this.title=void 0,this.beforeBody=void 0,this.body=void 0,this.afterBody=void 0,this.footer=void 0,this.xAlign=void 0,this.yAlign=void 0,this.x=void 0,this.y=void 0,this.height=void 0,this.width=void 0,this.caretX=void 0,this.caretY=void 0,this.labelColors=void 0,this.labelPointStyles=void 0,this.labelTextColors=void 0}initialize(t){this.options=t,this._cachedAnimations=void 0,this.$context=void 0}_resolveAnimations(){const t=this._cachedAnimations;if(t)return t;const e=this.chart,i=this.options.setContext(this.getContext()),s=i.enabled&&e.options.animation&&i.animations,n=new Ts(this.chart,s);return s._cacheable&&(this._cachedAnimations=Object.freeze(n)),n}getContext(){return this.$context||(this.$context=(t=this.chart.getContext(),e=this,i=this._tooltipItems,Ci(t,{tooltip:e,tooltipItems:i,type:"tooltip"})));var t,e,i}getTitle(t,e){const{callbacks:i}=e,s=Ba(i,"beforeTitle",this,t),n=Ba(i,"title",this,t),o=Ba(i,"afterTitle",this,t);let a=[];return a=Ca(a,Oa(s)),a=Ca(a,Oa(n)),a=Ca(a,Oa(o)),a}getBeforeBody(t,e){return za(Ba(e.callbacks,"beforeBody",this,t))}getBody(t,e){const{callbacks:i}=e,s=[];return u(t,(t=>{const e={before:[],lines:[],after:[]},n=Fa(i,t);Ca(e.before,Oa(Ba(n,"beforeLabel",this,t))),Ca(e.lines,Ba(n,"label",this,t)),Ca(e.after,Oa(Ba(n,"afterLabel",this,t))),s.push(e)})),s}getAfterBody(t,e){return za(Ba(e.callbacks,"afterBody",this,t))}getFooter(t,e){const{callbacks:i}=e,s=Ba(i,"beforeFooter",this,t),n=Ba(i,"footer",this,t),o=Ba(i,"afterFooter",this,t);let a=[];return a=Ca(a,Oa(s)),a=Ca(a,Oa(n)),a=Ca(a,Oa(o)),a}_createItems(t){const e=this._active,i=this.chart.data,s=[],n=[],o=[];let a,r,l=[];for(a=0,r=e.length;a<r;++a)l.push(Aa(this.chart,e[a]));return t.filter&&(l=l.filter(((e,s,n)=>t.filter(e,s,n,i)))),t.itemSort&&(l=l.sort(((e,s)=>t.itemSort(e,s,i)))),u(l,(e=>{const i=Fa(t.callbacks,e);s.push(Ba(i,"labelColor",this,e)),n.push(Ba(i,"labelPointStyle",this,e)),o.push(Ba(i,"labelTextColor",this,e))})),this.labelColors=s,this.labelPointStyles=n,this.labelTextColors=o,this.dataPoints=l,l}update(t,e){const i=this.options.setContext(this.getContext()),s=this._active;let n,o=[];if(s.length){const t=Da[i.position].call(this,s,this._eventPosition);o=this._createItems(i),this.title=this.getTitle(o,i),this.beforeBody=this.getBeforeBody(o,i),this.body=this.getBody(o,i),this.afterBody=this.getAfterBody(o,i),this.footer=this.getFooter(o,i);const e=this._size=Ta(this,i),a=Object.assign({},t,e),r=Ea(this.chart,i,a),l=Ra(i,a,r,this.chart);this.xAlign=r.xAlign,this.yAlign=r.yAlign,n={opacity:1,x:l.x,y:l.y,width:e.width,height:e.height,caretX:t.x,caretY:t.y}}else 0!==this.opacity&&(n={opacity:0});this._tooltipItems=o,this.$context=void 0,n&&this._resolveAnimations().update(this,n),t&&i.external&&i.external.call(this,{chart:this.chart,tooltip:this,replay:e})}drawCaret(t,e,i,s){const n=this.getCaretPosition(t,i,s);e.lineTo(n.x1,n.y1),e.lineTo(n.x2,n.y2),e.lineTo(n.x3,n.y3)}getCaretPosition(t,e,i){const{xAlign:s,yAlign:n}=this,{caretSize:o,cornerRadius:a}=i,{topLeft:r,topRight:l,bottomLeft:h,bottomRight:c}=wi(a),{x:d,y:u}=t,{width:f,height:g}=e;let p,m,x,b,_,y;return"center"===n?(_=u+g/2,"left"===s?(p=d,m=p-o,b=_+o,y=_-o):(p=d+f,m=p+o,b=_-o,y=_+o),x=p):(m="left"===s?d+Math.max(r,h)+o:"right"===s?d+f-Math.max(l,c)-o:this.caretX,"top"===n?(b=u,_=b-o,p=m-o,x=m+o):(b=u+g,_=b+o,p=m+o,x=m-o),y=b),{x1:p,x2:m,x3:x,y1:b,y2:_,y3:y}}drawTitle(t,e,i){const s=this.title,n=s.length;let o,a,r;if(n){const l=Oi(i.rtl,this.x,this.width);for(t.x=Ia(this,i.titleAlign,i),e.textAlign=l.textAlign(i.titleAlign),e.textBaseline="middle",o=Si(i.titleFont),a=i.titleSpacing,e.fillStyle=i.titleColor,e.font=o.string,r=0;r<n;++r)e.fillText(s[r],l.x(t.x),t.y+o.lineHeight/2),t.y+=o.lineHeight+a,r+1===n&&(t.y+=i.titleMarginBottom-a)}}_drawColorBox(t,e,i,s,n){const a=this.labelColors[i],r=this.labelPointStyles[i],{boxHeight:l,boxWidth:h}=n,c=Si(n.bodyFont),d=Ia(this,"left",n),u=s.x(d),f=l<c.lineHeight?(c.lineHeight-l)/2:0,g=e.y+f;if(n.usePointStyle){const e={radius:Math.min(h,l)/2,pointStyle:r.pointStyle,rotation:r.rotation,borderWidth:1},i=s.leftForLtr(u,h)+h/2,o=g+l/2;t.strokeStyle=n.multiKeyBackground,t.fillStyle=n.multiKeyBackground,Le(t,e,i,o),t.strokeStyle=a.borderColor,t.fillStyle=a.backgroundColor,Le(t,e,i,o)}else{t.lineWidth=o(a.borderWidth)?Math.max(...Object.values(a.borderWidth)):a.borderWidth||1,t.strokeStyle=a.borderColor,t.setLineDash(a.borderDash||[]),t.lineDashOffset=a.borderDashOffset||0;const e=s.leftForLtr(u,h),i=s.leftForLtr(s.xPlus(u,1),h-2),r=wi(a.borderRadius);Object.values(r).some((t=>0!==t))?(t.beginPath(),t.fillStyle=n.multiKeyBackground,He(t,{x:e,y:g,w:h,h:l,radius:r}),t.fill(),t.stroke(),t.fillStyle=a.backgroundColor,t.beginPath(),He(t,{x:i,y:g+1,w:h-2,h:l-2,radius:r}),t.fill()):(t.fillStyle=n.multiKeyBackground,t.fillRect(e,g,h,l),t.strokeRect(e,g,h,l),t.fillStyle=a.backgroundColor,t.fillRect(i,g+1,h-2,l-2))}t.fillStyle=this.labelTextColors[i]}drawBody(t,e,i){const{body:s}=this,{bodySpacing:n,bodyAlign:o,displayColors:a,boxHeight:r,boxWidth:l,boxPadding:h}=i,c=Si(i.bodyFont);let d=c.lineHeight,f=0;const g=Oi(i.rtl,this.x,this.width),p=function(i){e.fillText(i,g.x(t.x+f),t.y+d/2),t.y+=d+n},m=g.textAlign(o);let x,b,_,y,v,M,w;for(e.textAlign=o,e.textBaseline="middle",e.font=c.string,t.x=Ia(this,m,i),e.fillStyle=i.bodyColor,u(this.beforeBody,p),f=a&&"right"!==m?"center"===o?l/2+h:l+2+h:0,y=0,M=s.length;y<M;++y){for(x=s[y],b=this.labelTextColors[y],e.fillStyle=b,u(x.before,p),_=x.lines,a&&_.length&&(this._drawColorBox(e,t,y,g,i),d=Math.max(c.lineHeight,r)),v=0,w=_.length;v<w;++v)p(_[v]),d=c.lineHeight;u(x.after,p)}f=0,d=c.lineHeight,u(this.afterBody,p),t.y-=n}drawFooter(t,e,i){const s=this.footer,n=s.length;let o,a;if(n){const r=Oi(i.rtl,this.x,this.width);for(t.x=Ia(this,i.footerAlign,i),t.y+=i.footerMarginTop,e.textAlign=r.textAlign(i.footerAlign),e.textBaseline="middle",o=Si(i.footerFont),e.fillStyle=i.footerColor,e.font=o.string,a=0;a<n;++a)e.fillText(s[a],r.x(t.x),t.y+o.lineHeight/2),t.y+=o.lineHeight+i.footerSpacing}}drawBackground(t,e,i,s){const{xAlign:n,yAlign:o}=this,{x:a,y:r}=t,{width:l,height:h}=i,{topLeft:c,topRight:d,bottomLeft:u,bottomRight:f}=wi(s.cornerRadius);e.fillStyle=s.backgroundColor,e.strokeStyle=s.borderColor,e.lineWidth=s.borderWidth,e.beginPath(),e.moveTo(a+c,r),"top"===o&&this.drawCaret(t,e,i,s),e.lineTo(a+l-d,r),e.quadraticCurveTo(a+l,r,a+l,r+d),"center"===o&&"right"===n&&this.drawCaret(t,e,i,s),e.lineTo(a+l,r+h-f),e.quadraticCurveTo(a+l,r+h,a+l-f,r+h),"bottom"===o&&this.drawCaret(t,e,i,s),e.lineTo(a+u,r+h),e.quadraticCurveTo(a,r+h,a,r+h-u),"center"===o&&"left"===n&&this.drawCaret(t,e,i,s),e.lineTo(a,r+c),e.quadraticCurveTo(a,r,a+c,r),e.closePath(),e.fill(),s.borderWidth>0&&e.stroke()}_updateAnimationTarget(t){const e=this.chart,i=this.$animations,s=i&&i.x,n=i&&i.y;if(s||n){const i=Da[t.position].call(this,this._active,this._eventPosition);if(!i)return;const o=this._size=Ta(this,t),a=Object.assign({},i,this._size),r=Ea(e,t,a),l=Ra(t,a,r,e);s._to===l.x&&n._to===l.y||(this.xAlign=r.xAlign,this.yAlign=r.yAlign,this.width=o.width,this.height=o.height,this.caretX=i.x,this.caretY=i.y,this._resolveAnimations().update(this,l))}}_willRender(){return!!this.opacity}draw(t){const e=this.options.setContext(this.getContext());let i=this.opacity;if(!i)return;this._updateAnimationTarget(e);const s={width:this.width,height:this.height},n={x:this.x,y:this.y};i=Math.abs(i)<.001?0:i;const o=ki(e.padding),a=this.title.length||this.beforeBody.length||this.body.length||this.afterBody.length||this.footer.length;e.enabled&&a&&(t.save(),t.globalAlpha=i,this.drawBackground(n,t,s,e),Ai(t,e.textDirection),n.y+=o.top,this.drawTitle(n,t,e),this.drawBody(n,t,e),this.drawFooter(n,t,e),Ti(t,e.textDirection),t.restore())}getActiveElements(){return this._active||[]}setActiveElements(t,e){const i=this._active,s=t.map((({datasetIndex:t,index:e})=>{const i=this.chart.getDatasetMeta(t);if(!i)throw new Error("Cannot find a dataset at index "+t);return{datasetIndex:t,element:i.data[e],index:e}})),n=!f(i,s),o=this._positionChanged(s,e);(n||o)&&(this._active=s,this._eventPosition=e,this._ignoreReplayEvents=!0,this.update(!0))}handleEvent(t,e,i=!0){if(e&&this._ignoreReplayEvents)return!1;this._ignoreReplayEvents=!1;const s=this.options,n=this._active||[],o=this._getActiveElements(t,n,e,i),a=this._positionChanged(o,t),r=e||!f(o,n)||a;return r&&(this._active=o,(s.enabled||s.external)&&(this._eventPosition={x:t.x,y:t.y},this.update(!0,e))),r}_getActiveElements(t,e,i,s){const n=this.options;if("mouseout"===t.type)return[];if(!s)return e.filter((t=>this.chart.data.datasets[t.datasetIndex]&&void 0!==this.chart.getDatasetMeta(t.datasetIndex).controller.getParsed(t.index)));const o=this.chart.getElementsAtEventForMode(t,n.mode,n,i);return n.reverse&&o.reverse(),o}_positionChanged(t,e){const{caretX:i,caretY:s,options:n}=this,o=Da[n.position].call(this,t,e);return!1!==o&&(i!==o.x||s!==o.y)}}var Na={id:"tooltip",_element:Wa,positioners:Da,afterInit(t,e,i){i&&(t.tooltip=new Wa({chart:t,options:i}))},beforeUpdate(t,e,i){t.tooltip&&t.tooltip.initialize(i)},reset(t,e,i){t.tooltip&&t.tooltip.initialize(i)},afterDraw(t){const e=t.tooltip;if(e&&e._willRender()){const i={tooltip:e};if(!1===t.notifyPlugins("beforeTooltipDraw",{...i,cancelable:!0}))return;e.draw(t.ctx),t.notifyPlugins("afterTooltipDraw",i)}},afterEvent(t,e){if(t.tooltip){const i=e.replay;t.tooltip.handleEvent(e.event,i,e.inChartArea)&&(e.changed=!0)}},defaults:{enabled:!0,external:null,position:"average",backgroundColor:"rgba(0,0,0,0.8)",titleColor:"#fff",titleFont:{weight:"bold"},titleSpacing:2,titleMarginBottom:6,titleAlign:"left",bodyColor:"#fff",bodySpacing:2,bodyFont:{},bodyAlign:"left",footerColor:"#fff",footerSpacing:2,footerMarginTop:6,footerFont:{weight:"bold"},footerAlign:"left",padding:6,caretPadding:2,caretSize:5,cornerRadius:6,boxHeight:(t,e)=>e.bodyFont.size,boxWidth:(t,e)=>e.bodyFont.size,multiKeyBackground:"#fff",displayColors:!0,boxPadding:0,borderColor:"rgba(0,0,0,0)",borderWidth:0,animation:{duration:400,easing:"easeOutQuart"},animations:{numbers:{type:"number",properties:["x","y","width","height","caretX","caretY"]},opacity:{easing:"linear",duration:200}},callbacks:Va},defaultRoutes:{bodyFont:"font",footerFont:"font",titleFont:"font"},descriptors:{_scriptable:t=>"filter"!==t&&"itemSort"!==t&&"external"!==t,_indexable:!1,callbacks:{_scriptable:!1,_indexable:!1},animation:{_fallback:!1},animations:{_fallback:"animation"}},additionalOptionScopes:["interaction"]};return Tn.register(Un,$o,go,t),Tn.helpers={...Hi},Tn._adapters=In,Tn.Animation=As,Tn.Animations=Ts,Tn.animator=bt,Tn.controllers=nn.controllers.items,Tn.DatasetController=js,Tn.Element=$s,Tn.elements=go,Tn.Interaction=Ki,Tn.layouts=ls,Tn.platforms=Ds,Tn.Scale=tn,Tn.Ticks=ae,Object.assign(Tn,Un,$o,go,t,Ds),Tn.Chart=Tn,"undefined"!=typeof window&&(window.Chart=Tn),Tn}));
./scripts/health/assets/health-assets/ECHARTS-6.1.0-LICENSE.txt:13:      "Licensor" shall mean the copyright owner or entity authorized by
./scripts/health/assets/health-assets/ECHARTS-6.1.0-LICENSE.txt:14:      the copyright owner that is granting the License.
./scripts/health/assets/health-assets/ECHARTS-6.1.0-LICENSE.txt:38:      copyright notice that is included in or attached to the work
./scripts/health/assets/health-assets/ECHARTS-6.1.0-LICENSE.txt:52:      submitted to Licensor for inclusion in the Work by the copyright owner
./scripts/health/assets/health-assets/ECHARTS-6.1.0-LICENSE.txt:54:      the copyright owner. For the purposes of this definition, "submitted"
./scripts/health/assets/health-assets/ECHARTS-6.1.0-LICENSE.txt:61:      designated in writing by the copyright owner as "Not a Contribution."
./scripts/health/assets/health-assets/ECHARTS-6.1.0-LICENSE.txt:67:   2. Grant of Copyright License. Subject to the terms and conditions of
./scripts/health/assets/health-assets/ECHARTS-6.1.0-LICENSE.txt:70:      copyright license to reproduce, prepare Derivative Works of,
./scripts/health/assets/health-assets/ECHARTS-6.1.0-LICENSE.txt:102:          that You distribute, all copyright, patent, trademark, and
./scripts/health/assets/health-assets/ECHARTS-6.1.0-LICENSE.txt:124:      You may add Your own copyright statement to Your modifications and
./scripts/health/assets/health-assets/ECHARTS-6.1.0-LICENSE.txt:187:      same "printed page" as the copyright notice for easier
./scripts/health/assets/health-assets/ECHARTS-6.1.0-LICENSE.txt:190:   Copyright [yyyy] [name of copyright owner]
./scripts/health/assets/health-assets/ECHARTS-6.1.0-LICENSE.txt:211:The Apache ECharts project contains subcomponents with separate copyright
./scripts/health/assets/health-assets/ECHARTS-6.1.0-NOTICE.txt:2:Copyright 2017-2026 The Apache Software Foundation


I’m going to make the payload fields explicit instead of optional free text. That gives the server and worker the same exact shape and makes `unknown` a deliberate value that cannot accidentally create a personal tolerance row.


Input:
*** Begin Patch
*** Update File: scripts/health/health_dashboard_action_worker.py
@@
 MAPPING_CONFIDENCE = frozenset({"low", "medium", "high"})
 MAPPING_METHODS = frozenset({"sighi_reference", "ingredient_label", "manual_review", "local_alias"})
+PERSONAL_TOLERANCE_STATUSES = frozenset(
+    {"unknown", "documented_tolerated", "documented_not_tolerated", "unclear"}
+)
 HISTAMINE_DEFINITION_VERSION = "sighi_mapping_load_v2"
@@
         "source_version",
         "note",
         "ingredient_review_required",
+        "personal_tolerance_status",
+        "personal_tolerance_note",
     }
-    optional = {"personal_tolerance"}
-    if not required <= set(payload) <= required | optional:
+    if set(payload) != required:
         raise ValueError("invalid mapping payload shape")
@@
     note = " ".join(str(payload["note"] or "").split())
     ingredient_review = payload["ingredient_review_required"]
-    tolerance = payload.get("personal_tolerance")
+    tolerance_status = str(payload["personal_tolerance_status"] or "")
+    tolerance_note = " ".join(str(payload["personal_tolerance_note"] or "").split())
@@
         ("source version", source_version, 40),
         ("note", note, 300),
+        ("personal tolerance note", tolerance_note, 160),
     ):
         if len(text) > maximum or re.search(r"[\x00-\x1f\x7f]|https?://|/|\\\\|\.hermes", text, re.I):
             raise ValueError(f"invalid {field_name}")
@@
     if type(ingredient_review) is not bool:
         raise ValueError("invalid ingredient review flag")
-    if tolerance is not None and (not isinstance(tolerance, str) or len(tolerance) > 160 or re.search(r"[\x00-\x1f\x7f]|https?://|/|\\\\|\.hermes", tolerance, re.I)):
-        raise ValueError("invalid personal tolerance")
+    if tolerance_status not in PERSONAL_TOLERANCE_STATUSES:
+        raise ValueError("invalid personal tolerance status")
@@
         "note": note,
         "ingredient_review_required": ingredient_review,
-        "personal_tolerance": tolerance,
+        "personal_tolerance_status": tolerance_status,
+        "personal_tolerance_note": tolerance_note,
     }
@@
         CREATE TABLE IF NOT EXISTS nutrition_mapping_action_log (
@@
             note TEXT,
             ingredient_review_required INTEGER,
-            personal_tolerance TEXT,
+            personal_tolerance_status TEXT,
+            personal_tolerance_note TEXT,
             definition_version TEXT,
             applied_at TEXT DEFAULT CURRENT_TIMESTAMP
         );
@@
             source_version TEXT,
             note TEXT,
             ingredient_review_required INTEGER,
-            personal_tolerance TEXT,
+            personal_tolerance_status TEXT,
+            personal_tolerance_note TEXT,
             definition_version TEXT,
             affected_day TEXT NOT NULL,
             applied_at TEXT DEFAULT CURRENT_TIMESTAMP,
             UNIQUE(action_id, affected_day)
         );
+        CREATE TABLE IF NOT EXISTS personal_food_tolerance (
+            canonical_food TEXT PRIMARY KEY,
+            personal_status TEXT NOT NULL DEFAULT 'unknown'
+                CHECK(personal_status IN ('unknown','documented_tolerated','documented_not_tolerated','unclear')),
+            evidence_level TEXT DEFAULT 'user_documented'
+                CHECK(evidence_level IN ('low','medium','high','user_documented')),
+            notes TEXT,
+            updated_at TEXT DEFAULT CURRENT_TIMESTAMP
+        );
         CREATE TABLE IF NOT EXISTS nutrition_composite_product_review (
             normalized_name TEXT PRIMARY KEY,
             canonical_food TEXT,
@@
         "nutrition_mapping_action_log": {
             "decision": "TEXT", "alias": "TEXT", "canonical_food": "TEXT", "sighi_score": "INTEGER",
             "confidence": "TEXT", "mapping_method": "TEXT", "source_label": "TEXT", "source_version": "TEXT",
-            "note": "TEXT", "ingredient_review_required": "INTEGER", "personal_tolerance": "TEXT",
+            "note": "TEXT", "ingredient_review_required": "INTEGER", "personal_tolerance_status": "TEXT",
+            "personal_tolerance_note": "TEXT",
             "definition_version": "TEXT",
         },
         "nutrition_mapping_provenance": {
             "decision": "TEXT", "alias": "TEXT", "confidence": "TEXT", "mapping_method": "TEXT",
             "source_label": "TEXT", "source_version": "TEXT", "note": "TEXT",
-            "ingredient_review_required": "INTEGER", "personal_tolerance": "TEXT",
+            "ingredient_review_required": "INTEGER", "personal_tolerance_status": "TEXT",
+            "personal_tolerance_note": "TEXT",
             "definition_version": "TEXT",
         },
@@
             elif payload["decision"] == "assign":
                 for row in item_rows:
                     connection.execute(
@@
                         (row["id"], canonical, score, "classified", payload["mapping_method"], payload["confidence"], payload["note"] or "local validated mapping action"),
                     )
+                if payload["personal_tolerance_status"] != "unknown":
+                    connection.execute(
+                        """INSERT INTO personal_food_tolerance
+                           (canonical_food,personal_status,evidence_level,notes,updated_at)
+                           VALUES(?,?, 'user_documented', ?, CURRENT_TIMESTAMP)
+                           ON CONFLICT(canonical_food) DO UPDATE SET
+                             personal_status=excluded.personal_status,
+                             evidence_level=excluded.evidence_level,
+                             notes=excluded.notes,
+                             updated_at=CURRENT_TIMESTAMP""",
+                        (
+                            canonical,
+                            payload["personal_tolerance_status"],
+                            payload["personal_tolerance_note"] or None,
+                        ),
+                    )
                 connection.execute("UPDATE nutrition_review_queue SET status='mapped',updated_at=CURRENT_TIMESTAMP WHERE normalized_name=?", (normalized,))
@@
                         """INSERT OR IGNORE INTO nutrition_mapping_provenance
                        (action_id,normalized_name,decision,alias,canonical_food,sighi_score,confidence,
                         mapping_method,source_label,source_version,note,ingredient_review_required,
-                        personal_tolerance,definition_version,affected_day)
+                        personal_tolerance_status,personal_tolerance_note,definition_version,affected_day)
                        VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
                     (
                         action_id, normalized, payload["decision"], payload["alias"], canonical or None,
                         score, payload["confidence"], payload["mapping_method"], payload["source_label"],
                         payload["source_version"], payload["note"], int(payload["ingredient_review_required"]),
-                        payload.get("personal_tolerance"), HISTAMINE_DEFINITION_VERSION, str(day),
+                        payload["personal_tolerance_status"], payload["personal_tolerance_note"] or None,
+                        HISTAMINE_DEFINITION_VERSION, str(day),
                     ),
                 )
             connection.execute(
                 """INSERT INTO nutrition_mapping_action_log
                    (action_id,action_hash,queue_key,decision,alias,canonical_food,sighi_score,confidence,
                     mapping_method,source_label,source_version,note,ingredient_review_required,
-                    personal_tolerance,definition_version)
+                    personal_tolerance_status,personal_tolerance_note,definition_version)
                    VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
                 (
                     action_id, action_hash, payload["queue_key"], payload["decision"], payload["alias"],
                     canonical or None, score, payload["confidence"], payload["mapping_method"],
                     payload["source_label"], payload["source_version"], payload["note"],
-                    int(payload["ingredient_review_required"]), payload.get("personal_tolerance"),
-                    HISTAMINE_DEFINITION_VERSION,
+                    int(payload["ingredient_review_required"]), payload["personal_tolerance_status"],
+                    payload["personal_tolerance_note"] or None, HISTAMINE_DEFINITION_VERSION,
                 ),
             )
*** End Patch


Success. Updated the following files:
M scripts/health/health_dashboard_action_worker.py

@@ -55,2 +55,5 @@
 MAPPING_METHODS = frozenset({"sighi_reference", "ingredient_label", "manual_review", "local_alias"})
+PERSONAL_TOLERANCE_STATUSES = frozenset(
+    {"unknown", "documented_tolerated", "documented_not_tolerated", "unclear"}
+)
 HISTAMINE_DEFINITION_VERSION = "sighi_mapping_load_v2"
@@ -125,5 +128,6 @@
         "ingredient_review_required",
+        "personal_tolerance_status",
+        "personal_tolerance_note",
     }
-    optional = {"personal_tolerance"}
-    if not required <= set(payload) <= required | optional:
+    if set(payload) != required:
         raise ValueError("invalid mapping payload shape")
@@ -142,3 +146,4 @@
     ingredient_review = payload["ingredient_review_required"]
-    tolerance = payload.get("personal_tolerance")
+    tolerance_status = str(payload["personal_tolerance_status"] or "")
+    tolerance_note = " ".join(str(payload["personal_tolerance_note"] or "").split())
     if not isinstance(queue_key, str) or not re.fullmatch(r"[a-z0-9_]{1,80}", queue_key):
@@ -153,2 +158,3 @@
         ("note", note, 300),
+        ("personal tolerance note", tolerance_note, 160),
     ):
@@ -162,4 +168,4 @@
         raise ValueError("invalid ingredient review flag")
-    if tolerance is not None and (not isinstance(tolerance, str) or len(tolerance) > 160 or re.search(r"[\x00-\x1f\x7f]|https?://|/|\\\\|\.hermes", tolerance, re.I)):
-        raise ValueError("invalid personal tolerance")
+    if tolerance_status not in PERSONAL_TOLERANCE_STATUSES:
+        raise ValueError("invalid personal tolerance status")
     if decision == "assign":
@@ -187,3 +193,4 @@
         "ingredient_review_required": ingredient_review,
-        "personal_tolerance": tolerance,
+        "personal_tolerance_status": tolerance_status,
+        "personal_tolerance_note": tolerance_note,
     }
@@ -337,3 +344,4 @@
             ingredient_review_required INTEGER,
-            personal_tolerance TEXT,
+            personal_tolerance_status TEXT,
+            personal_tolerance_note TEXT,
             definition_version TEXT,
@@ -355,3 +363,4 @@
             ingredient_review_required INTEGER,
-            personal_tolerance TEXT,
+            personal_tolerance_status TEXT,
+            personal_tolerance_note TEXT,
             definition_version TEXT,
@@ -361,2 +370,11 @@
         );
+        CREATE TABLE IF NOT EXISTS personal_food_tolerance (
+            canonical_food TEXT PRIMARY KEY,
+            personal_status TEXT NOT NULL DEFAULT 'unknown'
+                CHECK(personal_status IN ('unknown','documented_tolerated','documented_not_tolerated','unclear')),
+            evidence_level TEXT DEFAULT 'user_documented'
+                CHECK(evidence_level IN ('low','medium','high','user_documented')),
+            notes TEXT,
+            updated_at TEXT DEFAULT CURRENT_TIMESTAMP
+        );
         CREATE TABLE IF NOT EXISTS nutrition_composite_product_review (
@@ -373,3 +391,4 @@
             "confidence": "TEXT", "mapping_method": "TEXT", "source_label": "TEXT", "source_version": "TEXT",
-            "note": "TEXT", "ingredient_review_required": "INTEGER", "personal_tolerance": "TEXT",
+            "note": "TEXT", "ingredient_review_required": "INTEGER", "personal_tolerance_status": "TEXT",
+            "personal_tolerance_note": "TEXT",
             "definition_version": "TEXT",
@@ -379,3 +398,4 @@
             "source_label": "TEXT", "source_version": "TEXT", "note": "TEXT",
-            "ingredient_review_required": "INTEGER", "personal_tolerance": "TEXT",
+            "ingredient_review_required": "INTEGER", "personal_tolerance_status": "TEXT",
+            "personal_tolerance_note": "TEXT",
             "definition_version": "TEXT",
@@ -497,2 +517,18 @@
                     )
+                if payload["personal_tolerance_status"] != "unknown":
+                    connection.execute(
+                        """INSERT INTO personal_food_tolerance
+                           (canonical_food,personal_status,evidence_level,notes,updated_at)
+                           VALUES(?,?, 'user_documented', ?, CURRENT_TIMESTAMP)
+                           ON CONFLICT(canonical_food) DO UPDATE SET
+                             personal_status=excluded.personal_status,
+                             evidence_level=excluded.evidence_level,
+                             notes=excluded.notes,
+                             updated_at=CURRENT_TIMESTAMP""",
+                        (
+                            canonical,
+                            payload["personal_tolerance_status"],
+                            payload["personal_tolerance_note"] or None,
+                        ),
+                    )
                 connection.execute("UPDATE nutrition_review_queue SET status='mapped',updated_at=CURRENT_TIMESTAMP WHERE normalized_name=?", (normalized,))
@@ -504,6 +540,6 @@
                 connection.execute(
-                    """INSERT OR IGNORE INTO nutrition_mapping_provenance
+                        """INSERT OR IGNORE INTO nutrition_mapping_provenance
                        (action_id,normalized_name,decision,alias,canonical_food,sighi_score,confidence,
                         mapping_method,source_label,source_version,note,ingredient_review_required,
-                        personal_tolerance,definition_version,affected_day)
+                        personal_tolerance_status,personal_tolerance_note,definition_version,affected_day)
                        VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
@@ -513,3 +549,4 @@
                         payload["source_version"], payload["note"], int(payload["ingredient_review_required"]),
-                        payload.get("personal_tolerance"), HISTAMINE_DEFINITION_VERSION, str(day),
+                        payload["personal_tolerance_status"], payload["personal_tolerance_note"] or None,
+                        HISTAMINE_DEFINITION_VERSION, str(day),
                     ),
@@ -520,3 +557,3 @@
                     mapping_method,source_label,source_version,note,ingredient_review_required,
-                    personal_tolerance,definition_version)
+                    personal_tolerance_status,personal_tolerance_note,definition_version)
                    VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
@@ -526,4 +563,4 @@
                     payload["source_label"], payload["source_version"], payload["note"],
-                    int(payload["ingredient_review_required"]), payload.get("personal_tolerance"),
-                    HISTAMINE_DEFINITION_VERSION,
+                    int(payload["ingredient_review_required"]), payload["personal_tolerance_status"],
+                    payload["personal_tolerance_note"] or None, HISTAMINE_DEFINITION_VERSION,
                 ),


Output:
Exit code: 0
Wall time: 0.1 seconds
Output:
Success. Updated the following files:
M scripts/health/health_dashboard_action_worker.py


Input:
*** Begin Patch
*** Update File: scripts/health/health_dashboard_action_worker.py
@@
                         """INSERT OR IGNORE INTO nutrition_mapping_provenance
                        (action_id,normalized_name,decision,alias,canonical_food,sighi_score,confidence,
                         mapping_method,source_label,source_version,note,ingredient_review_required,
                         personal_tolerance_status,personal_tolerance_note,definition_version,affected_day)
-                       VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
+                       VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
@@
                    (action_id,action_hash,queue_key,decision,alias,canonical_food,sighi_score,confidence,
                     mapping_method,source_label,source_version,note,ingredient_review_required,
                     personal_tolerance_status,personal_tolerance_note,definition_version)
-                   VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
+                   VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
*** End Patch


Output:
Exit code: 0
Wall time: 0.1 seconds
Output:
Success. Updated the following files:
M scripts/health/health_dashboard_action_worker.py


The worker now separates SIGHi provenance from personal tolerance writes. Next I’m matching the HTTP form validation and the browser controls to that exact payload so queued actions cannot use the removed `personal_tolerance` field.


Input:
*** Begin Patch
*** Update File: scripts/health/health_dashboard_server.py
@@
 MAPPING_CONFIDENCE = {"low", "medium", "high"}
 MAPPING_METHODS = {"sighi_reference", "ingredient_label", "manual_review", "local_alias"}
+PERSONAL_TOLERANCE_STATUSES = {
+    "unknown",
+    "documented_tolerated",
+    "documented_not_tolerated",
+    "unclear",
+}
 CSRF_TTL_SECONDS = max(
@@
         "source_version",
         "note",
         "ingredient_review_required",
+        "personal_tolerance_status",
+        "personal_tolerance_note",
         "return_to",
     }
-    allowed = required | {"personal_tolerance"}
-    if not required <= set(form) <= allowed or any(len(values) != 1 for values in form.values()):
+    if set(form) != required or any(len(values) != 1 for values in form.values()):
         raise ValueError("invalid mapping shape")
@@
     note = " ".join(form["note"][0].split())
     review = form["ingredient_review_required"][0]
-    tolerance = " ".join(form.get("personal_tolerance", [""])[0].split())
+    tolerance_status = form["personal_tolerance_status"][0]
+    tolerance_note = " ".join(form["personal_tolerance_note"][0].split())
@@
-    for value, maximum in ((alias, 160), (canonical_food, 120), (source_label, 80), (source_version, 40), (note, 300), (tolerance, 160)):
+    for value, maximum in (
+        (alias, 160),
+        (canonical_food, 120),
+        (source_label, 80),
+        (source_version, 40),
+        (note, 300),
+        (tolerance_note, 160),
+    ):
         if len(value) > maximum or re.search(r"[\x00-\x1f\x7f]|https?://|/|\\\\|\.hermes", value, re.I):
             raise ValueError("invalid mapping text")
@@
-    if decision not in MAPPING_DECISIONS or score not in MAPPING_SCORES or confidence not in MAPPING_CONFIDENCE or method not in MAPPING_METHODS or review not in {"true", "false"}:
+    if (
+        decision not in MAPPING_DECISIONS
+        or score not in MAPPING_SCORES
+        or confidence not in MAPPING_CONFIDENCE
+        or method not in MAPPING_METHODS
+        or review not in {"true", "false"}
+        or tolerance_status not in PERSONAL_TOLERANCE_STATUSES
+    ):
         raise ValueError("invalid mapping value")
@@
         "source_version": source_version,
         "note": note,
         "ingredient_review_required": ingredient_review_required,
-        **({"personal_tolerance": tolerance} if tolerance else {}),
+        "personal_tolerance_status": tolerance_status,
+        "personal_tolerance_note": tolerance_note,
     }, return_to
*** End Patch


Success. Updated the following files:
M scripts/health/health_dashboard_server.py

@@ -139,2 +139,8 @@
 MAPPING_METHODS = {"sighi_reference", "ingredient_label", "manual_review", "local_alias"}
+PERSONAL_TOLERANCE_STATUSES = {
+    "unknown",
+    "documented_tolerated",
+    "documented_not_tolerated",
+    "unclear",
+}
 CSRF_TTL_SECONDS = max(
@@ -517,6 +523,7 @@
         "ingredient_review_required",
+        "personal_tolerance_status",
+        "personal_tolerance_note",
         "return_to",
     }
-    allowed = required | {"personal_tolerance"}
-    if not required <= set(form) <= allowed or any(len(values) != 1 for values in form.values()):
+    if set(form) != required or any(len(values) != 1 for values in form.values()):
         raise ValueError("invalid mapping shape")
@@ -536,6 +543,14 @@
     review = form["ingredient_review_required"][0]
-    tolerance = " ".join(form.get("personal_tolerance", [""])[0].split())
+    tolerance_status = form["personal_tolerance_status"][0]
+    tolerance_note = " ".join(form["personal_tolerance_note"][0].split())
     if not re.fullmatch(r"[a-z0-9_]{1,80}", queue_key):
         raise ValueError("invalid queue key")
-    for value, maximum in ((alias, 160), (canonical_food, 120), (source_label, 80), (source_version, 40), (note, 300), (tolerance, 160)):
+    for value, maximum in (
+        (alias, 160),
+        (canonical_food, 120),
+        (source_label, 80),
+        (source_version, 40),
+        (note, 300),
+        (tolerance_note, 160),
+    ):
         if len(value) > maximum or re.search(r"[\x00-\x1f\x7f]|https?://|/|\\\\|\.hermes", value, re.I):
@@ -544,3 +559,10 @@
         raise ValueError("mapping documentation required")
-    if decision not in MAPPING_DECISIONS or score not in MAPPING_SCORES or confidence not in MAPPING_CONFIDENCE or method not in MAPPING_METHODS or review not in {"true", "false"}:
+    if (
+        decision not in MAPPING_DECISIONS
+        or score not in MAPPING_SCORES
+        or confidence not in MAPPING_CONFIDENCE
+        or method not in MAPPING_METHODS
+        or review not in {"true", "false"}
+        or tolerance_status not in PERSONAL_TOLERANCE_STATUSES
+    ):
         raise ValueError("invalid mapping value")
@@ -567,3 +589,4 @@
         "ingredient_review_required": ingredient_review_required,
-        **({"personal_tolerance": tolerance} if tolerance else {}),
+        "personal_tolerance_status": tolerance_status,
+        "personal_tolerance_note": tolerance_note,
     }, return_to


Output:
Exit code: 0
Wall time: 0.1 seconds
Output:
Success. Updated the following files:
M scripts/health/health_dashboard_server.py


The server now rejects the old field entirely. I’m adding a small status select plus bounded note input in the existing mapping form and resetting them to unknown for composite/ignore decisions.


Input:
*** Begin Patch
*** Update File: scripts/health/assets/health-assets/dashboard-v5.js
@@
         const note = node("input");
         note.name = "note";
         note.maxLength = 300;
         note.value = item.reason || "";
+        const personalStatus = node("select");
+        personalStatus.name = "personal_tolerance_status";
+        [
+          ["unknown", "persönlich unbekannt"],
+          ["documented_tolerated", "dokumentiert vertragen"],
+          ["documented_not_tolerated", "dokumentiert nicht vertragen"],
+          ["unclear", "unklar dokumentiert"],
+        ].forEach(([value, label]) => {
+          const option = node("option", "", label);
+          option.value = value;
+          personalStatus.append(option);
+        });
+        const personalNote = node("input");
+        personalNote.name = "personal_tolerance_note";
+        personalNote.maxLength = 160;
         const reviewRequired = node("input");
         reviewRequired.type = "hidden";
         reviewRequired.name = "ingredient_review_required";
@@
           score.value = composite || ignore ? "unknown" : score.value;
           canonical.value = ignore ? "" : canonical.value;
           canonical.readOnly = ignore;
+          personalStatus.value = composite || ignore ? "unknown" : personalStatus.value;
+          personalNote.value = composite || ignore ? "" : personalNote.value;
+          personalStatus.disabled = composite || ignore;
+          personalNote.disabled = composite || ignore;
         });
@@
           ["Methode", method],
           ["Notiz", note],
+          ["Persönliche Toleranz", personalStatus],
+          ["Toleranznotiz", personalNote],
         ].forEach(([labelText, control]) => {
*** End Patch


Success. Updated the following files:
M scripts/health/assets/health-assets/dashboard-v5.js

@@ -483,2 +483,17 @@
         note.value = item.reason || "";
+        const personalStatus = node("select");
+        personalStatus.name = "personal_tolerance_status";
+        [
+          ["unknown", "persönlich unbekannt"],
+          ["documented_tolerated", "dokumentiert vertragen"],
+          ["documented_not_tolerated", "dokumentiert nicht vertragen"],
+          ["unclear", "unklar dokumentiert"],
+        ].forEach(([value, label]) => {
+          const option = node("option", "", label);
+          option.value = value;
+          personalStatus.append(option);
+        });
+        const personalNote = node("input");
+        personalNote.name = "personal_tolerance_note";
+        personalNote.maxLength = 160;
         const reviewRequired = node("input");
@@ -494,2 +509,6 @@
           canonical.readOnly = ignore;
+          personalStatus.value = composite || ignore ? "unknown" : personalStatus.value;
+          personalNote.value = composite || ignore ? "" : personalNote.value;
+          personalStatus.disabled = composite || ignore;
+          personalNote.disabled = composite || ignore;
         });
@@ -502,2 +521,4 @@
           ["Notiz", note],
+          ["Persönliche Toleranz", personalStatus],
+          ["Toleranznotiz", personalNote],
         ].forEach(([labelText, control]) => {


Output:
Exit code: 0
Wall time: 0.1 seconds
Output:
Success. Updated the following files:
M scripts/health/assets/health-assets/dashboard-v5.js


Input:
*** Begin Patch
*** Update File: scripts/health/assets/health-assets/dashboard-v5.js
@@
           canonical.readOnly = ignore;
           personalStatus.value = composite || ignore ? "unknown" : personalStatus.value;
           personalNote.value = composite || ignore ? "" : personalNote.value;
-          personalStatus.disabled = composite || ignore;
-          personalNote.disabled = composite || ignore;
         });
*** End Patch


@@ -511,4 +511,2 @@
           personalNote.value = composite || ignore ? "" : personalNote.value;
-          personalStatus.disabled = composite || ignore;
-          personalNote.disabled = composite || ignore;
         });


Output:
Exit code: 0
Wall time: 0.1 seconds
Output:
Success. Updated the following files:
M scripts/health/assets/health-assets/dashboard-v5.js


Input:
*** Begin Patch
*** Update File: database/schema.sql
@@
 CREATE TABLE personal_food_tolerance (
     canonical_food TEXT PRIMARY KEY,
-    personal_status TEXT NOT NULL DEFAULT 'unknown',
-    evidence_level TEXT DEFAULT 'low' CHECK(evidence_level IN ('low','medium','high')),
+    personal_status TEXT NOT NULL DEFAULT 'unknown' CHECK(personal_status IN ('unknown','documented_tolerated','documented_not_tolerated','unclear')),
+    evidence_level TEXT DEFAULT 'user_documented' CHECK(evidence_level IN ('low','medium','high','user_documented')),
     notes TEXT,
     updated_at TEXT DEFAULT CURRENT_TIMESTAMP
 );
@@
     source_version TEXT,
     note TEXT,
     ingredient_review_required INTEGER,
-    personal_tolerance TEXT,
+    personal_tolerance_status TEXT,
+    personal_tolerance_note TEXT,
     definition_version TEXT,
     applied_at TEXT DEFAULT CURRENT_TIMESTAMP
 );
@@
     source_version TEXT,
     note TEXT,
     ingredient_review_required INTEGER,
-    personal_tolerance TEXT,
+    personal_tolerance_status TEXT,
+    personal_tolerance_note TEXT,
     definition_version TEXT,
     affected_day TEXT NOT NULL,
*** End Patch


Success. Updated the following files:
M database/schema.sql

@@ -425,4 +425,4 @@
     canonical_food TEXT PRIMARY KEY,
-    personal_status TEXT NOT NULL DEFAULT 'unknown',
-    evidence_level TEXT DEFAULT 'low' CHECK(evidence_level IN ('low','medium','high')),
+    personal_status TEXT NOT NULL DEFAULT 'unknown' CHECK(personal_status IN ('unknown','documented_tolerated','documented_not_tolerated','unclear')),
+    evidence_level TEXT DEFAULT 'user_documented' CHECK(evidence_level IN ('low','medium','high','user_documented')),
     notes TEXT,
@@ -445,3 +445,4 @@
     ingredient_review_required INTEGER,
-    personal_tolerance TEXT,
+    personal_tolerance_status TEXT,
+    personal_tolerance_note TEXT,
     definition_version TEXT,
@@ -466,3 +467,4 @@
     ingredient_review_required INTEGER,
-    personal_tolerance TEXT,
+    personal_tolerance_status TEXT,
+    personal_tolerance_note TEXT,
     definition_version TEXT,


Output:
Exit code: 0
Wall time: 0.1 seconds
Output:
Success. Updated the following files:
M database/schema.sql


Input:
*** Begin Patch
*** Update File: tests/test_dashboard_v5_sprint6g_nutrition.py
@@
 def test_mapping_worker_is_transactional_idempotent_and_recomputes_only_affected_days(
     fixture_db: Path, monkeypatch: pytest.MonkeyPatch
 ) -> None:
@@
         "source_version": "2026-test",
         "note": "synthetic reviewed mapping",
         "ingredient_review_required": False,
-        "personal_tolerance": "documented local note",
+        "personal_tolerance_status": "documented_tolerated",
+        "personal_tolerance_note": "documented local note",
     }
@@
-        provenance = connection.execute("SELECT decision,alias,confidence,mapping_method,source_label,source_version,note,ingredient_review_required,personal_tolerance,definition_version FROM nutrition_mapping_provenance").fetchone()
+        source_row = connection.execute(
+            """SELECT canonical_food,sighi_score,confidence,reason
+               FROM nutrition_histamine_scores WHERE item_id=2"""
+        ).fetchone()
+        assert dict(source_row) == {
+            "canonical_food": "synthetic reviewed food",
+            "sighi_score": 1,
+            "confidence": "medium",
+            "reason": "synthetic reviewed mapping",
+        }
+        provenance = connection.execute("SELECT decision,alias,confidence,mapping_method,source_label,source_version,note,ingredient_review_required,personal_tolerance_status,personal_tolerance_note,definition_version FROM nutrition_mapping_provenance").fetchone()
         assert dict(provenance) == {
             "decision": "assign",
             "alias": "SYNTHETIC_INCOMPLETE_FOOD_MAPPING_WITH_A_VERY_LONG_NAME_THAT_MUST_WRAP",
@@
             "source_version": "2026-test",
             "note": "synthetic reviewed mapping",
             "ingredient_review_required": 0,
-            "personal_tolerance": "documented local note",
+            "personal_tolerance_status": "documented_tolerated",
+            "personal_tolerance_note": "documented local note",
             "definition_version": "sighi_mapping_load_v2",
         }
-        assert connection.execute("SELECT COUNT(*) FROM personal_food_tolerance").fetchone()[0] == 0
+        personal = connection.execute(
+            """SELECT canonical_food,personal_status,evidence_level,notes
+               FROM personal_food_tolerance"""
+        ).fetchone()
+        assert dict(personal) == {
+            "canonical_food": "synthetic reviewed food",
+            "personal_status": "documented_tolerated",
+            "evidence_level": "user_documented",
+            "notes": "documented local note",
+        }
@@
         "source_version": "2026-test",
         "note": "synthetic decision",
         "ingredient_review_required": True,
+        "personal_tolerance_status": "documented_not_tolerated",
+        "personal_tolerance_note": "must not be stored for composite or ignore",
     }
@@
         assert connection.execute("SELECT sighi_score FROM nutrition_histamine_scores WHERE item_id=2").fetchone()[0] is None
         assert connection.execute("SELECT status FROM nutrition_composite_product_review").fetchone()[0] == "needs_ingredient_review"
         assert connection.execute("SELECT COUNT(*) FROM nutrition_mapping_action_log").fetchone()[0] == 2
+        assert connection.execute("SELECT COUNT(*) FROM personal_food_tolerance").fetchone()[0] == 0
     finally:
         connection.close()
@@
         "source_version": ["2026-test"],
         "note": ["bounded note"],
         "ingredient_review_required": ["false"],
-        "personal_tolerance": ["neutral note"],
+        "personal_tolerance_status": ["unclear"],
+        "personal_tolerance_note": ["neutral note"],
     }
     payload, return_to = server.validate_mapping_submission(form)
     assert return_to == "v5"
     assert payload["decision"] == "assign"
-    assert payload["personal_tolerance"] == "neutral note"
+    assert payload["personal_tolerance_status"] == "unclear"
+    assert payload["personal_tolerance_note"] == "neutral note"
     with pytest.raises(ValueError):
         server.validate_mapping_submission({**form, "canonical_food": ["x", "y"]})
@@
     assert server.consume_csrf_token(token, now=101.0)
     assert not server.consume_csrf_token(token, now=102.0)
+
+
+def test_unknown_personal_tolerance_does_not_create_personal_row(
+    fixture_db: Path, monkeypatch: pytest.MonkeyPatch
+) -> None:
+    import health_dashboard_action_worker as worker
+
+    monkeypatch.setattr(worker, "DASHBOARD_DB", fixture_db)
+    monkeypatch.setattr(worker, "DASHBOARD_V5_FILE", None)
+    payload = {
+        "version": 1,
+        "action": "nutrition_mapping",
+        "decision": "assign",
+        "queue_key": "synthetic_incomplete_food_mapping_with_a_very_long_name_that_must_wrap",
+        "alias": "SYNTHETIC_INCOMPLETE_FOOD_MAPPING_WITH_A_VERY_LONG_NAME_THAT_MUST_WRAP",
+        "canonical_food": "synthetic reviewed food",
+        "sighi_score": 1,
+        "confidence": "medium",
+        "mapping_method": "manual_review",
+        "source_label": "SIGHi",
+        "source_version": "2026-test",
+        "note": "synthetic reviewed mapping",
+        "ingredient_review_required": False,
+        "personal_tolerance_status": "unknown",
+        "personal_tolerance_note": "",
+    }
+
+    worker.apply_mapping_action(payload, "d" * 32)
+
+    connection = sqlite3.connect(fixture_db)
+    try:
+        assert connection.execute("SELECT COUNT(*) FROM personal_food_tolerance").fetchone()[0] == 0
+        assert connection.execute("SELECT sighi_score FROM nutrition_histamine_scores WHERE item_id=2").fetchone()[0] == 1
+    finally:
+        connection.close()
+
+
+def _raw_tables(connection: sqlite3.Connection) -> dict[str, list[tuple[object, ...]]]:
+    tables = [
+        row[0]
+        for row in connection.execute(
+            "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name"
+        )
+    ]
+    result: dict[str, list[tuple[object, ...]]] = {}
+    for table in tables:
+        result[table] = list(connection.execute(f'SELECT * FROM "{table}" ORDER BY rowid'))
+    return result
+
+
+def test_mapping_schema_migration_restore_from_fixture_backup(tmp_path: Path, fixture_db: Path) -> None:
+    import health_dashboard_action_worker as worker
+
+    copied = tmp_path / "copied-fixture.db"
+    copied.write_bytes(fixture_db.read_bytes())
+    backup_bytes = copied.read_bytes()
+    connection = sqlite3.connect(copied)
+    try:
+        before = _raw_tables(connection)
+        worker.ensure_nutrition_mapping_schema(connection)
+        worker.ensure_nutrition_mapping_schema(connection)
+        connection.commit()
+        assert connection.execute("PRAGMA integrity_check").fetchone()[0] == "ok"
+    finally:
+        connection.close()
+
+    copied.write_bytes(backup_bytes)
+    restored = sqlite3.connect(copied)
+    try:
+        assert restored.execute("PRAGMA integrity_check").fetchone()[0] == "ok"
+        assert _raw_tables(restored) == before
+    finally:
+        restored.close()
*** End Patch


Success. Updated the following files:
M tests/test_dashboard_v5_sprint6g_nutrition.py

@@ -119,3 +119,4 @@
         "ingredient_review_required": False,
-        "personal_tolerance": "documented local note",
+        "personal_tolerance_status": "documented_tolerated",
+        "personal_tolerance_note": "documented local note",
     }
@@ -140,3 +141,13 @@
         assert dict(unaffected) == {"kcal": 123.0, "histamine_score": 2.0}
-        provenance = connection.execute("SELECT decision,alias,confidence,mapping_method,source_label,source_version,note,ingredient_review_required,personal_tolerance,definition_version FROM nutrition_mapping_provenance").fetchone()
+        source_row = connection.execute(
+            """SELECT canonical_food,sighi_score,confidence,reason
+               FROM nutrition_histamine_scores WHERE item_id=2"""
+        ).fetchone()
+        assert dict(source_row) == {
+            "canonical_food": "synthetic reviewed food",
+            "sighi_score": 1,
+            "confidence": "medium",
+            "reason": "synthetic reviewed mapping",
+        }
+        provenance = connection.execute("SELECT decision,alias,confidence,mapping_method,source_label,source_version,note,ingredient_review_required,personal_tolerance_status,personal_tolerance_note,definition_version FROM nutrition_mapping_provenance").fetchone()
         assert dict(provenance) == {
@@ -150,6 +161,16 @@
             "ingredient_review_required": 0,
-            "personal_tolerance": "documented local note",
+            "personal_tolerance_status": "documented_tolerated",
+            "personal_tolerance_note": "documented local note",
             "definition_version": "sighi_mapping_load_v2",
         }
-        assert connection.execute("SELECT COUNT(*) FROM personal_food_tolerance").fetchone()[0] == 0
+        personal = connection.execute(
+            """SELECT canonical_food,personal_status,evidence_level,notes
+               FROM personal_food_tolerance"""
+        ).fetchone()
+        assert dict(personal) == {
+            "canonical_food": "synthetic reviewed food",
+            "personal_status": "documented_tolerated",
+            "evidence_level": "user_documented",
+            "notes": "documented local note",
+        }
     finally:
@@ -198,2 +219,4 @@
         "ingredient_review_required": True,
+        "personal_tolerance_status": "documented_not_tolerated",
+        "personal_tolerance_note": "must not be stored for composite or ignore",
     }
@@ -209,2 +232,3 @@
         assert connection.execute("SELECT COUNT(*) FROM nutrition_mapping_action_log").fetchone()[0] == 2
+        assert connection.execute("SELECT COUNT(*) FROM personal_food_tolerance").fetchone()[0] == 0
     finally:
@@ -233,3 +257,4 @@
         "ingredient_review_required": ["false"],
-        "personal_tolerance": ["neutral note"],
+        "personal_tolerance_status": ["unclear"],
+        "personal_tolerance_note": ["neutral note"],
     }
@@ -238,3 +263,4 @@
     assert payload["decision"] == "assign"
-    assert payload["personal_tolerance"] == "neutral note"
+    assert payload["personal_tolerance_status"] == "unclear"
+    assert payload["personal_tolerance_note"] == "neutral note"
     with pytest.raises(ValueError):
@@ -243 +269,74 @@
     assert not server.consume_csrf_token(token, now=102.0)
+
+
+def test_unknown_personal_tolerance_does_not_create_personal_row(
+    fixture_db: Path, monkeypatch: pytest.MonkeyPatch
+) -> None:
+    import health_dashboard_action_worker as worker
+
+    monkeypatch.setattr(worker, "DASHBOARD_DB", fixture_db)
+    monkeypatch.setattr(worker, "DASHBOARD_V5_FILE", None)
+    payload = {
+        "version": 1,
+        "action": "nutrition_mapping",
+        "decision": "assign",
+        "queue_key": "synthetic_incomplete_food_mapping_with_a_very_long_name_that_must_wrap",
+        "alias": "SYNTHETIC_INCOMPLETE_FOOD_MAPPING_WITH_A_VERY_LONG_NAME_THAT_MUST_WRAP",
+        "canonical_food": "synthetic reviewed food",
+        "sighi_score": 1,
+        "confidence": "medium",
+        "mapping_method": "manual_review",
+        "source_label": "SIGHi",
+        "source_version": "2026-test",
+        "note": "synthetic reviewed mapping",
+        "ingredient_review_required": False,
+        "personal_tolerance_status": "unknown",
+        "personal_tolerance_note": "",
+    }
+
+    worker.apply_mapping_action(payload, "d" * 32)
+
+    connection = sqlite3.connect(fixture_db)
+    try:
+        assert connection.execute("SELECT COUNT(*) FROM personal_food_tolerance").fetchone()[0] == 0
+        assert connection.execute("SELECT sighi_score FROM nutrition_histamine_scores WHERE item_id=2").fetchone()[0] == 1
+    finally:
+        connection.close()
+
+
+def _raw_tables(connection: sqlite3.Connection) -> dict[str, list[tuple[object, ...]]]:
+    tables = [
+        row[0]
+        for row in connection.execute(
+            "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name"
+        )
+    ]
+    result: dict[str, list[tuple[object, ...]]] = {}
+    for table in tables:
+        result[table] = list(connection.execute(f'SELECT * FROM "{table}" ORDER BY rowid'))
+    return result
+
+
+def test_mapping_schema_migration_restore_from_fixture_backup(tmp_path: Path, fixture_db: Path) -> None:
+    import health_dashboard_action_worker as worker
+
+    copied = tmp_path / "copied-fixture.db"
+    copied.write_bytes(fixture_db.read_bytes())
+    backup_bytes = copied.read_bytes()
+    connection = sqlite3.connect(copied)
+    try:
+        before = _raw_tables(connection)
+        worker.ensure_nutrition_mapping_schema(connection)
+        worker.ensure_nutrition_mapping_schema(connection)
+        connection.commit()
+        assert connection.execute("PRAGMA integrity_check").fetchone()[0] == "ok"
+    finally:
+        connection.close()
+
+    copied.write_bytes(backup_bytes)
+    restored = sqlite3.connect(copied)
+    try:
+        assert restored.execute("PRAGMA integrity_check").fetchone()[0] == "ok"
+        assert _raw_tables(restored) == before
+    finally:
+        restored.close()


Output:
Exit code: 0
Wall time: 0.1 seconds
Output:
Success. Updated the following files:
M tests/test_dashboard_v5_sprint6g_nutrition.py


rg -n "personal_tolerance|personal_tolerance_status|nutrition_mapping.*payload|\"action\": \"nutrition_mapping\"" tests scripts docs -S


python -m py_compile scripts/health/health_dashboard_action_worker.py scripts/health/health_dashboard_server.py tests/test_dashboard_v5_sprint6g_nutrition.py


Output:
Chunk ID: 6c168d
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1712
Output:
scripts/health/health_dashboard_action_worker.py:56:PERSONAL_TOLERANCE_STATUSES = frozenset(
scripts/health/health_dashboard_action_worker.py:129:        "personal_tolerance_status",
scripts/health/health_dashboard_action_worker.py:130:        "personal_tolerance_note",
scripts/health/health_dashboard_action_worker.py:147:    tolerance_status = str(payload["personal_tolerance_status"] or "")
scripts/health/health_dashboard_action_worker.py:148:    tolerance_note = " ".join(str(payload["personal_tolerance_note"] or "").split())
scripts/health/health_dashboard_action_worker.py:169:    if tolerance_status not in PERSONAL_TOLERANCE_STATUSES:
scripts/health/health_dashboard_action_worker.py:182:        "action": "nutrition_mapping",
scripts/health/health_dashboard_action_worker.py:194:        "personal_tolerance_status": tolerance_status,
scripts/health/health_dashboard_action_worker.py:195:        "personal_tolerance_note": tolerance_note,
scripts/health/health_dashboard_action_worker.py:345:            personal_tolerance_status TEXT,
scripts/health/health_dashboard_action_worker.py:346:            personal_tolerance_note TEXT,
scripts/health/health_dashboard_action_worker.py:364:            personal_tolerance_status TEXT,
scripts/health/health_dashboard_action_worker.py:365:            personal_tolerance_note TEXT,
scripts/health/health_dashboard_action_worker.py:392:            "note": "TEXT", "ingredient_review_required": "INTEGER", "personal_tolerance_status": "TEXT",
scripts/health/health_dashboard_action_worker.py:393:            "personal_tolerance_note": "TEXT",
scripts/health/health_dashboard_action_worker.py:399:            "ingredient_review_required": "INTEGER", "personal_tolerance_status": "TEXT",
scripts/health/health_dashboard_action_worker.py:400:            "personal_tolerance_note": "TEXT",
scripts/health/health_dashboard_action_worker.py:518:                if payload["personal_tolerance_status"] != "unknown":
scripts/health/health_dashboard_action_worker.py:530:                            payload["personal_tolerance_status"],
scripts/health/health_dashboard_action_worker.py:531:                            payload["personal_tolerance_note"] or None,
scripts/health/health_dashboard_action_worker.py:544:                        personal_tolerance_status,personal_tolerance_note,definition_version,affected_day)
scripts/health/health_dashboard_action_worker.py:550:                        payload["personal_tolerance_status"], payload["personal_tolerance_note"] or None,
scripts/health/health_dashboard_action_worker.py:558:                    personal_tolerance_status,personal_tolerance_note,definition_version)
scripts/health/health_dashboard_action_worker.py:564:                    int(payload["ingredient_review_required"]), payload["personal_tolerance_status"],
scripts/health/health_dashboard_action_worker.py:565:                    payload["personal_tolerance_note"] or None, HISTAMINE_DEFINITION_VERSION,
scripts/health/health_dashboard_v4.py:394:        personal_tolerances = list(c.execute("SELECT canonical_food,personal_status,evidence_level,notes,updated_at FROM personal_food_tolerance ORDER BY CASE personal_status WHEN 'problematic' THEN 1 WHEN 'unclear' THEN 2 WHEN 'safe' THEN 3 ELSE 4 END, canonical_food"))
scripts/health/health_dashboard_v4.py:658:        for r in personal_tolerances
scripts/health/health_dashboard_server.py:140:PERSONAL_TOLERANCE_STATUSES = {
scripts/health/health_dashboard_server.py:524:        "personal_tolerance_status",
scripts/health/health_dashboard_server.py:525:        "personal_tolerance_note",
scripts/health/health_dashboard_server.py:544:    tolerance_status = form["personal_tolerance_status"][0]
scripts/health/health_dashboard_server.py:545:    tolerance_note = " ".join(form["personal_tolerance_note"][0].split())
scripts/health/health_dashboard_server.py:566:        or tolerance_status not in PERSONAL_TOLERANCE_STATUSES
scripts/health/health_dashboard_server.py:578:        "action": "nutrition_mapping",
scripts/health/health_dashboard_server.py:590:        "personal_tolerance_status": tolerance_status,
scripts/health/health_dashboard_server.py:591:        "personal_tolerance_note": tolerance_note,
tests/test_dashboard_v5_sprint6g_nutrition.py:108:        "action": "nutrition_mapping",
tests/test_dashboard_v5_sprint6g_nutrition.py:120:        "personal_tolerance_status": "documented_tolerated",
tests/test_dashboard_v5_sprint6g_nutrition.py:121:        "personal_tolerance_note": "documented local note",
tests/test_dashboard_v5_sprint6g_nutrition.py:152:        provenance = connection.execute("SELECT decision,alias,confidence,mapping_method,source_label,source_version,note,ingredient_review_required,personal_tolerance_status,personal_tolerance_note,definition_version FROM nutrition_mapping_provenance").fetchone()
tests/test_dashboard_v5_sprint6g_nutrition.py:162:            "personal_tolerance_status": "documented_tolerated",
tests/test_dashboard_v5_sprint6g_nutrition.py:163:            "personal_tolerance_note": "documented local note",
tests/test_dashboard_v5_sprint6g_nutrition.py:209:        "action": "nutrition_mapping",
tests/test_dashboard_v5_sprint6g_nutrition.py:220:        "personal_tolerance_status": "documented_not_tolerated",
tests/test_dashboard_v5_sprint6g_nutrition.py:221:        "personal_tolerance_note": "must not be stored for composite or ignore",
tests/test_dashboard_v5_sprint6g_nutrition.py:258:        "personal_tolerance_status": ["unclear"],
tests/test_dashboard_v5_sprint6g_nutrition.py:259:        "personal_tolerance_note": ["neutral note"],
tests/test_dashboard_v5_sprint6g_nutrition.py:264:    assert payload["personal_tolerance_status"] == "unclear"
tests/test_dashboard_v5_sprint6g_nutrition.py:265:    assert payload["personal_tolerance_note"] == "neutral note"
tests/test_dashboard_v5_sprint6g_nutrition.py:272:def test_unknown_personal_tolerance_does_not_create_personal_row(
tests/test_dashboard_v5_sprint6g_nutrition.py:281:        "action": "nutrition_mapping",
tests/test_dashboard_v5_sprint6g_nutrition.py:293:        "personal_tolerance_status": "unknown",
tests/test_dashboard_v5_sprint6g_nutrition.py:294:        "personal_tolerance_note": "",
scripts/health/health_dashboard_v3.py:293:        personal_tolerances = list(c.execute("SELECT canonical_food,personal_status,evidence_level,notes,updated_at FROM personal_food_tolerance ORDER BY CASE personal_status WHEN 'problematic' THEN 1 WHEN 'unclear' THEN 2 WHEN 'safe' THEN 3 ELSE 4 END, canonical_food"))
scripts/health/health_dashboard_v3.py:544:        for r in personal_tolerances
scripts/health/assets/health-assets/dashboard-v5.js:485:        personalStatus.name = "personal_tolerance_status";
scripts/health/assets/health-assets/dashboard-v5.js:497:        personalNote.name = "personal_tolerance_note";


Output:
Chunk ID: a66c55
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 0
Output:


One compatibility issue is worth fixing inside the same scope: existing databases may have the old `personal_food_tolerance` CHECK that rejects `user_documented`. I’m adding a tiny table rebuild in the worker bootstrap so the new upsert works on copied/legacy DBs without touching any data outside that table.


Input:
*** Begin Patch
*** Update File: scripts/health/health_dashboard_action_worker.py
@@
 def ensure_nutrition_mapping_schema(connection: sqlite3.Connection) -> None:
@@
     for table, columns in {
@@
         for name, declaration in columns.items():
             if name not in existing:
                 connection.execute(f"ALTER TABLE {table} ADD COLUMN {name} {declaration}")
+    personal_sql = connection.execute(
+        "SELECT sql FROM sqlite_master WHERE type='table' AND name='personal_food_tolerance'"
+    ).fetchone()
+    if personal_sql and (
+        "user_documented" not in str(personal_sql["sql"])
+        or "documented_tolerated" not in str(personal_sql["sql"])
+    ):
+        connection.executescript(
+            """
+            ALTER TABLE personal_food_tolerance RENAME TO personal_food_tolerance_legacy;
+            CREATE TABLE personal_food_tolerance (
+                canonical_food TEXT PRIMARY KEY,
+                personal_status TEXT NOT NULL DEFAULT 'unknown'
+                    CHECK(personal_status IN ('unknown','documented_tolerated','documented_not_tolerated','unclear')),
+                evidence_level TEXT DEFAULT 'user_documented'
+                    CHECK(evidence_level IN ('low','medium','high','user_documented')),
+                notes TEXT,
+                updated_at TEXT DEFAULT CURRENT_TIMESTAMP
+            );
+            INSERT OR REPLACE INTO personal_food_tolerance
+                (canonical_food,personal_status,evidence_level,notes,updated_at)
+            SELECT canonical_food,
+                   CASE
+                     WHEN personal_status IN ('unknown','documented_tolerated','documented_not_tolerated','unclear')
+                     THEN personal_status ELSE 'unknown'
+                   END,
+                   CASE
+                     WHEN evidence_level IN ('low','medium','high','user_documented')
+                     THEN evidence_level ELSE 'user_documented'
+                   END,
+                   notes,updated_at
+              FROM personal_food_tolerance_legacy;
+            DROP TABLE personal_food_tolerance_legacy;
+            """
+        )
*** End Patch


@@ -407,2 +407,37 @@
                 connection.execute(f"ALTER TABLE {table} ADD COLUMN {name} {declaration}")
+    personal_sql = connection.execute(
+        "SELECT sql FROM sqlite_master WHERE type='table' AND name='personal_food_tolerance'"
+    ).fetchone()
+    if personal_sql and (
+        "user_documented" not in str(personal_sql["sql"])
+        or "documented_tolerated" not in str(personal_sql["sql"])
+    ):
+        connection.executescript(
+            """
+            ALTER TABLE personal_food_tolerance RENAME TO personal_food_tolerance_legacy;
+            CREATE TABLE personal_food_tolerance (
+                canonical_food TEXT PRIMARY KEY,
+                personal_status TEXT NOT NULL DEFAULT 'unknown'
+                    CHECK(personal_status IN ('unknown','documented_tolerated','documented_not_tolerated','unclear')),
+                evidence_level TEXT DEFAULT 'user_documented'
+                    CHECK(evidence_level IN ('low','medium','high','user_documented')),
+                notes TEXT,
+                updated_at TEXT DEFAULT CURRENT_TIMESTAMP
+            );
+            INSERT OR REPLACE INTO personal_food_tolerance
+                (canonical_food,personal_status,evidence_level,notes,updated_at)
+            SELECT canonical_food,
+                   CASE
+                     WHEN personal_status IN ('unknown','documented_tolerated','documented_not_tolerated','unclear')
+                     THEN personal_status ELSE 'unknown'
+                   END,
+                   CASE
+                     WHEN evidence_level IN ('low','medium','high','user_documented')
+                     THEN evidence_level ELSE 'user_documented'
+                   END,
+                   notes,updated_at
+              FROM personal_food_tolerance_legacy;
+            DROP TABLE personal_food_tolerance_legacy;
+            """
+        )


Output:
Exit code: 0
Wall time: 0.1 seconds
Output:
Success. Updated the following files:
M scripts/health/health_dashboard_action_worker.py


I caught a row-factory assumption in that bootstrap migration and am making it work for plain SQLite cursors too.


Input:
*** Begin Patch
*** Update File: scripts/health/health_dashboard_action_worker.py
@@
     personal_sql = connection.execute(
         "SELECT sql FROM sqlite_master WHERE type='table' AND name='personal_food_tolerance'"
     ).fetchone()
+    personal_ddl = str(personal_sql["sql"] if isinstance(personal_sql, sqlite3.Row) else personal_sql[0]) if personal_sql else ""
     if personal_sql and (
-        "user_documented" not in str(personal_sql["sql"])
-        or "documented_tolerated" not in str(personal_sql["sql"])
+        "user_documented" not in personal_ddl
+        or "documented_tolerated" not in personal_ddl
     ):
*** End Patch


@@ -410,5 +410,6 @@
     ).fetchone()
+    personal_ddl = str(personal_sql["sql"] if isinstance(personal_sql, sqlite3.Row) else personal_sql[0]) if personal_sql else ""
     if personal_sql and (
-        "user_documented" not in str(personal_sql["sql"])
-        or "documented_tolerated" not in str(personal_sql["sql"])
+        "user_documented" not in personal_ddl
+        or "documented_tolerated" not in personal_ddl
     ):


Output:
Exit code: 0
Wall time: 0.1 seconds
Output:
Success. Updated the following files:
M scripts/health/health_dashboard_action_worker.py


Input:
*** Begin Patch
*** Update File: docs/adr/0003-dietary-inflammation-model.md
@@
 Technical inventory from the Sprint 6G-A fixture/audit, abstracted without product names:

+- production technical inventory counts: 24 nutrition days, 257 nutrition items;
+- meal item/day coverage: breakfast 66 items across 21 days, dinner 64 items across 16 days, lunch 96 items across 19 days, snack 31 items across 12 days, unknown meals 0;
+- unclassified histamine mappings: 40 items, 15.6%;
+- known-unit component day coverage: fiber 24 days, sugar 24 days, saturated fat 24 days, sodium 23 days;
 - nutrition item records: present, with meal, date, item label, imported macros, and source-local product identifiers;
 - item nutrient rows: present for selected macro and nutrient keys, with mixed unit coverage;
 - histamine mapping rows: present for mapped and unmapped item states;
*** End Patch


Success. Updated the following files:
M docs/adr/0003-dietary-inflammation-model.md

@@ -14,2 +14,6 @@

+- production technical inventory counts: 24 nutrition days, 257 nutrition items;
+- meal item/day coverage: breakfast 66 items across 21 days, dinner 64 items across 16 days, lunch 96 items across 19 days, snack 31 items across 12 days, unknown meals 0;
+- unclassified histamine mappings: 40 items, 15.6%;
+- known-unit component day coverage: fiber 24 days, sugar 24 days, saturated fat 24 days, sodium 23 days;
 - nutrition item records: present, with meal, date, item label, imported macros, and source-local product identifiers;


Output:
Exit code: 0
Wall time: 0.1 seconds
Output:
Success. Updated the following files:
M docs/adr/0003-dietary-inflammation-model.md


Implementation patches are in. I’m running only the Sprint 6G focused tests plus syntax/static checks now, and I’ll fix any focused failures without expanding scope.


python -m pytest tests/test_dashboard_v5_sprint6g_nutrition.py -q


Output:
Chunk ID: f94274
Wall time: 14.7396 seconds
Process exited with code 1
Original token count: 479
Output:
.......F                                                                 [100%]
=================================== FAILURES ===================================
__________ test_mapping_schema_migration_restore_from_fixture_backup ___________

tmp_path = PosixPath('/tmp/pytest-of-agent/pytest-740/test_mapping_schema_migration_0')
fixture_db = PosixPath('/tmp/pytest-of-agent/pytest-740/test_mapping_schema_migration_0/fixture.db')

    def test_mapping_schema_migration_restore_from_fixture_backup(tmp_path: Path, fixture_db: Path) -> None:
        import health_dashboard_action_worker as worker

        copied = tmp_path / "copied-fixture.db"
        copied.write_bytes(fixture_db.read_bytes())
        backup_bytes = copied.read_bytes()
        connection = sqlite3.connect(copied)
        try:
            before = _raw_tables(connection)
>           worker.ensure_nutrition_mapping_schema(connection)

tests/test_dashboard_v5_sprint6g_nutrition.py:329:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
scripts/health/health_dashboard_action_worker.py:404: in ensure_nutrition_mapping_schema
    existing = {row["name"] for row in connection.execute(f"PRAGMA table_info({table})")}
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

.0 = <sqlite3.Cursor object at 0x76674d5473c0>

>   existing = {row["name"] for row in connection.execute(f"PRAGMA table_info({table})")}
                ^^^^^^^^^^^
E   TypeError: tuple indices must be integers or slices, not str

scripts/health/health_dashboard_action_worker.py:404: TypeError
=========================== short test summary info ============================
FAILED tests/test_dashboard_v5_sprint6g_nutrition.py::test_mapping_schema_migration_restore_from_fixture_backup
1 failed, 7 passed in 14.52s


Output:
Chunk ID: c11484
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 0
Output:


Output:
Chunk ID: d25713
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 0
Output: