You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Every material in libdedx carries one tabulated density (service.getDensity(materialId), src/lib/wasm/libdedx.ts's _dedx_get_density), and today that's the only density aidedx ever
uses. It's what turns libdedx's native mass-normalized numbers (MeV·cm²/g stopping power,
g/cm² CSDA range) into the physical, length-normalized numbers people actually ask for
(keV/µm, a range in cm) — see stoppingPowerToKevPerUm() / csdaRangeToCm() in src/lib/format.ts
and their call sites in src/lib/nlg/render.ts and (mirrored) AnswerFormatter.kt.
The tabulated value is a reasonable default (e.g. water = 1.0 g/cm³, PMMA ≈ 1.19 g/cm³), but real
dosimetry and treatment-planning work routinely deals with materials whose actual density
differs from the NIST reference value:
Water isn't always at reference density. Room-temperature liquid water is ~0.998 g/cm³, not
the tabulated 1.0; some workflows care about that half-percent.
Snow, ice, and packed powders vary enormously with compaction — loose snow ~0.1–0.3 g/cm³,
packed snow ~0.4–0.6 g/cm³, ice ~0.92 g/cm³ — and none of that is "water" in libdedx's tables.
Compressed or rarefied gases (air in a range-shifter or ion chamber at non-standard pressure)
scale density roughly linearly with pressure; the tabulated "Air (dry, near sea level)" density
is only right at 1 atm.
3D-printed phantom materials (PLA, ABS) have an effective density well below the bulk
polymer's, set by print infill percentage — a phantom printed at 60% infill is not the same
density as solid PMMA/PLA.
Tissue-equivalent gel dosimeters and custom mixes are often batch-mixed to a target density
that's close to, but not exactly, the reference value in the closest tabulated material.
In every one of these cases the person already knows the density they want to use — they measured
it, mixed it, or set the pressure themselves — and today has no way to tell aidedx that short of
picking a different (wrong) material or not using voice at all. This issue proposes letting a
spoken query carry that density explicitly: "water with a density of 0.99 grams per cubic
centimeter."
Example sentences / use cases
Direct, single-quantity queries:
"What's the range of 150 MeV protons in water with a density of 0.99 grams per cubic
centimeter?"
"Stopping power of a 60 MeV proton in PMMA, but use a density of 1.15 g/cm3 instead of the
default."
"How far does a 200 MeV carbon ion go in snow at a density of 0.35 grams per cc?"
"Range of 70 MeV protons in air compressed to 0.0024 g/cm3."
"What is the dE/dx of a 100 MeV proton in ice, density 0.92?"
"Stopping power of a 4 MeV alpha particle in 3D-printed PLA, density 1.1."
"What's the CSDA range of 250 MeV protons in tissue-equivalent plastic at 1.127 g per cm3?"
Inverse queries (density needed to convert the given length/rate target, not just the answer —
this is the case rangeTargetToGcm2() / stpTargetToMassUnits() in src/lib/compute/compute.ts
already has a ComputeError for when density is missing):
"What energy proton gives a 10 cm range in a gel dosimeter with density 1.02 g/cm3?"
"What energy carbon ion gives 8 keV/µm in water at 0.998 g/cm3?"
Unit-variant and spelled-out phrasing (ASR realistically produces both):
"Water with a density of zero point nine nine eight grams per cubic centimeter" (spelled-out
decimal — same NUMBER_PREFIX_SRC spelled-number path issue Screenshot (Jul 29, 2026 22:13:57) #156 already built for energies).
"Density of one thousand kilograms per cubic meter" (kg/m³ → ÷1000 → g/cm³).
"Aluminum at a density of 2.7 grams per milliliter" (g/mL is numerically identical to g/cm³).
Adversarial / edge cases the matcher and schema must handle gracefully:
"Water with a density of 0 g/cm3" — same treatment; would also divide-by-zero downstream.
"Aluminum with a density of 2.7 g/cm3" — override happens to equal the tabulated value; should
parse fine and just be a no-op numerically (still worth asserting it doesn't crash or
double-count an assumption note).
Explicitly out of scope for v1 (see "Open questions" below):
"If the water were half as dense, what would the range be?" — relative/comparative density,
needs a base value to scale from.
"Compare the range in water at 1.0 versus water at 0.92 g/cm3" — two densities for the same
material in one query; today's material dedup (dedupeByKey keyed on resolved material id in matcher.ts) would collapse these into one slot. Flagged as a real limitation below, not
silently mishandled.
What changes physically, and what doesn't
This has to be described honestly to whoever reads the rendered answer, so it's worth being
precise about what a density override actually does to the computation:
Mass stopping power (MeV·cm²/g) is unchanged. libdedx's tabulated stopping-power values
already are the density-independent quantity (to the accuracy of the tables); nothing about the
Bethe-Bloch/tabulated calculation is re-run.
What changes is purely the mass ⇄ length conversion layer: keV/µm display
(stoppingPowerToKevPerUm), CSDA range in cm (csdaRangeToCm), and — for inverse queries — the
reverse direction, converting a spoken length/MeV-per-cm target into the g/cm²/MeV·cm²/g units
libdedx's inverse lookups need (rangeTargetToGcm2, stpTargetToMassUnits).
The density-effect correction inside the Bethe-Bloch formula itself is not recomputed.
libdedx's tabulated values bake in the density-effect correction (Sternheimer parameters) for the tabulated density. At densities far from that reference (e.g. a compressed gas at high
pressure), this is a real, known approximation error — small for condensed matter swapped between
nearby densities (0.92 vs. 1.0 for ice/water), potentially not small for gases compressed by a
large factor. The rendered answer's assumption note (below) should say this plainly so the number
isn't mistaken for a from-scratch recomputation.
This scope is exactly why the feature is cheap to build: it's a conversion-layer parameter, not a
new physics computation, and it only touches code that already threads a density: number value
through both stacks (ComputeSeries.density on web, the densityGramPerCm3: Float? parameter of AnswerFormatter.format() on Android) — the override just needs to out-rank service.getDensity()
/ LibdedxBridge.getDensity() as the source of that same value.
Schema change (QueryIntent)
Add an optional field to MaterialSlot in src/lib/intent/query-intent.ts:
exportinterfaceMaterialSlot{match: string;/** * Spoken density override in g/cm³, replacing libdedx's tabulated * `getDensity()` value for this material in unit conversions only (see * "What changes physically" — never the tabulated mass stopping power * itself). Already canonicalized to g/cm³ by the matcher; the schema * doesn't need to carry the original spoken unit. */densityOverride?: number;}
Scoped to MaterialSlot (not a top-level intent field) because the spoken phrase binds locally to
one material ("water with a density of…"), and it keeps the door open for per-material overrides
in a compareDim: "material" query (e.g. comparing water at reference density against a
compressed-gas material at its own overridden density — a real, in-scope case, distinct from the
two-densities-of-the-same-material case flagged as out of scope above).
validateQueryIntent() needs one new check: densityOverride, when present, must be a positive
finite number (reuse the existing isPositiveFiniteNumber helper — the same rule already applied
to energies, issue #42 §5).
Add "density-override" to EVAL_TAGS in the same file, for tagging new eval examples.
Web implementation plan
Matcher grammar (src/lib/intent/matcher.ts)
Add a density grammar parallel to the existing LENGTH_TARGET_RE / STP_TARGET_RES patterns
(§"2. Energies / target" in the matcher's pipeline comment, matcher.ts lines ~470–515):
Runs after particle/energy/target extraction and before the material n-gram scan, so its
matched span (the whole "with a density of 0.99 g/cm3" clause) is added to consumedSpans and
never mis-parsed as a material or number elsewhere — same ordering rationale already documented
for particles/energies at matcher.ts's "2. Particles and energies/target first" comment.
Normalizes the captured unit to g/cm³ at parse time: g/cm3/g/cc/g/mL/spelled-out grams
variants pass through as-is; kg/m3/spelled-out kilograms variants divide by 1000.
Spelled-out decimals ("zero point nine nine eight") are already handled upstream by spellOutNumbers() (issue Screenshot (Jul 29, 2026 22:13:57) #156) before this regex ever runs, since matchIntent() runs that
normalization pass over the whole query first.
Because there's no existing "phrase that names both a material and a density" coordination
logic, keep v1 to one density clause per query, attached to the first (or only) material
slot that appears in the query. Attaching a spoken density to a specific material in a
multi-material comparison (in-scope but harder: "water" and "compressed air, density 0.002" in
the same sentence) is a stretch goal, not v1 — flagged below.
compute.ts wiring
forwardSeries() and inverseSeries() currently call service.getDensity(material.id)
unconditionally (compute.ts lines ~436 and ~473). Both need an optional override parameter that
takes priority:
computeIntent()'s three branches (compareDim: "material", "particle"/"program", and the "none"/"energy" fallthrough) all resolve a MaterialSlot right before calling build(...) —
thread m.densityOverride (or reqFirst(intent.materials, ...).densityOverride) through to forwardSeries/inverseSeries alongside the already-resolved ResolvedMaterial.
Add an assumption note whenever an override is used, mirroring the existing assumptions.push(...) pattern for isotope/per-nucleon assumptions:
if(override!==undefined){assumptions.push(`${material.name}: using spoken density ${override} g/cm³ (tabulated value is ${tabulated??"unavailable"} g/cm³)`,);}
This is also where the honesty note from "What changes physically" belongs — either folded into
that assumption string or appended once per answer, not per series, to avoid repeating it in every
row of a comparison.
NLG (src/lib/nlg/render.ts)
No structural change needed — valueText() already converts using whatever series.density holds
(render.ts line ~103), and it doesn't care whether that number came from getDensity() or an
override. The override surfaces purely through the assumptions line already rendered as Note: … (render.ts line ~217-218) via the assumption text above.
Eval set (eval/intents.jsonl)
Add a handful of examples covering the sentences above (direct/unit-variant/adversarial), tagged "density-override" (new tag) plus the existing quantity/phrasing tags, e.g.:
{
"id": "dns-001",
"text": "What's the range of 150 MeV protons in water with a density of 0.99 grams per cubic centimeter?",
"expected": {
"quantity": "csdaRange", "compareDim": "none",
"particles": [{ "match": "protons" }],
"materials": [{ "match": "water", "densityOverride": 0.99 }],
"energies": [{ "value": 150, "unit": "MeV" }],
"assumptions": [], "confidence": 1
},
"tags": ["direct", "quantity-csda-range", "single", "unit-MeV", "density-override"]
}
The two adversarial examples (non-positive density) should assert the override is absent from expected (dropped, not clamped) — same convention as the existing negative-energy eval examples.
Run pnpm coverage:intents after adding these, per the existing workflow.
Android implementation plan
The Kotlin matcher (bench/android/full-app/app/src/main/java/com/aidedx/fullapp/nlu/ KotlinMatcher.kt) is a deliberately scoped-down subset of matcher.ts — single
particle+material+energy, stoppingPower/csdaRange only, no compareDim, no inverse queries/ target (see the file's own header comment). That scoping actually makes this feature simpler
to port than most: there's no inverse-query density dependency to worry about yet, only the
forward-answer keV/µm and cm conversions AnswerFormatter.kt already parameterizes on density.
Plan:
KotlinMatcher.kt — add a DENSITY_RE mirroring the web pattern above (Kotlin Regex,
same g/cm³ + kg/m³ + spelled-unit coverage), and a densityOverride: Float? field on MatchedIntent (currently: quantity, particleMatch, particleId, massNumber, materialMatch, materialId, energy — no target/density field exists yet). Consume the
matched span before materialMatch is resolved, same ordering as web.
MainActivity.kt — at the existing call site (val density = …; AnswerFormatter.format(matched, stp, csda, density),
around line 253–278), prefer matched.densityOverride over LibdedxBridge.getDensity(materialId): val density = matched.densityOverride ?: LibdedxBridge.getDensity(matched.materialId).
AnswerFormatter.kt — no change needed; it already only consumes a nullable densityGramPerCm3: Float? and doesn't care about its source (same "no structural NLG change"
situation as web). Consider surfacing the override in the result line's text, matching whatever
the web assumption note lands on, so the two apps stay behaviorally consistent.
KotlinMatcherAgreementTest.kt / KotlinMatcherTest.kt — add unit tests for the new regex
(parse + reject-non-positive), and extend the agreement harness with density-override examples
once the corresponding eval/intents.jsonl entries exist, following the same "measure
agreement, don't assume parity" discipline docs/android-full-app-spike.md §4 established for
the rest of the matcher.
Not needed on Android for v1: any change to LibdedxBridge.kt / the JNI layer — nativeGetDensity stays exactly as-is; the override never touches it, same as compute.ts never
touching service.getDensity()'s implementation on web.
Open questions / risks
Per-material density in a multi-material comparison (water override vs. a second material's
own override in one compareDim: "material" query) is schema-compatible (densityOverride
lives on each MaterialSlot) but the matcher grammar above only attaches one parsed density
clause to the first material found — extending that to correctly associate multiple density
clauses with multiple materials in one sentence is real NLU work, not a mechanical follow-on.
Comparing two densities of the same material is blocked by dedupeByKey's current key
(resolved.id alone, matcher.ts) — it would need to key on (id, densityOverride) instead,
which has knock-on effects on decideCompareDim (would a density-only variation count as a
"material" comparison, or need a new compareDim value?). Worth a follow-up issue rather than
folding into v1.
Plausibility bounds. Should a wildly implausible spoken density (e.g. "water at 50 g/cm3")
be rejected outright, clamped, or passed through with a stronger warning than the assumption
note? v1's plan above is "parse anything positive, let the assumption note carry the honesty
burden" — deliberately not inventing a plausibility table per material, but flagged here since
it's a real product decision, not an oversight.
Relative/comparative density phrasing ("half as dense", "twice the density of ice") — needs
a base value to scale from and isn't covered by the direct value+unit grammar above at all; out
of scope.
Implementation checklist
query-intent.ts: add MaterialSlot.densityOverride, validator check, "density-override"
eval tag
pnpm run format:check && pnpm run lint && pnpm run check && pnpm run validate:eval && pnpm test && pnpm build
before opening a PR, per CLAUDE.md's CI gates
Motivation
Every material in libdedx carries one tabulated density (
service.getDensity(materialId),src/lib/wasm/libdedx.ts's_dedx_get_density), and today that's the only density aidedx everuses. It's what turns libdedx's native mass-normalized numbers (MeV·cm²/g stopping power,
g/cm² CSDA range) into the physical, length-normalized numbers people actually ask for
(keV/µm, a range in cm) — see
stoppingPowerToKevPerUm()/csdaRangeToCm()insrc/lib/format.tsand their call sites in
src/lib/nlg/render.tsand (mirrored)AnswerFormatter.kt.The tabulated value is a reasonable default (e.g. water = 1.0 g/cm³, PMMA ≈ 1.19 g/cm³), but real
dosimetry and treatment-planning work routinely deals with materials whose actual density
differs from the NIST reference value:
the tabulated 1.0; some workflows care about that half-percent.
packed snow ~0.4–0.6 g/cm³, ice ~0.92 g/cm³ — and none of that is "water" in libdedx's tables.
scale density roughly linearly with pressure; the tabulated "Air (dry, near sea level)" density
is only right at 1 atm.
polymer's, set by print infill percentage — a phantom printed at 60% infill is not the same
density as solid PMMA/PLA.
that's close to, but not exactly, the reference value in the closest tabulated material.
In every one of these cases the person already knows the density they want to use — they measured
it, mixed it, or set the pressure themselves — and today has no way to tell aidedx that short of
picking a different (wrong) material or not using voice at all. This issue proposes letting a
spoken query carry that density explicitly: "water with a density of 0.99 grams per cubic
centimeter."
Example sentences / use cases
Direct, single-quantity queries:
centimeter?"
default."
Inverse queries (density needed to convert the given length/rate target, not just the answer —
this is the case
rangeTargetToGcm2()/stpTargetToMassUnits()insrc/lib/compute/compute.tsalready has a
ComputeErrorfor when density is missing):Unit-variant and spelled-out phrasing (ASR realistically produces both):
decimal — same
NUMBER_PREFIX_SRCspelled-number path issue Screenshot (Jul 29, 2026 22:13:57) #156 already built for energies).Adversarial / edge cases the matcher and schema must handle gracefully:
negative-energy handling (issue [polish] First-user-testing feedback: units, error messages, download modal, copy, docs, UI polish #42 §5), not silently used.
parse fine and just be a no-op numerically (still worth asserting it doesn't crash or
double-count an assumption note).
Explicitly out of scope for v1 (see "Open questions" below):
needs a base value to scale from.
material in one query; today's material dedup (
dedupeByKeykeyed on resolved material id inmatcher.ts) would collapse these into one slot. Flagged as a real limitation below, notsilently mishandled.
What changes physically, and what doesn't
This has to be described honestly to whoever reads the rendered answer, so it's worth being
precise about what a density override actually does to the computation:
already are the density-independent quantity (to the accuracy of the tables); nothing about the
Bethe-Bloch/tabulated calculation is re-run.
(
stoppingPowerToKevPerUm), CSDA range in cm (csdaRangeToCm), and — for inverse queries — thereverse direction, converting a spoken length/MeV-per-cm target into the g/cm²/MeV·cm²/g units
libdedx's inverse lookups need (
rangeTargetToGcm2,stpTargetToMassUnits).libdedx's tabulated values bake in the density-effect correction (Sternheimer parameters) for the
tabulated density. At densities far from that reference (e.g. a compressed gas at high
pressure), this is a real, known approximation error — small for condensed matter swapped between
nearby densities (0.92 vs. 1.0 for ice/water), potentially not small for gases compressed by a
large factor. The rendered answer's assumption note (below) should say this plainly so the number
isn't mistaken for a from-scratch recomputation.
This scope is exactly why the feature is cheap to build: it's a conversion-layer parameter, not a
new physics computation, and it only touches code that already threads a
density: numbervaluethrough both stacks (
ComputeSeries.densityon web, thedensityGramPerCm3: Float?parameter ofAnswerFormatter.format()on Android) — the override just needs to out-rankservice.getDensity()/
LibdedxBridge.getDensity()as the source of that same value.Schema change (
QueryIntent)Add an optional field to
MaterialSlotinsrc/lib/intent/query-intent.ts:Scoped to
MaterialSlot(not a top-level intent field) because the spoken phrase binds locally toone material ("water with a density of…"), and it keeps the door open for per-material overrides
in a
compareDim: "material"query (e.g. comparing water at reference density against acompressed-gas material at its own overridden density — a real, in-scope case, distinct from the
two-densities-of-the-same-material case flagged as out of scope above).
validateQueryIntent()needs one new check:densityOverride, when present, must be a positivefinite number (reuse the existing
isPositiveFiniteNumberhelper — the same rule already appliedto energies, issue #42 §5).
Add
"density-override"toEVAL_TAGSin the same file, for tagging new eval examples.Web implementation plan
Matcher grammar (
src/lib/intent/matcher.ts)Add a density grammar parallel to the existing
LENGTH_TARGET_RE/STP_TARGET_RESpatterns(§"2. Energies / target" in the matcher's pipeline comment,
matcher.tslines ~470–515):matched span (the whole "with a density of 0.99 g/cm3" clause) is added to
consumedSpansandnever mis-parsed as a material or number elsewhere — same ordering rationale already documented
for particles/energies at
matcher.ts's "2. Particles and energies/target first" comment.g/cm3/g/cc/g/mL/spelled-out gramsvariants pass through as-is;
kg/m3/spelled-out kilograms variants divide by 1000.spellOutNumbers()(issue Screenshot (Jul 29, 2026 22:13:57) #156) before this regex ever runs, sincematchIntent()runs thatnormalization pass over the whole query first.
≤ 0 is dropped rather than filled in, exactly like
negativeEnergySpans— the span still getsexcluded from material matching so a nonsense number doesn't leak into the material scan.
logic, keep v1 to one density clause per query, attached to the first (or only) material
slot that appears in the query. Attaching a spoken density to a specific material in a
multi-material comparison (in-scope but harder: "water" and "compressed air, density 0.002" in
the same sentence) is a stretch goal, not v1 — flagged below.
compute.tswiringforwardSeries()andinverseSeries()currently callservice.getDensity(material.id)unconditionally (
compute.tslines ~436 and ~473). Both need an optional override parameter thattakes priority:
computeIntent()'s three branches (compareDim: "material","particle"/"program", and the"none"/"energy"fallthrough) all resolve aMaterialSlotright before callingbuild(...)—thread
m.densityOverride(orreqFirst(intent.materials, ...).densityOverride) through toforwardSeries/inverseSeriesalongside the already-resolvedResolvedMaterial.Add an assumption note whenever an override is used, mirroring the existing
assumptions.push(...)pattern for isotope/per-nucleon assumptions:This is also where the honesty note from "What changes physically" belongs — either folded into
that assumption string or appended once per answer, not per series, to avoid repeating it in every
row of a comparison.
NLG (
src/lib/nlg/render.ts)No structural change needed —
valueText()already converts using whateverseries.densityholds(
render.tsline ~103), and it doesn't care whether that number came fromgetDensity()or anoverride. The override surfaces purely through the assumptions line already rendered as
Note: …(render.tsline ~217-218) via the assumption text above.Eval set (
eval/intents.jsonl)Add a handful of examples covering the sentences above (direct/unit-variant/adversarial), tagged
"density-override"(new tag) plus the existing quantity/phrasing tags, e.g.:{ "id": "dns-001", "text": "What's the range of 150 MeV protons in water with a density of 0.99 grams per cubic centimeter?", "expected": { "quantity": "csdaRange", "compareDim": "none", "particles": [{ "match": "protons" }], "materials": [{ "match": "water", "densityOverride": 0.99 }], "energies": [{ "value": 150, "unit": "MeV" }], "assumptions": [], "confidence": 1 }, "tags": ["direct", "quantity-csda-range", "single", "unit-MeV", "density-override"] }The two adversarial examples (non-positive density) should assert the override is absent from
expected(dropped, not clamped) — same convention as the existing negative-energy eval examples.Run
pnpm coverage:intentsafter adding these, per the existing workflow.Android implementation plan
The Kotlin matcher (
bench/android/full-app/app/src/main/java/com/aidedx/fullapp/nlu/ KotlinMatcher.kt) is a deliberately scoped-down subset ofmatcher.ts— singleparticle+material+energy,
stoppingPower/csdaRangeonly, nocompareDim, no inverse queries/target(see the file's own header comment). That scoping actually makes this feature simplerto port than most: there's no inverse-query density dependency to worry about yet, only the
forward-answer keV/µm and cm conversions
AnswerFormatter.ktalready parameterizes on density.Plan:
KotlinMatcher.kt— add aDENSITY_REmirroring the web pattern above (KotlinRegex,same g/cm³ + kg/m³ + spelled-unit coverage), and a
densityOverride: Float?field onMatchedIntent(currently:quantity,particleMatch,particleId,massNumber,materialMatch,materialId,energy— no target/density field exists yet). Consume thematched span before
materialMatchis resolved, same ordering as web.MainActivity.kt— at the existing call site (val density = …; AnswerFormatter.format(matched, stp, csda, density),around line 253–278), prefer
matched.densityOverrideoverLibdedxBridge.getDensity(materialId):val density = matched.densityOverride ?: LibdedxBridge.getDensity(matched.materialId).AnswerFormatter.kt— no change needed; it already only consumes a nullabledensityGramPerCm3: Float?and doesn't care about its source (same "no structural NLG change"situation as web). Consider surfacing the override in the result line's text, matching whatever
the web assumption note lands on, so the two apps stay behaviorally consistent.
KotlinMatcherAgreementTest.kt/KotlinMatcherTest.kt— add unit tests for the new regex(parse + reject-non-positive), and extend the agreement harness with density-override examples
once the corresponding
eval/intents.jsonlentries exist, following the same "measureagreement, don't assume parity" discipline
docs/android-full-app-spike.md§4 established forthe rest of the matcher.
Not needed on Android for v1: any change to
LibdedxBridge.kt/ the JNI layer —nativeGetDensitystays exactly as-is; the override never touches it, same ascompute.tsnevertouching
service.getDensity()'s implementation on web.Open questions / risks
own override in one
compareDim: "material"query) is schema-compatible (densityOverridelives on each
MaterialSlot) but the matcher grammar above only attaches one parsed densityclause to the first material found — extending that to correctly associate multiple density
clauses with multiple materials in one sentence is real NLU work, not a mechanical follow-on.
dedupeByKey's current key(
resolved.idalone,matcher.ts) — it would need to key on(id, densityOverride)instead,which has knock-on effects on
decideCompareDim(would a density-only variation count as a"material" comparison, or need a new
compareDimvalue?). Worth a follow-up issue rather thanfolding into v1.
be rejected outright, clamped, or passed through with a stronger warning than the assumption
note? v1's plan above is "parse anything positive, let the assumption note carry the honesty
burden" — deliberately not inventing a plausibility table per material, but flagged here since
it's a real product decision, not an oversight.
a base value to scale from and isn't covered by the direct value+unit grammar above at all; out
of scope.
Implementation checklist
query-intent.ts: addMaterialSlot.densityOverride, validator check,"density-override"eval tag
matcher.ts:DENSITY_RE, span consumption ordering, non-positive rejectioncompute.ts: override-aware density resolution inforwardSeries/inverseSeries,assumption note
eval/intents.jsonl: examples above (direct, unit-variant, adversarial),pnpm coverage:intentsre-runKotlinMatcher.kt:DENSITY_RE,MatchedIntent.densityOverrideMainActivity.kt: prefer override overLibdedxBridge.getDensity()pnpm run format:check && pnpm run lint && pnpm run check && pnpm run validate:eval && pnpm test && pnpm buildbefore opening a PR, per
CLAUDE.md's CI gates