Module:StraitsDaily: Difference between revisions
From MicrasWiki
Jump to navigationJump to search
NewZimiaGov (talk | contribs) No edit summary |
NewZimiaGov (talk | contribs) No edit summary |
||
| (34 intermediate revisions by the same user not shown) | |||
| Line 1: | Line 1: | ||
-- Module: StraitsDaily | -- Module: StraitsDaily | ||
-- Purpose: Daily, in-world report of Strait of Haifa enforcement | -- Purpose: Daily, in-world report of Strait of Haifa enforcement [[Straits Conventions of 52.06 PSSC]] | ||
local p = {} | local p = {} | ||
| Line 16: | Line 9: | ||
local function getPSSCDate() | local function getPSSCDate() | ||
local startDate = os.time({year = 1999, month = 8, day = 6}) | local startDate = os.time({year = 1999, month = 8, day = 6}) | ||
local secondsInDay = 86400 | local secondsInDay = 86400 | ||
local daysPerYear | local daysPerYear = 183 | ||
local now = os.time() | local now = os.time() | ||
local | local totalDays = math.floor((now - startDate) / secondsInDay) | ||
local yearFraction = | local yearFraction = totalDays / daysPerYear | ||
local psscYear | local psscYear = math.floor(yearFraction) | ||
local dayOfYear | local dayOfYear = math.floor((yearFraction - psscYear) * daysPerYear) + 1 | ||
local month, dayOfMonth | local month, dayOfMonth | ||
| Line 42: | Line 35: | ||
------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ||
local sectors = { | local sectors = { | ||
" | "Strait South Inbound","Strait South Outbound","Strait North Inbound","Strait North Outbound", | ||
"Sacred Buffer Approaches","Environmental Buffer","Northern Approach TSS","Southern Approach TSS" | "Archipelago Zone Inbound", "Archipelago Zone Outbound", "Sacred Buffer Approaches", | ||
"Environmental Buffer","Northern Approach TSS","Southern Approach TSS", | |||
"Northern Sea of Storms","Western Sea of Storms","Southern Sea of Storms", | |||
} | } | ||
local flags = { | local flags = { | ||
{name="Bassaridian-flag", | {name="Bassaridian-flag", weight=6, risky=false}, | ||
{name="Moorland", | {name="Moorland", weight=2, risky=false}, | ||
{name="Neutral Merchant", | {name="Imperial Federation", weight=5, risky=false}, | ||
{name=" | {name="Matamoros (IF)", weight=2, risky=false}, | ||
{name="Consortium Registry", | {name="Neutral Merchant", weight=4, risky=false}, | ||
{name=" | {name="Diaspora Stakeholders- Euranidom", weight=3, risky=true}, | ||
{name="Diaspora Stakeholders- Corumia", weight=3, risky=true}, | |||
{name="Diaspora Stakeholders- South Keltiania", weight=3, risky=true}, | |||
{name="Consortium Registry", weight=2, risky=true}, | |||
{name="Independent Coastal State", weight=7, risky=false} | |||
} | } | ||
| Line 64: | Line 60: | ||
} | } | ||
local | local cargoTypes = { | ||
{ | {name="Medical reagents (dual-use precursors)", weight=3, risk=0.14, dual=true}, | ||
{ | {name="Industrial oxidizers / nitrates (dual-use)", weight=2, risk=0.16, dual=true}, | ||
{ | {name="Precision machine tools & CNC spares (dual-use)", weight=3, risk=0.15, dual=true}, | ||
{ | {name="Encrypted electronics & comms modules (dual-use)", weight=2, risk=0.17, dual=true}, | ||
{ | {name="Satellite components / sensors (dual-use)", weight=1, risk=0.18, dual=true}, | ||
{ | {name="Industrial fasteners (coded)", weight=2, risk=0.12, dual=true}, | ||
{ | {name="Hydraulic lubricants (dual-use)", weight=2, risk=0.13, dual=true}, | ||
{name="High-frequency alloys (coded)", weight=1, risk=0.14, dual=true}, | |||
{name="Aviation fuels & additives", weight=3, risk=0.12, dual=false}, | |||
{name="Refined petroleum products", weight=4, risk=0.08, dual=false}, | |||
{name="Relief supplies (mixed pallets)", weight=5, risk=0.06, dual=false}, | |||
{name="Agricultural commodities", weight=5, risk=0.04, dual=false}, | |||
{name="Finished consumer goods", weight=6, risk=0.03, dual=false} | |||
} | } | ||
local | local cargoTags = { | ||
{name=" | {name="standard", weight=6, risk=0.00}, | ||
{name="expedited", weight=3, risk=0.05}, | |||
{name="emergency", weight=2, risk=0.07}, | |||
{name=" | {name="perishable",weight=2, risk=0.06} | ||
{name=" | |||
{name=" | |||
} | } | ||
local | local partnerProfiles = { | ||
" | {code="P-07", weight=4, risk=0.00, recentLift=false}, | ||
" | {code="P-12", weight=3, risk=0.03, recentLift=false}, | ||
" | {code="P-17", weight=3, risk=0.12, recentLift=true}, | ||
" | {code="P-22", weight=3, risk=0.14, recentLift=true}, | ||
" | {code="P-31", weight=3, risk=0.05, recentLift=false}, | ||
" | {code="P-38", weight=2, risk=0.10, recentLift=false}, | ||
" | {code="P-44", weight=4, risk=0.02, recentLift=false}, | ||
" | {code="P-53", weight=3, risk=0.04, recentLift=false} | ||
" | } | ||
-- Corum districts (for WL district annotations) | |||
local corumDistricts = { | |||
{name="Lyvian Zones", kind="NZ"}, | |||
{name="Lucinarian Zones", kind="NZ"}, | |||
{name="Brentonian Zones", kind="NZ"}, | |||
{name="Ossyani Zones", kind="NZ"}, | |||
{name="Sinclarian Zones", kind="GM"}, | |||
{name="Nortonian Zones", kind="GM"}, | |||
{name="Wallisian Zones", kind="GM"}, | |||
{name="Alindan Zones", kind="GM"}, | |||
{name="Aijiac Zones", kind="GM"}, | |||
} | |||
local districtKindByName = {} | |||
for _,d in ipairs(corumDistricts) do districtKindByName[d.name] = d.kind end | |||
-- WL corridors (labels/windows) | |||
local wlCorridors = { | |||
{ code="WL-NZ-1", name="New Zimian Humanitarian Belt", win="0700–1800 (expanded)", kind="NZ", | |||
districts={"Lyvian Zones","Lucinarian Zones","Brentonian Zones","Ossyani Zones"} }, | |||
{ code="WL-GM-2", name="Bayen Relief Spur", win="0800–1900 (expanded)", kind="GM", | |||
districts={"Sinclarian Zones","Nortonian Zones","Wallisian Zones","Alindan Zones","Aijiac Zones"} }, | |||
{ code="WL-SEA-3", name="Corum Coastal Shuttle", win="0600–2000 (expanded)", kind="Mixed", | |||
districts={"Wallisian Zones","Nortonian Zones","Lyvian Zones"} } | |||
} | |||
local observers = { | |||
"Neutral temple auditor","Independent port compliance inspector", | |||
"Community elder (witness)","Straits Convention monitor (civilian)" | |||
} | |||
local wl03Signers = { | |||
["Matamoros (IF)"] = true, | |||
["Imperial Federation"] = true, | |||
["Bassaridian-flag"] = true, | |||
["Neutral Merchant"] = true, | |||
["Moorland"] = true, | |||
["Independent Coastal State"] = true | |||
} | } | ||
------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ||
-- ( | -- (B1.5) SECTOR RULES (allow/block flags and/or vessels per sector) | ||
------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ||
local | local sectorRules = { | ||
local | ["Northern Sea of Storms"] = { | ||
for _, | flags = { block = { ["Imperial Federation"]=true, ["Matamoros (IF)"]=true, ["Moorland"]=true } }, | ||
-- vessels = { allow = { ["Research craft"]=true } }, -- optional example | |||
}, | |||
["Western Sea of Storms"] = { | |||
if | flags = { block = { ["Imperial Federation"]=true, ["Matamoros (IF)"]=true, ["Moorland"]=true } }, | ||
}, | |||
["Southern Sea of Storms"] = { | |||
flags = { block = { ["Imperial Federation"]=true, ["Matamoros (IF)"]=true, ["Moorland"]=true } }, | |||
}, | |||
} | |||
------------------------------------------------------------------------------- | |||
-- (B2) PUBLIC HEALTH (<=10% rows) | |||
------------------------------------------------------------------------------- | |||
local phDiseases = { | |||
{name="Thalassan Rot", code="TR", controls="canal flush; rat-proofing; food-handler checks"}, | |||
{name="Festival Blush", code="FB", controls="ring vaccination; crowd spacing; dorm rotation"}, | |||
{name="Pilgrim’s Flux", code="PF", controls="boil notices; cistern audit; ORS distribution"}, | |||
{name="Stygian Dust", code="SD", controls="respirators; mist carts; guide clinic checks"}, | |||
{name="Haifan Salt-Sickness", code="HS", controls="indoor rotations; antifungal clinics; dehumidify"}, | |||
{name="Norsolyrian Drain", code="ND", controls="lined ditches; praziquantel; snail abatement"}, | |||
{name="Somniumpolis Lung", code="SL", controls="dust suppression; respirator fit-checks"}, | |||
{name="Haifan Crimson Fever", code="HCF",controls="larval control; dusk patrols; vector abatement"}, | |||
{name="Strip Yellowing", code="SY", controls="single-use ladles; vendor relicensing; recall"}, | |||
{name="Eye of Styx", code="ES", controls="eye-wash stations; visor compliance"}, | |||
{name="Cato’s Curse", code="CC", controls="PPE audits; solvent/oxidizer exposure controls"}, | |||
{name="White Pestilence", code="WP", controls="tiered quarantine; convoy assays; cordons"}, | |||
{name="Breath of Nephele", code="BN", controls="ventilation drills; mask catechism; spacing"}, | |||
{name="Agnian Lockjaw", code="AL", controls="tetanus boosters; injury clinics"}, | |||
} | |||
local phIndex = {} | |||
for _,d in ipairs(phDiseases) do phIndex[d.code] = d end | |||
local function phScore(sector, flag, vessel, cargo, tag, partner) | |||
local score = 0 | |||
local cname = cargo and cargo.name or "" | |||
local tname = tag and tag.name or "standard" | |||
if sector == "Environmental Buffer" or sector == "Sacred Buffer Approaches" | |||
or sector:find("Strait") or sector:find("Approach") then score = score + 1 end | |||
if cname == "Agricultural commodities" or tname == "perishable" then score = score + 2 end | |||
if cname == "Relief supplies (mixed pallets)" then score = score + 1 end | |||
if cname:find("oxidizers") or cname:find("lubricants") or cname:find("petroleum") or cname:find("fuels") then score = score + 2 end | |||
if vessel == "Ro-Ro ferry" and (tname == "expedited" or tname == "emergency") then score = score + 2 end | |||
if (partner and partner.risk or 0) >= 0.09 then score = score + 1 end | |||
if flag and flag.risky then score = score + 0.5 end | |||
local function pick(code) | |||
local d = phIndex[code]; if not d then return 0,nil,nil end | |||
return score, d.code, (d.code .. " ("..d.name.."): " .. d.controls) | |||
end | end | ||
return | if score >= 5 then return pick("WP") end | ||
if (cname == "Agricultural commodities" or tname == "perishable") then return pick("SY") end | |||
if cname == "Relief supplies (mixed pallets)" then return pick("PF") end | |||
if sector == "Environmental Buffer" or sector:find("Strait") then return pick("TR") end | |||
if sector:find("Archipelago Zone") then return pick("HS") end -- matches Inbound/Outbound | |||
if (cname:find("oxidizers") or cname:find("lubricants") or cname:find("petroleum") or cname:find("fuels")) then return pick("CC") end | |||
if vessel == "Ro-Ro ferry" and (tname == "expedited" or tname == "emergency") then return pick("FB") end | |||
if (partner and partner.risk or 0) >= 0.09 then return pick("BN") end | |||
return 0,nil,nil | |||
end | |||
------------------------------------------------------------------------------- | |||
-- (C) POLICY STAGES (per flag) | |||
------------------------------------------------------------------------------- | |||
local stageMap = { | |||
["Imperial Federation"] = 1, | |||
["Matamoros (IF)"] = 1, | |||
} | |||
local function getFlagStage(flagName, globalStage) | |||
local s = stageMap[flagName] | |||
if s ~= nil then return s end | |||
return globalStage or 0 | |||
end | end | ||
------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ||
-- (D) | -- (D) HELPERS | ||
------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ||
local function | local function pick(list) return list[math.random(#list)] end | ||
local | local function pickWeighted(list) | ||
local total = 0 | |||
for _, item in ipairs(list) do total = total + (item.weight or 1) end | |||
local roll, cumul = math.random(total), 0 | |||
for _, item in ipairs(list) do | |||
cumul = cumul + (item.weight or 1) | |||
if roll <= cumul then return item end | |||
end | end | ||
return list[#list] | |||
return | |||
end | end | ||
-- | -- Filter helpers (respect sectorRules; safe fallback if empty) | ||
local function | local function isAllowedByRule(ruleTbl, key) | ||
if not ruleTbl then return true end | |||
if ruleTbl.allow then return ruleTbl.allow[key] == true end | |||
if ruleTbl.block then return ruleTbl.block[key] ~= true end | |||
return true | |||
end | |||
local function flagAllowedInSector(sector, flagName) | |||
local rules = sectorRules[sector] | |||
return isAllowedByRule(rules and rules.flags, flagName) | |||
end | |||
local function vesselAllowedInSector(sector, vesselName) | |||
local rules = sectorRules[sector] | |||
return isAllowedByRule(rules and rules.vessels, vesselName) | |||
end | end | ||
local function pickWeightedFiltered(list, predicate) | |||
local filtered = {} | |||
for _, item in ipairs(list) do | |||
if predicate(item) then table.insert(filtered, item) end | |||
end | end | ||
if #filtered == 0 then return pickWeighted(list) end | |||
for _, | return pickWeighted(filtered) | ||
if ( | end | ||
local function pickFiltered(list, predicate) | |||
local filtered = {} | |||
for _, item in ipairs(list) do | |||
if predicate(item) then table.insert(filtered, item) end | |||
end | end | ||
return | if #filtered == 0 then return pick(list) end | ||
return pick(filtered) | |||
end | end | ||
-- | -- FULL WL expansion switch | ||
-- ( | local fullWL = true | ||
local function | -- WL eligibility (expanded) | ||
local function wlEligibility(cargo, tag, flag, partner) | |||
local | if not cargo or not tag then return false, "n/a" end | ||
local disqualify = false | |||
if cargo.dual then disqualify = true end | |||
if flag and flag.name == "Matamoros (IF)" and not wl03Signers["Matamoros (IF)"] then disqualify = true end | |||
local | if disqualify then | ||
local signed = wl03Signers[flag.name] | |||
return | if signed == nil then signed = true end | ||
local cooling = (partner and partner.recentLift) or false | |||
if not signed then return false, "WL-03 not signed (M-AMBER)" end | |||
if cooling then return false, "WL-03 cooldown (90 days)" end | |||
return false, "Not eligible cargo/tag" | |||
end | end | ||
local | if fullWL then | ||
return true, "WL-03 compliant" | |||
else | |||
local green = (cargo.name == "Relief supplies (mixed pallets)" or cargo.name == "Agricultural commodities") | |||
local amber = (tag.name == "expedited" or tag.name == "emergency" or tag.name == "perishable") | |||
local baseEligible = ((green or amber) and not cargo.dual) | |||
local signed = wl03Signers[flag.name]; if signed == nil then signed = true end | |||
local cooling = (partner and partner.recentLift) or false | |||
if not baseEligible then return false, "Not eligible cargo/tag" end | |||
if not signed then return false, "WL-03 not signed (M-AMBER)" end | |||
if cooling then return false, "WL-03 cooldown (90 days)" end | |||
return true, "WL-03 compliant" | |||
end | |||
end | |||
local function legalBasis(sector, wl, wl03note, phApplied) | |||
if ( | if phApplied then | ||
return "Art VII (Public Health Safeguards); Annex D; HCE PH-Bulletin" | |||
end | |||
if wl then | |||
return "Art III.5 (White-Lane); Annex G; Art X.3 (expedited remedies); HCE Circular WL-03 (" .. wl03note .. ")" | |||
end | |||
if sector == "Sacred Buffer Approaches" or sector == "Environmental Buffer" then | |||
return "Art III.2.3 (Sacred quiet zones); Annex A (delims); Art VII; Annex D" | |||
end | end | ||
return "Art IV.1–1.2 (PSC); Annex F (SCT/SAC procedures)" | |||
end | |||
local function appealsStatus(sector, wl, tag, cargo) | |||
if wl then | |||
local relief = 0.70 | |||
if tag and (tag.name=="emergency" or tag.name=="perishable") then relief = relief + 0.10 end | |||
if cargo and cargo.dual then relief = relief - 0.20 end | |||
local r = math.random() | |||
local outcome = (r <= relief * 0.6) and "clearance granted" | |||
or (r <= relief) and "expedited with conditions" | |||
or "conditions imposed" | |||
return string.format("HCE Review → SCT (72h) → SAC (≤5d) — %s (Art X.3; Annex G; WL-03 liaison)", outcome) | |||
else | |||
local level = (sector=="Sacred Buffer Approaches" or sector=="Environmental Buffer") and "SAC Referral" | |||
or ((math.random()<0.5) and "SCT Admin Review" or "PSC reconciliation") | |||
local outcome = (level=="PSC reconciliation") and "notice acknowledged" | |||
or ((math.random()<0.5) and "clearance granted" or "conditions imposed") | |||
local cite = (level=="SAC Referral") and "Annex F (SAC)" or "Annex F (SCT)" | |||
return string.format("%s — %s (%s)", level, outcome, cite) | |||
end | end | ||
end | |||
-- | local function hceRef(dateInfo, corridor) | ||
return string.format("HCE-%03d.%03d-%s", dateInfo.year, dateInfo.dayOfYear, corridor.code) | |||
end | |||
return { | local function pickServedDistrict(corr) | ||
local dname = corr.districts[math.random(#corr.districts)] | |||
local kind = districtKindByName[dname] or "NZ" | |||
return dname, kind | |||
end | |||
local function buildTriggers(flag, sector, cargo, tag, partner, wlEligible, wl03note, riskPush, flagStage) | |||
local t = {} | |||
if flag.risky then table.insert(t, "risky flag") end | |||
if sector == "Sacred Buffer Approaches" or sector == "Environmental Buffer" then | |||
table.insert(t, "sacred/ecology buffer") | |||
end | |||
if cargo and cargo.dual then table.insert(t, "dual-use cargo") end | |||
if cargo and (cargo.risk or 0) >= 0.12 then table.insert(t, "cargo risk ≥0.12") end | |||
if tag and tag.name ~= "standard" then table.insert(t, "non-standard tag: "..tag.name) end | |||
if partner and (partner.risk or 0) >= 0.09 then table.insert(t, "partner risk ≥0.09") end | |||
if wlEligible then table.insert(t, "WL pre-cleared") end | |||
if wl03note == "WL-03 not signed (M-AMBER)" then table.insert(t, "WL-03 unsigned (M-AMBER)") end | |||
if wl03note == "WL-03 cooldown (90 days)" then table.insert(t, "WL-03 cooldown") end | |||
if flagStage and flagStage > 0 then table.insert(t, "policy stage "..flagStage) end | |||
if riskPush and riskPush >= 2 then table.insert(t, "multi-trigger escalation") end | |||
if #t == 0 then return "none" end | |||
return table.concat(t, "; ") | |||
end | end | ||
------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ||
-- ( | -- (E) MAIN GENERATION (PH <=10%; no Notes column) | ||
------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ||
function p.makeTable(frame) | function p.makeTable(frame) | ||
local dateInfo = getPSSCDate() | local dateInfo = getPSSCDate() | ||
local dailySeed = dateInfo.year*1000 + dateInfo.dayOfYear | |||
math.randomseed(dailySeed) | |||
local | local rows = tonumber(frame and frame.args and frame.args.rows) or 36 | ||
local globalStage = tonumber(frame and frame.args and frame.args.policy_stage) or 0 | |||
local showLegend = (frame and frame.args and tostring(frame.args.showlegend) or "yes"):lower() ~= "no" | |||
-- PH toggles (cap fixed at 10%) | |||
local phOn = (frame and frame.args and tostring(frame.args.ph) or "on"):lower() ~= "off" | |||
local phRateArg = tonumber(frame and frame.args and frame.args.phrate) or 0.10 | |||
if phRateArg < 0 then phRateArg = 0 end | |||
if phRateArg > 0.10 then phRateArg = 0.10 end | |||
local showPHLeg = (frame and frame.args and tostring(frame.args.showphlegend) or "yes"):lower() ~= "no" | |||
local phCap = math.floor(rows * 0.10) | |||
-- Pass 1: compose rows and score PH | |||
local rowObjs = {} | |||
for i = 1, rows do | |||
math.randomseed(dailySeed + i*97) | |||
local sector = pick(sectors) | |||
-- NEW: sector-aware picks | |||
local flag = pickWeightedFiltered(flags, function(f) | |||
return flagAllowedInSector(sector, f.name) | |||
end) | |||
local vessel = pickFiltered(vessels, function(v) | |||
return vesselAllowedInSector(sector, v) | |||
end) | |||
local cargo = pickWeighted(cargoTypes) | |||
local tag = pickWeighted(cargoTags) | |||
local partner = pickWeighted(partnerProfiles) | |||
local wlOK, wl03note = wlEligibility(cargo, tag, flag, partner) | |||
local wlEligible = wlOK | |||
local corridor = wlEligible and wlCorridors[math.random(#wlCorridors)] or nil | |||
local obs = wlEligible and observers[math.random(#observers)] or nil | |||
local flagStage = getFlagStage(flag.name, globalStage) | |||
local function baseProb() | |||
local q = 0.20 | |||
if flag.risky then q = q + 0.05 end | |||
if sector == "Sacred Buffer Approaches" or sector == "Environmental Buffer" then q = q + 0.08 end | |||
q = q + (cargo and cargo.risk or 0) | |||
if cargo and cargo.dual then q = q + 0.12 end | |||
q = q + (tag and tag.risk or 0) | |||
q = q + (partner and partner.risk or 0) | |||
if wlEligible then q = math.max(q - 0.08, 0.08) end | |||
local isIF = flag.name == "Imperial Federation" | |||
local isMT = flag.name:match("^Matamoros") | |||
if (isIF or isMT) and not wlEligible then | |||
if flagStage >= 1 then q = q + 0.04 end | |||
if flagStage >= 2 then q = q + 0.04 end | |||
end | |||
if flag.name:match("Matamoros") and wl03note == "WL-03 not signed (M-AMBER)" then | |||
q = q + 0.08 | |||
end | |||
if flag.name == "Bassaridian-flag" then | |||
q = q - 0.18 | |||
if wlEligible then q = q - 0.04 end | |||
if q < 0.02 then q = 0.02 end | |||
if q > 0.08 then q = 0.08 end | |||
end | |||
return math.min(q, 0.72) | |||
end | |||
local riskPush = 0 | |||
if flag.name:match("Imperial Federation") or flag.name:match("^Matamoros") then riskPush = riskPush + 1 end | |||
if cargo.dual or (cargo.risk or 0) >= 0.12 then riskPush = riskPush + 1 end | |||
if tag.name ~= "standard" then riskPush = riskPush + 1 end | |||
if (partner.risk or 0) >= 0.09 then riskPush = riskPush + 1 end | |||
if wlEligible then riskPush = math.max(riskPush - 1, 0) end | |||
local score, phCode, phText = phScore(sector, flag, vessel, cargo, tag, partner) | |||
score = score + phRateArg * 0.5 | |||
rowObjs[i] = { | |||
i=i, sector=sector, flag=flag, vessel=vessel, cargo=cargo, tag=tag, partner=partner, | |||
wlEligible = wlEligible, wl03note = wl03note, corridor = corridor, obs = obs, | |||
flagStage = flagStage, enforceProb = baseProb(), riskPush = riskPush, | |||
phScore = score, phCode = phCode, phText = phText | |||
} | |||
end | |||
-- Select top K PH rows | |||
if phOn and phCap > 0 then | |||
table.sort(rowObjs, function(a,b) return a.phScore > b.phScore end) | |||
local applied = 0 | |||
for _,r in ipairs(rowObjs) do | |||
if r.phCode and applied < phCap and r.phScore > 0 then | |||
r.phApplied = true | |||
applied = applied + 1 | |||
else | |||
r.phApplied = false | |||
end | |||
end | |||
table.sort(rowObjs, function(a,b) return a.i < b.i end) | |||
else | |||
for _,r in ipairs(rowObjs) do r.phApplied = false end | |||
end | |||
-- Pass 2: render (no Notes column) | |||
local out = {} | local out = {} | ||
table.insert(out, | table.insert(out, "{| class='wikitable sortable'") | ||
table.insert(out, | if phOn then | ||
table.insert(out, "! Date !! Sector !! Vessel/Flag !! Action !! Legal Basis !! Outcome !! Appeals/Review (Straits Conventions) !! Case ID !! PH Case !! Public Health Advisory !! Triggers") | |||
else | |||
table.insert(out, "! Date !! Sector !! Vessel/Flag !! Action !! Legal Basis !! Outcome !! Appeals/Review (Straits Conventions) !! Case ID !! Triggers") | |||
end | |||
for _,r in ipairs(rowObjs) do | |||
math.randomseed(dailySeed + r.i*131) | |||
local action, legal, outcome, appealCol | |||
local dname, dkind | |||
if r.phApplied then | |||
action = "Public health interdiction ("..r.phCode..")" | |||
legal = legalBasis(r.sector, false, r.wl03note, true) | |||
outcome = "Public health hold; extended review" | |||
appealCol = "SAC Referral — health safeguards (Annex D)" | |||
else | |||
if r.wlEligible then | |||
dname, dkind = pickServedDistrict(r.corridor) | |||
action = "WL transit / pilotage verification" | |||
legal = legalBasis(r.sector, true, r.wl03note, false) | |||
else | |||
action = (r.wl03note == "WL-03 not signed (M-AMBER)" or r.wl03note == "WL-03 cooldown (90 days)") | |||
and "WL-ineligible: standard PSC applied" | |||
or "Cargo manifest reconciliation audit" | |||
legal = legalBasis(r.sector, false, r.wl03note, false) | |||
end | |||
local enforce = r.enforceProb | |||
if math.random() > enforce then | |||
outcome = r.wlEligible and "Cleared WL window" or "No reports" | |||
appealCol = r.wlEligible and "N/A (WL cleared)" or "N/A" | |||
else | |||
if | outcome = r.wlEligible and "WL reconciliation check; monitoring" or "Administrative notice issued; monitoring" | ||
if r.riskPush >= 2 and math.random() < 0.65 then | |||
outcome = (r.wlEligible and "WL hold; clearance pending expedited review" or "Random audit hold; clearance pending") | |||
if r.flag.name == "Imperial Federation" or r.flag.name:match("^Matamoros") then | |||
outcome = outcome .. " [Extended regulatory review]" | |||
end | |||
end | |||
local isIF = r.flag.name == "Imperial Federation" | |||
local isMT = r.flag.name:match("^Matamoros") | |||
if r.flagStage >= 3 and (isIF or isMT) and not r.wlEligible and r.riskPush >= 2 then | |||
outcome = "Denial of passage (policy Stage III)" | |||
appealCol = "SCT injunction eligible | Appeals Case pending" | |||
end | |||
if r.flag.name == "Bassaridian-flag" and outcome:match("hold") then | |||
outcome = r.wlEligible and "WL reconciliation complete; cleared same window" or "Administrative reconciliation complete; cleared" | |||
appealCol = r.wlEligible and "N/A (WL cleared)" or "N/A" | |||
end | |||
appealCol = appealCol or appealsStatus(r.sector, r.wlEligible, r.tag, r.cargo) | |||
end | end | ||
end | |||
local caseId = string.format("CASE-%03d.%03d-%02d", dateInfo.year, dateInfo.dayOfYear, r.i) | |||
local phCaseId = "—" | |||
local phText = "No concern" | |||
if phOn and r.phApplied then | |||
phCaseId = string.format("PH-CASE-%03d.%03d-%02d-%s", dateInfo.year, dateInfo.dayOfYear, r.i, r.phCode or "PH") | |||
phText = r.phText or "Public health concern" | |||
end | |||
local triggers = buildTriggers(r.flag, r.sector, r.cargo, r.tag, r.partner, r.wlEligible, r.wl03note, r.riskPush, r.flagStage) | |||
if outcome:lower():match("hold") or outcome:lower():match("conditions") or outcome:lower():match("monitoring") or outcome:lower():match("denial") then | |||
appealCol = (appealCol ~= "N/A" and (appealCol .. " | ")) or "" | |||
appealCol = appealCol .. ("Appeals Case: APP-" .. caseId) | |||
end | |||
local style | |||
local o = outcome:lower() | |||
if r.phApplied or o:match("hold") or o:match("denial") or o:match("detention") or o:match("impound") or o:match("extended regulatory review") then | |||
style = ' style="background-color:#f8d7da;"' | |||
elseif o:match("administrative notice") or o:match("monitoring") or o:match("conditions") or o:match("reconciliation") then | |||
style = ' style="background-color:#fff3cd;"' | |||
else | |||
style = ' style="background-color:#d4edda;"' | |||
end | |||
table.insert(out, "|-" .. style) | |||
if phOn then | |||
table.insert(out, string.format("| %s || %s || %s (%s) || %s || %s || %s || %s || %s || %s || %s || %s", | |||
dateInfo.text, r.sector, r.vessel, r.flag.name, action, legal, outcome, appealCol, caseId, phCaseId, phText, triggers)) | |||
else | |||
table.insert(out, string.format("| %s || %s || %s (%s) || %s || %s || %s || %s || %s || %s", | |||
dateInfo.text, r.sector, r.vessel, r.flag.name, action, legal, outcome, appealCol, caseId, triggers)) | |||
end | end | ||
end | end | ||
table.insert(out, "|}") | |||
table.insert(out, " | if showLegend then | ||
table.insert(out, " | table.insert(out, "\n<div style='margin-top:0.5em; font-size:90%;'>") | ||
table.insert(out, "<b>Legend:</b> ") | |||
table.insert(out, "<span style='background:#d4edda;padding:2px 6px;border:1px solid #c3e6cb;'>Green</span> = Cleared/Compliant ") | |||
table.insert(out, "<span style='background:#fff3cd;padding:2px 6px;border:1px solid #ffeeba;'>Yellow</span> = Administrative/Conditional/Monitoring ") | |||
table.insert(out, "<span style='background:#f8d7da;padding:2px 6px;border:1px solid #f5c6cb;'>Red</span> = Hold/Denial/Extended Review") | |||
table.insert(out, "<br/><i>Policy Stage (per flag):</i> I = Audit emphasis; II = +Market surcharge advisory; III = limited denial on high-risk, non-WL rows.") | |||
table.insert(out, "</div>") | |||
end | |||
if phOn then | |||
table.insert(out, "\n<div style='margin-top:0.3em; font-size:90%;'><b>Public Health Advisory Codes:</b> ") | |||
local parts = {} | |||
for _,d in ipairs(phDiseases) do | |||
table.insert(parts, string.format("<span style='white-space:nowrap;'><b>%s</b> – %s</span>", d.code, d.name)) | |||
end | |||
table.insert(out, table.concat(parts, " ")) | |||
table.insert(out, "</div>") | |||
end | end | ||
return table.concat(out, "\n") | return table.concat(out, "\n") | ||
end | end | ||
return p | return p | ||
Latest revision as of 14:03, 13 October 2025
Documentation for this module may be created at Module:StraitsDaily/doc
-- Module: StraitsDaily
-- Purpose: Daily, in-world report of Strait of Haifa enforcement [[Straits Conventions of 52.06 PSSC]]
local p = {}
-------------------------------------------------------------------------------
-- (A) PSSC DATE
-------------------------------------------------------------------------------
local function getPSSCDate()
local startDate = os.time({year = 1999, month = 8, day = 6})
local secondsInDay = 86400
local daysPerYear = 183
local now = os.time()
local totalDays = math.floor((now - startDate) / secondsInDay)
local yearFraction = totalDays / daysPerYear
local psscYear = math.floor(yearFraction)
local dayOfYear = math.floor((yearFraction - psscYear) * daysPerYear) + 1
local month, dayOfMonth
if dayOfYear <= 61 then
month = 1; dayOfMonth = dayOfYear
elseif dayOfYear <= 122 then
month = 2; dayOfMonth = dayOfYear - 61
else
month = 3; dayOfMonth = dayOfYear - 122
end
local dateString = string.format("%d/%d/%d PSSC", dayOfMonth, month, psscYear)
return { text = dateString, year = psscYear, dayOfYear = dayOfYear }
end
-------------------------------------------------------------------------------
-- (B) STATIC DATA
-------------------------------------------------------------------------------
local sectors = {
"Strait South Inbound","Strait South Outbound","Strait North Inbound","Strait North Outbound",
"Archipelago Zone Inbound", "Archipelago Zone Outbound", "Sacred Buffer Approaches",
"Environmental Buffer","Northern Approach TSS","Southern Approach TSS",
"Northern Sea of Storms","Western Sea of Storms","Southern Sea of Storms",
}
local flags = {
{name="Bassaridian-flag", weight=6, risky=false},
{name="Moorland", weight=2, risky=false},
{name="Imperial Federation", weight=5, risky=false},
{name="Matamoros (IF)", weight=2, risky=false},
{name="Neutral Merchant", weight=4, risky=false},
{name="Diaspora Stakeholders- Euranidom", weight=3, risky=true},
{name="Diaspora Stakeholders- Corumia", weight=3, risky=true},
{name="Diaspora Stakeholders- South Keltiania", weight=3, risky=true},
{name="Consortium Registry", weight=2, risky=true},
{name="Independent Coastal State", weight=7, risky=false}
}
local vessels = {
"Handymax bulker","Product tanker","General cargo ship","Container feeder",
"Ro-Ro ferry","Research craft","Declared submarine","Undeclared submarine"
}
local cargoTypes = {
{name="Medical reagents (dual-use precursors)", weight=3, risk=0.14, dual=true},
{name="Industrial oxidizers / nitrates (dual-use)", weight=2, risk=0.16, dual=true},
{name="Precision machine tools & CNC spares (dual-use)", weight=3, risk=0.15, dual=true},
{name="Encrypted electronics & comms modules (dual-use)", weight=2, risk=0.17, dual=true},
{name="Satellite components / sensors (dual-use)", weight=1, risk=0.18, dual=true},
{name="Industrial fasteners (coded)", weight=2, risk=0.12, dual=true},
{name="Hydraulic lubricants (dual-use)", weight=2, risk=0.13, dual=true},
{name="High-frequency alloys (coded)", weight=1, risk=0.14, dual=true},
{name="Aviation fuels & additives", weight=3, risk=0.12, dual=false},
{name="Refined petroleum products", weight=4, risk=0.08, dual=false},
{name="Relief supplies (mixed pallets)", weight=5, risk=0.06, dual=false},
{name="Agricultural commodities", weight=5, risk=0.04, dual=false},
{name="Finished consumer goods", weight=6, risk=0.03, dual=false}
}
local cargoTags = {
{name="standard", weight=6, risk=0.00},
{name="expedited", weight=3, risk=0.05},
{name="emergency", weight=2, risk=0.07},
{name="perishable",weight=2, risk=0.06}
}
local partnerProfiles = {
{code="P-07", weight=4, risk=0.00, recentLift=false},
{code="P-12", weight=3, risk=0.03, recentLift=false},
{code="P-17", weight=3, risk=0.12, recentLift=true},
{code="P-22", weight=3, risk=0.14, recentLift=true},
{code="P-31", weight=3, risk=0.05, recentLift=false},
{code="P-38", weight=2, risk=0.10, recentLift=false},
{code="P-44", weight=4, risk=0.02, recentLift=false},
{code="P-53", weight=3, risk=0.04, recentLift=false}
}
-- Corum districts (for WL district annotations)
local corumDistricts = {
{name="Lyvian Zones", kind="NZ"},
{name="Lucinarian Zones", kind="NZ"},
{name="Brentonian Zones", kind="NZ"},
{name="Ossyani Zones", kind="NZ"},
{name="Sinclarian Zones", kind="GM"},
{name="Nortonian Zones", kind="GM"},
{name="Wallisian Zones", kind="GM"},
{name="Alindan Zones", kind="GM"},
{name="Aijiac Zones", kind="GM"},
}
local districtKindByName = {}
for _,d in ipairs(corumDistricts) do districtKindByName[d.name] = d.kind end
-- WL corridors (labels/windows)
local wlCorridors = {
{ code="WL-NZ-1", name="New Zimian Humanitarian Belt", win="0700–1800 (expanded)", kind="NZ",
districts={"Lyvian Zones","Lucinarian Zones","Brentonian Zones","Ossyani Zones"} },
{ code="WL-GM-2", name="Bayen Relief Spur", win="0800–1900 (expanded)", kind="GM",
districts={"Sinclarian Zones","Nortonian Zones","Wallisian Zones","Alindan Zones","Aijiac Zones"} },
{ code="WL-SEA-3", name="Corum Coastal Shuttle", win="0600–2000 (expanded)", kind="Mixed",
districts={"Wallisian Zones","Nortonian Zones","Lyvian Zones"} }
}
local observers = {
"Neutral temple auditor","Independent port compliance inspector",
"Community elder (witness)","Straits Convention monitor (civilian)"
}
local wl03Signers = {
["Matamoros (IF)"] = true,
["Imperial Federation"] = true,
["Bassaridian-flag"] = true,
["Neutral Merchant"] = true,
["Moorland"] = true,
["Independent Coastal State"] = true
}
-------------------------------------------------------------------------------
-- (B1.5) SECTOR RULES (allow/block flags and/or vessels per sector)
-------------------------------------------------------------------------------
local sectorRules = {
["Northern Sea of Storms"] = {
flags = { block = { ["Imperial Federation"]=true, ["Matamoros (IF)"]=true, ["Moorland"]=true } },
-- vessels = { allow = { ["Research craft"]=true } }, -- optional example
},
["Western Sea of Storms"] = {
flags = { block = { ["Imperial Federation"]=true, ["Matamoros (IF)"]=true, ["Moorland"]=true } },
},
["Southern Sea of Storms"] = {
flags = { block = { ["Imperial Federation"]=true, ["Matamoros (IF)"]=true, ["Moorland"]=true } },
},
}
-------------------------------------------------------------------------------
-- (B2) PUBLIC HEALTH (<=10% rows)
-------------------------------------------------------------------------------
local phDiseases = {
{name="Thalassan Rot", code="TR", controls="canal flush; rat-proofing; food-handler checks"},
{name="Festival Blush", code="FB", controls="ring vaccination; crowd spacing; dorm rotation"},
{name="Pilgrim’s Flux", code="PF", controls="boil notices; cistern audit; ORS distribution"},
{name="Stygian Dust", code="SD", controls="respirators; mist carts; guide clinic checks"},
{name="Haifan Salt-Sickness", code="HS", controls="indoor rotations; antifungal clinics; dehumidify"},
{name="Norsolyrian Drain", code="ND", controls="lined ditches; praziquantel; snail abatement"},
{name="Somniumpolis Lung", code="SL", controls="dust suppression; respirator fit-checks"},
{name="Haifan Crimson Fever", code="HCF",controls="larval control; dusk patrols; vector abatement"},
{name="Strip Yellowing", code="SY", controls="single-use ladles; vendor relicensing; recall"},
{name="Eye of Styx", code="ES", controls="eye-wash stations; visor compliance"},
{name="Cato’s Curse", code="CC", controls="PPE audits; solvent/oxidizer exposure controls"},
{name="White Pestilence", code="WP", controls="tiered quarantine; convoy assays; cordons"},
{name="Breath of Nephele", code="BN", controls="ventilation drills; mask catechism; spacing"},
{name="Agnian Lockjaw", code="AL", controls="tetanus boosters; injury clinics"},
}
local phIndex = {}
for _,d in ipairs(phDiseases) do phIndex[d.code] = d end
local function phScore(sector, flag, vessel, cargo, tag, partner)
local score = 0
local cname = cargo and cargo.name or ""
local tname = tag and tag.name or "standard"
if sector == "Environmental Buffer" or sector == "Sacred Buffer Approaches"
or sector:find("Strait") or sector:find("Approach") then score = score + 1 end
if cname == "Agricultural commodities" or tname == "perishable" then score = score + 2 end
if cname == "Relief supplies (mixed pallets)" then score = score + 1 end
if cname:find("oxidizers") or cname:find("lubricants") or cname:find("petroleum") or cname:find("fuels") then score = score + 2 end
if vessel == "Ro-Ro ferry" and (tname == "expedited" or tname == "emergency") then score = score + 2 end
if (partner and partner.risk or 0) >= 0.09 then score = score + 1 end
if flag and flag.risky then score = score + 0.5 end
local function pick(code)
local d = phIndex[code]; if not d then return 0,nil,nil end
return score, d.code, (d.code .. " ("..d.name.."): " .. d.controls)
end
if score >= 5 then return pick("WP") end
if (cname == "Agricultural commodities" or tname == "perishable") then return pick("SY") end
if cname == "Relief supplies (mixed pallets)" then return pick("PF") end
if sector == "Environmental Buffer" or sector:find("Strait") then return pick("TR") end
if sector:find("Archipelago Zone") then return pick("HS") end -- matches Inbound/Outbound
if (cname:find("oxidizers") or cname:find("lubricants") or cname:find("petroleum") or cname:find("fuels")) then return pick("CC") end
if vessel == "Ro-Ro ferry" and (tname == "expedited" or tname == "emergency") then return pick("FB") end
if (partner and partner.risk or 0) >= 0.09 then return pick("BN") end
return 0,nil,nil
end
-------------------------------------------------------------------------------
-- (C) POLICY STAGES (per flag)
-------------------------------------------------------------------------------
local stageMap = {
["Imperial Federation"] = 1,
["Matamoros (IF)"] = 1,
}
local function getFlagStage(flagName, globalStage)
local s = stageMap[flagName]
if s ~= nil then return s end
return globalStage or 0
end
-------------------------------------------------------------------------------
-- (D) HELPERS
-------------------------------------------------------------------------------
local function pick(list) return list[math.random(#list)] end
local function pickWeighted(list)
local total = 0
for _, item in ipairs(list) do total = total + (item.weight or 1) end
local roll, cumul = math.random(total), 0
for _, item in ipairs(list) do
cumul = cumul + (item.weight or 1)
if roll <= cumul then return item end
end
return list[#list]
end
-- Filter helpers (respect sectorRules; safe fallback if empty)
local function isAllowedByRule(ruleTbl, key)
if not ruleTbl then return true end
if ruleTbl.allow then return ruleTbl.allow[key] == true end
if ruleTbl.block then return ruleTbl.block[key] ~= true end
return true
end
local function flagAllowedInSector(sector, flagName)
local rules = sectorRules[sector]
return isAllowedByRule(rules and rules.flags, flagName)
end
local function vesselAllowedInSector(sector, vesselName)
local rules = sectorRules[sector]
return isAllowedByRule(rules and rules.vessels, vesselName)
end
local function pickWeightedFiltered(list, predicate)
local filtered = {}
for _, item in ipairs(list) do
if predicate(item) then table.insert(filtered, item) end
end
if #filtered == 0 then return pickWeighted(list) end
return pickWeighted(filtered)
end
local function pickFiltered(list, predicate)
local filtered = {}
for _, item in ipairs(list) do
if predicate(item) then table.insert(filtered, item) end
end
if #filtered == 0 then return pick(list) end
return pick(filtered)
end
-- FULL WL expansion switch
local fullWL = true
-- WL eligibility (expanded)
local function wlEligibility(cargo, tag, flag, partner)
if not cargo or not tag then return false, "n/a" end
local disqualify = false
if cargo.dual then disqualify = true end
if flag and flag.name == "Matamoros (IF)" and not wl03Signers["Matamoros (IF)"] then disqualify = true end
if disqualify then
local signed = wl03Signers[flag.name]
if signed == nil then signed = true end
local cooling = (partner and partner.recentLift) or false
if not signed then return false, "WL-03 not signed (M-AMBER)" end
if cooling then return false, "WL-03 cooldown (90 days)" end
return false, "Not eligible cargo/tag"
end
if fullWL then
return true, "WL-03 compliant"
else
local green = (cargo.name == "Relief supplies (mixed pallets)" or cargo.name == "Agricultural commodities")
local amber = (tag.name == "expedited" or tag.name == "emergency" or tag.name == "perishable")
local baseEligible = ((green or amber) and not cargo.dual)
local signed = wl03Signers[flag.name]; if signed == nil then signed = true end
local cooling = (partner and partner.recentLift) or false
if not baseEligible then return false, "Not eligible cargo/tag" end
if not signed then return false, "WL-03 not signed (M-AMBER)" end
if cooling then return false, "WL-03 cooldown (90 days)" end
return true, "WL-03 compliant"
end
end
local function legalBasis(sector, wl, wl03note, phApplied)
if phApplied then
return "Art VII (Public Health Safeguards); Annex D; HCE PH-Bulletin"
end
if wl then
return "Art III.5 (White-Lane); Annex G; Art X.3 (expedited remedies); HCE Circular WL-03 (" .. wl03note .. ")"
end
if sector == "Sacred Buffer Approaches" or sector == "Environmental Buffer" then
return "Art III.2.3 (Sacred quiet zones); Annex A (delims); Art VII; Annex D"
end
return "Art IV.1–1.2 (PSC); Annex F (SCT/SAC procedures)"
end
local function appealsStatus(sector, wl, tag, cargo)
if wl then
local relief = 0.70
if tag and (tag.name=="emergency" or tag.name=="perishable") then relief = relief + 0.10 end
if cargo and cargo.dual then relief = relief - 0.20 end
local r = math.random()
local outcome = (r <= relief * 0.6) and "clearance granted"
or (r <= relief) and "expedited with conditions"
or "conditions imposed"
return string.format("HCE Review → SCT (72h) → SAC (≤5d) — %s (Art X.3; Annex G; WL-03 liaison)", outcome)
else
local level = (sector=="Sacred Buffer Approaches" or sector=="Environmental Buffer") and "SAC Referral"
or ((math.random()<0.5) and "SCT Admin Review" or "PSC reconciliation")
local outcome = (level=="PSC reconciliation") and "notice acknowledged"
or ((math.random()<0.5) and "clearance granted" or "conditions imposed")
local cite = (level=="SAC Referral") and "Annex F (SAC)" or "Annex F (SCT)"
return string.format("%s — %s (%s)", level, outcome, cite)
end
end
local function hceRef(dateInfo, corridor)
return string.format("HCE-%03d.%03d-%s", dateInfo.year, dateInfo.dayOfYear, corridor.code)
end
local function pickServedDistrict(corr)
local dname = corr.districts[math.random(#corr.districts)]
local kind = districtKindByName[dname] or "NZ"
return dname, kind
end
local function buildTriggers(flag, sector, cargo, tag, partner, wlEligible, wl03note, riskPush, flagStage)
local t = {}
if flag.risky then table.insert(t, "risky flag") end
if sector == "Sacred Buffer Approaches" or sector == "Environmental Buffer" then
table.insert(t, "sacred/ecology buffer")
end
if cargo and cargo.dual then table.insert(t, "dual-use cargo") end
if cargo and (cargo.risk or 0) >= 0.12 then table.insert(t, "cargo risk ≥0.12") end
if tag and tag.name ~= "standard" then table.insert(t, "non-standard tag: "..tag.name) end
if partner and (partner.risk or 0) >= 0.09 then table.insert(t, "partner risk ≥0.09") end
if wlEligible then table.insert(t, "WL pre-cleared") end
if wl03note == "WL-03 not signed (M-AMBER)" then table.insert(t, "WL-03 unsigned (M-AMBER)") end
if wl03note == "WL-03 cooldown (90 days)" then table.insert(t, "WL-03 cooldown") end
if flagStage and flagStage > 0 then table.insert(t, "policy stage "..flagStage) end
if riskPush and riskPush >= 2 then table.insert(t, "multi-trigger escalation") end
if #t == 0 then return "none" end
return table.concat(t, "; ")
end
-------------------------------------------------------------------------------
-- (E) MAIN GENERATION (PH <=10%; no Notes column)
-------------------------------------------------------------------------------
function p.makeTable(frame)
local dateInfo = getPSSCDate()
local dailySeed = dateInfo.year*1000 + dateInfo.dayOfYear
math.randomseed(dailySeed)
local rows = tonumber(frame and frame.args and frame.args.rows) or 36
local globalStage = tonumber(frame and frame.args and frame.args.policy_stage) or 0
local showLegend = (frame and frame.args and tostring(frame.args.showlegend) or "yes"):lower() ~= "no"
-- PH toggles (cap fixed at 10%)
local phOn = (frame and frame.args and tostring(frame.args.ph) or "on"):lower() ~= "off"
local phRateArg = tonumber(frame and frame.args and frame.args.phrate) or 0.10
if phRateArg < 0 then phRateArg = 0 end
if phRateArg > 0.10 then phRateArg = 0.10 end
local showPHLeg = (frame and frame.args and tostring(frame.args.showphlegend) or "yes"):lower() ~= "no"
local phCap = math.floor(rows * 0.10)
-- Pass 1: compose rows and score PH
local rowObjs = {}
for i = 1, rows do
math.randomseed(dailySeed + i*97)
local sector = pick(sectors)
-- NEW: sector-aware picks
local flag = pickWeightedFiltered(flags, function(f)
return flagAllowedInSector(sector, f.name)
end)
local vessel = pickFiltered(vessels, function(v)
return vesselAllowedInSector(sector, v)
end)
local cargo = pickWeighted(cargoTypes)
local tag = pickWeighted(cargoTags)
local partner = pickWeighted(partnerProfiles)
local wlOK, wl03note = wlEligibility(cargo, tag, flag, partner)
local wlEligible = wlOK
local corridor = wlEligible and wlCorridors[math.random(#wlCorridors)] or nil
local obs = wlEligible and observers[math.random(#observers)] or nil
local flagStage = getFlagStage(flag.name, globalStage)
local function baseProb()
local q = 0.20
if flag.risky then q = q + 0.05 end
if sector == "Sacred Buffer Approaches" or sector == "Environmental Buffer" then q = q + 0.08 end
q = q + (cargo and cargo.risk or 0)
if cargo and cargo.dual then q = q + 0.12 end
q = q + (tag and tag.risk or 0)
q = q + (partner and partner.risk or 0)
if wlEligible then q = math.max(q - 0.08, 0.08) end
local isIF = flag.name == "Imperial Federation"
local isMT = flag.name:match("^Matamoros")
if (isIF or isMT) and not wlEligible then
if flagStage >= 1 then q = q + 0.04 end
if flagStage >= 2 then q = q + 0.04 end
end
if flag.name:match("Matamoros") and wl03note == "WL-03 not signed (M-AMBER)" then
q = q + 0.08
end
if flag.name == "Bassaridian-flag" then
q = q - 0.18
if wlEligible then q = q - 0.04 end
if q < 0.02 then q = 0.02 end
if q > 0.08 then q = 0.08 end
end
return math.min(q, 0.72)
end
local riskPush = 0
if flag.name:match("Imperial Federation") or flag.name:match("^Matamoros") then riskPush = riskPush + 1 end
if cargo.dual or (cargo.risk or 0) >= 0.12 then riskPush = riskPush + 1 end
if tag.name ~= "standard" then riskPush = riskPush + 1 end
if (partner.risk or 0) >= 0.09 then riskPush = riskPush + 1 end
if wlEligible then riskPush = math.max(riskPush - 1, 0) end
local score, phCode, phText = phScore(sector, flag, vessel, cargo, tag, partner)
score = score + phRateArg * 0.5
rowObjs[i] = {
i=i, sector=sector, flag=flag, vessel=vessel, cargo=cargo, tag=tag, partner=partner,
wlEligible = wlEligible, wl03note = wl03note, corridor = corridor, obs = obs,
flagStage = flagStage, enforceProb = baseProb(), riskPush = riskPush,
phScore = score, phCode = phCode, phText = phText
}
end
-- Select top K PH rows
if phOn and phCap > 0 then
table.sort(rowObjs, function(a,b) return a.phScore > b.phScore end)
local applied = 0
for _,r in ipairs(rowObjs) do
if r.phCode and applied < phCap and r.phScore > 0 then
r.phApplied = true
applied = applied + 1
else
r.phApplied = false
end
end
table.sort(rowObjs, function(a,b) return a.i < b.i end)
else
for _,r in ipairs(rowObjs) do r.phApplied = false end
end
-- Pass 2: render (no Notes column)
local out = {}
table.insert(out, "{| class='wikitable sortable'")
if phOn then
table.insert(out, "! Date !! Sector !! Vessel/Flag !! Action !! Legal Basis !! Outcome !! Appeals/Review (Straits Conventions) !! Case ID !! PH Case !! Public Health Advisory !! Triggers")
else
table.insert(out, "! Date !! Sector !! Vessel/Flag !! Action !! Legal Basis !! Outcome !! Appeals/Review (Straits Conventions) !! Case ID !! Triggers")
end
for _,r in ipairs(rowObjs) do
math.randomseed(dailySeed + r.i*131)
local action, legal, outcome, appealCol
local dname, dkind
if r.phApplied then
action = "Public health interdiction ("..r.phCode..")"
legal = legalBasis(r.sector, false, r.wl03note, true)
outcome = "Public health hold; extended review"
appealCol = "SAC Referral — health safeguards (Annex D)"
else
if r.wlEligible then
dname, dkind = pickServedDistrict(r.corridor)
action = "WL transit / pilotage verification"
legal = legalBasis(r.sector, true, r.wl03note, false)
else
action = (r.wl03note == "WL-03 not signed (M-AMBER)" or r.wl03note == "WL-03 cooldown (90 days)")
and "WL-ineligible: standard PSC applied"
or "Cargo manifest reconciliation audit"
legal = legalBasis(r.sector, false, r.wl03note, false)
end
local enforce = r.enforceProb
if math.random() > enforce then
outcome = r.wlEligible and "Cleared WL window" or "No reports"
appealCol = r.wlEligible and "N/A (WL cleared)" or "N/A"
else
outcome = r.wlEligible and "WL reconciliation check; monitoring" or "Administrative notice issued; monitoring"
if r.riskPush >= 2 and math.random() < 0.65 then
outcome = (r.wlEligible and "WL hold; clearance pending expedited review" or "Random audit hold; clearance pending")
if r.flag.name == "Imperial Federation" or r.flag.name:match("^Matamoros") then
outcome = outcome .. " [Extended regulatory review]"
end
end
local isIF = r.flag.name == "Imperial Federation"
local isMT = r.flag.name:match("^Matamoros")
if r.flagStage >= 3 and (isIF or isMT) and not r.wlEligible and r.riskPush >= 2 then
outcome = "Denial of passage (policy Stage III)"
appealCol = "SCT injunction eligible | Appeals Case pending"
end
if r.flag.name == "Bassaridian-flag" and outcome:match("hold") then
outcome = r.wlEligible and "WL reconciliation complete; cleared same window" or "Administrative reconciliation complete; cleared"
appealCol = r.wlEligible and "N/A (WL cleared)" or "N/A"
end
appealCol = appealCol or appealsStatus(r.sector, r.wlEligible, r.tag, r.cargo)
end
end
local caseId = string.format("CASE-%03d.%03d-%02d", dateInfo.year, dateInfo.dayOfYear, r.i)
local phCaseId = "—"
local phText = "No concern"
if phOn and r.phApplied then
phCaseId = string.format("PH-CASE-%03d.%03d-%02d-%s", dateInfo.year, dateInfo.dayOfYear, r.i, r.phCode or "PH")
phText = r.phText or "Public health concern"
end
local triggers = buildTriggers(r.flag, r.sector, r.cargo, r.tag, r.partner, r.wlEligible, r.wl03note, r.riskPush, r.flagStage)
if outcome:lower():match("hold") or outcome:lower():match("conditions") or outcome:lower():match("monitoring") or outcome:lower():match("denial") then
appealCol = (appealCol ~= "N/A" and (appealCol .. " | ")) or ""
appealCol = appealCol .. ("Appeals Case: APP-" .. caseId)
end
local style
local o = outcome:lower()
if r.phApplied or o:match("hold") or o:match("denial") or o:match("detention") or o:match("impound") or o:match("extended regulatory review") then
style = ' style="background-color:#f8d7da;"'
elseif o:match("administrative notice") or o:match("monitoring") or o:match("conditions") or o:match("reconciliation") then
style = ' style="background-color:#fff3cd;"'
else
style = ' style="background-color:#d4edda;"'
end
table.insert(out, "|-" .. style)
if phOn then
table.insert(out, string.format("| %s || %s || %s (%s) || %s || %s || %s || %s || %s || %s || %s || %s",
dateInfo.text, r.sector, r.vessel, r.flag.name, action, legal, outcome, appealCol, caseId, phCaseId, phText, triggers))
else
table.insert(out, string.format("| %s || %s || %s (%s) || %s || %s || %s || %s || %s || %s",
dateInfo.text, r.sector, r.vessel, r.flag.name, action, legal, outcome, appealCol, caseId, triggers))
end
end
table.insert(out, "|}")
if showLegend then
table.insert(out, "\n<div style='margin-top:0.5em; font-size:90%;'>")
table.insert(out, "<b>Legend:</b> ")
table.insert(out, "<span style='background:#d4edda;padding:2px 6px;border:1px solid #c3e6cb;'>Green</span> = Cleared/Compliant ")
table.insert(out, "<span style='background:#fff3cd;padding:2px 6px;border:1px solid #ffeeba;'>Yellow</span> = Administrative/Conditional/Monitoring ")
table.insert(out, "<span style='background:#f8d7da;padding:2px 6px;border:1px solid #f5c6cb;'>Red</span> = Hold/Denial/Extended Review")
table.insert(out, "<br/><i>Policy Stage (per flag):</i> I = Audit emphasis; II = +Market surcharge advisory; III = limited denial on high-risk, non-WL rows.")
table.insert(out, "</div>")
end
if phOn then
table.insert(out, "\n<div style='margin-top:0.3em; font-size:90%;'><b>Public Health Advisory Codes:</b> ")
local parts = {}
for _,d in ipairs(phDiseases) do
table.insert(parts, string.format("<span style='white-space:nowrap;'><b>%s</b> – %s</span>", d.code, d.name))
end
table.insert(out, table.concat(parts, " "))
table.insert(out, "</div>")
end
return table.concat(out, "\n")
end
return p