-- Standalone Lua 5.1+; policy/source checks, no engine and no network. local target=assert(arg[1],"species_effects.lua path required") local root=target:gsub("[^/\\]+$","") local hearth={} hearth.lifecycle=dofile(root.."lifecycle.lua") hearth.botany=dofile(root.."botany.lua")(hearth,{}) hearth.wiki_articles=dofile(root.."wiki_articles.lua") local E=dofile(target)(hearth,setmetatable({}, {__index=function(_,k) error("unexpected API access: "..k) end})) local passed=0 local function test(label,fn) fn(); passed=passed+1; print("ok "..passed.." - "..label) end local function near(a,b) assert(math.abs(a-b)<0.000001,tostring(a).." != "..tostring(b)) end local function entry(value,title) for _,row in ipairs(E.describe(value)) do if row.title==title then return row end end error("missing effect "..title) end test("all100 identity sources are mapped without claiming unreviewed trait evidence",function() local sources=E.sources(); local total=0 for _,r in ipairs(hearth.botany.list()) do local s=assert(sources[r.id]); total=total+1 assert(s.source_species==r.latin and s.source_url:match("^https://en%.wikipedia%.org/wiki/")) local article=hearth.wiki_articles[r.id] assert(s.identity_revision==(article and article.revision)) assert(s.identity_revision_url==(article and article.revision_url)) if not s.reviewed then assert(s.note:find("no species%-effect evidence reviewed")) end for _,row in ipairs(E.describe(r)) do assert(row.status=="simulated" or row.status=="descriptive") assert(row.game~="" and row.source_species==r.latin and row.source_url==s.source_url) end end assert(total==100) assert(E.source("unknown")==nil and #E.describe({id="unknown",tags={engineer=true}})==0) end) test("reviewed mechanisms cite species and explicitly identify broader evidence",function() for _,id in ipairs({"alder","clover","dog_lichen","sphagnum","duckweed","birch_polypore","fly_agaric"}) do local s=E.source(id); assert(s.reviewed and s.reviewed_on=="2026-11-03" and s.evidence_url) end assert(E.source("sphagnum").evidence_url:match("/Sphagnum$")) assert(E.source("duckweed").evidence_url:match("/Lemnoideae$")) assert(E.source("dog_lichen").evidence_url:match("/Peltigera$")) assert(entry("fly_agaric","ectomycorrhiza").game:find("debits at most 1 existing soil nutrient")) end) test("descriptors do not silently become engineer, parasite, resin or poison mechanics",function() for _,pair in ipairs({{"willow","engineer"},{"bracken","allelopathy"},{"honey_fungus","parasite"},{"pine","resin"},{"foxglove","toxicity"}}) do assert(entry(pair[1],pair[2]).status=="descriptive") end for _,id in ipairs({"alder","clover","dog_lichen"}) do assert(entry(id,"nitrogenfixer").status=="simulated") end assert(entry("pine","canopy").game:find("15%%")) assert(entry("birch_polypore","saprotroph").status=="simulated") assert(entry("yarrow","pollinator").status=="simulated") end) test("sphagnum changes only a fresh local land estimate with two-mat quantitative ceilings",function() local h={loaded=true,water=0,aquatic="bank",moisture=0.65,ph=6.5,light=10,nutrients=3,nested={value=1}} local out=E.habitat(h,{sphagnum=10000,duckweed=10000,willow=10000}) near(out.moisture,0.85); near(out.ph,6.1); assert(out.light==10 and out.nutrients==3) assert(h.ph==6.5 and h.moisture==0.65 and out.source_ph==6.5 and out.source_moisture==0.65) out.nested.value=2; assert(h.nested.value==1) for _=1,20 do near(E.habitat(h,{sphagnum=2}).ph,6.1) end out=E.habitat({loaded=true,water=0,moisture=0.99,ph=3.6},{sphagnum=2}) assert(out.moisture==1 and out.ph==3.5) local cowslip={loaded=true,ground="grass",substrate="grass",water=0,aquatic="land",light=12,moisture=0.3,ph=6} assert(hearth.botany.suitable("cowslip",cowslip,"Spring")) assert(not hearth.botany.suitable("cowslip",E.habitat(cowslip,{sphagnum=2}),"Spring"),"acidification changes real niche eligibility") end) test("floating mats shade only submerged freshwater, with a six-light ceiling and no brightening",function() for _,base in ipairs({15,7,3,1,0}) do local h={loaded=true,water=2,aquatic="submerged",freshwater=true,light=base,moisture=1,ph=6.5} local out=E.habitat(h,{duckweed=10000,sphagnum=10000}) assert(out.light==base-math.min(6,math.max(0,base-2)) and h.light==base) assert(out.water==2 and out.ph==6.5 and out.moisture==1) end for _,mode in ipairs({"surface","land","salt"}) do local h={loaded=true,water=mode=="land" and 0 or 2,aquatic=mode=="salt" and "submerged" or mode,freshwater=mode~="salt",light=10} assert(E.habitat(h,{duckweed=4}).light==10) end end) test("unknown, malformed and unloaded observations grant no effects",function() local h={loaded=true,water=0,moisture=0.5,ph=6.5,light=10} for _,n in ipairs({-1,math.huge,0/0,"2",false}) do local out=E.habitat(h,{sphagnum=n,duckweed=n}); assert(out.ph==h.ph and out.moisture==h.moisture) end assert(E.habitat(h,{unknown=999,alder=999}).ph==6.5) h.loaded=false; assert(E.habitat(h,{sphagnum=2}).ph==6.5) assert(E.habitat(nil)==nil) end) test("source and description results cannot mutate the reviewed policy",function() local row=entry("sphagnum","engineer"); row.status="descriptive"; row.source_url="wrong" assert(entry("sphagnum","engineer").status=="simulated") local sources=E.sources(); sources.alder.note="wrong" assert(E.source("alder").note~="wrong") end) test("effect text matches calendar policy without laundering identity references into timing evidence",function() assert(entry("clover","nitrogenfixer").game:find("90 real calendar days",1,true)) local fungus=entry("fly_agaric","ectomycorrhiza").game assert(fungus:find("No accelerated growth",1,true) and not fungus:find("16200",1,true)) assert(entry("birch_polypore","saprotroph").game:find("90 real days",1,true)) assert(entry("bilberry","pollinator").game:find("at most one extra berry",1,true)) for _,r in ipairs(hearth.botany.list()) do local text=entry(r.id,"lifecycle").game assert(text:find("source_reported, policy_fallback or UNKNOWN",1,true)) assert(text:find("unloaded time",1,true) and text:find("never authorize automatic death",1,true)) assert(not text:find("15 minutes",1,true)) end end) assert(passed==8) print("LAST_HEARTH_SPECIES_EFFECTS_PASS "..passed)