Module:StraitsDaily: Difference between revisions
From MicrasWiki
Jump to navigationJump to search
NewZimiaGov (talk | contribs) No edit summary |
NewZimiaGov (talk | contribs) No edit summary |
||
| Line 1: | Line 1: | ||
-- Module: StraitsDaily | -- Module: StraitsDaily | ||
-- Purpose: Daily, in-world report of Strait of Haifa enforcement [[Straits Conventions of 52.06 PSSC]] | -- Purpose: Daily, in-world report of Strait of Haifa enforcement [[Straits Conventions of 52.06 PSSC]] | ||
local p = {} | local p = {} | ||
| Line 107: | Line 106: | ||
for _,d in ipairs(corumDistricts) do districtKindByName[d.name] = d.kind end | for _,d in ipairs(corumDistricts) do districtKindByName[d.name] = d.kind end | ||
local wlCorridors = { | local wlCorridors = { | ||
{ code="WL-NZ-1", name="New Zimian Humanitarian Belt", win="0700–1100", kind="NZ", | { code="WL-NZ-1", name="New Zimian Humanitarian Belt", win="0700–1100", kind="NZ", | ||
| Line 122: | Line 120: | ||
} | } | ||
local wl03Signers = { | local wl03Signers = { | ||
["Matamoros (IF)"] = false, | ["Matamoros (IF)"] = false, | ||
["Imperial Federation"] = true, | ["Imperial Federation"] = true, | ||
["Bassaridian-flag"] = true, | ["Bassaridian-flag"] = true, | ||
["Neutral Merchant"] = true, | ["Neutral Merchant"] = true, | ||
| Line 133: | Line 130: | ||
------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ||
-- (B2) PUBLIC HEALTH: DISEASES | -- (B2) PUBLIC HEALTH: DISEASES (same canon) | ||
------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ||
local phDiseases = { | local phDiseases = { | ||
| Line 154: | Line 151: | ||
for _,d in ipairs(phDiseases) do phIndex[d.code] = d end | for _,d in ipairs(phDiseases) do phIndex[d.code] = d end | ||
-- | -- Score + candidate (no side effects). Returns score, code, text or 0,nil,nil | ||
local function | local function phScore(sector, flag, vessel, cargo, tag, partner) | ||
local score = 0 | local score = 0 | ||
local cname = cargo and cargo.name or "" | local cname = cargo and cargo.name or "" | ||
local tname = tag and tag.name or "standard" | local tname = tag and tag.name or "standard" | ||
-- geography | |||
if sector == "Environmental Buffer" or sector == "Sacred Buffer Approaches" | |||
or sector:find("Strait") or sector:find("Approach") then score = score + 1 end | |||
-- food/water hygiene | |||
if cname == "Agricultural commodities" or tname == "perishable" then score = score + 2 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 == "Relief supplies (mixed pallets)" then score = score + 1 end | ||
if cname | -- solvents/oxidizers/fuels | ||
if cname:find("oxidizers") or cname:find("lubricants") or cname:find("petroleum") or cname:find("fuels") then score = score + 2 end | |||
-- | -- crowd ferries | ||
if vessel == "Ro-Ro ferry" and (tname == "expedited" or tname == "emergency") then score = score + 2 end | if vessel == "Ro-Ro ferry" and (tname == "expedited" or tname == "emergency") then score = score + 2 end | ||
-- | -- partner/flag | ||
if (partner and partner.risk or 0) >= 0.09 then score = score + 1 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 | if flag and flag.risky then score = score + 0.5 end | ||
local function pick(code) | local function pick(code) | ||
local d = phIndex[code] | local d = phIndex[code]; if not d then return 0,nil,nil end | ||
return score, d.code, (d.code .. " ("..d.name.."): " .. d.controls) | |||
return d.code, (d.code .. " ("..d.name.."): " .. d.controls) | |||
end | end | ||
if score >= 5 then return pick("WP") end -- rare escalation | |||
if (cname == "Agricultural commodities" or tname == "perishable") then return pick("SY") end | |||
if score >= 5 | 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 == "Archipelago Zone" then return pick("HS") end | |||
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 (cname == "Agricultural commodities" or tname == "perishable") | if (partner and partner.risk or 0) >= 0.09 then return pick("BN") end | ||
return 0,nil,nil | |||
if | |||
if | |||
if sector == "Archipelago Zone" | |||
if (cname:find("oxidizers") or cname:find("lubricants") or cname:find("petroleum") or cname:find("fuels")) | |||
if vessel == "Ro-Ro ferry" and (tname == "expedited" or tname == "emergency") | |||
if partner and partner.risk >= 0.09 | |||
end | end | ||
------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ||
-- (C) POLICY STAGES | -- (C) POLICY STAGES | ||
------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ||
local stageMap = { | local stageMap = { | ||
["Imperial Federation"] = 1, | ["Imperial Federation"] = 1, | ||
["Matamoros (IF)"] = 3, | ["Matamoros (IF)"] = 3, | ||
} | } | ||
local function getFlagStage(flagName, globalStage) | local function getFlagStage(flagName, globalStage) | ||
| Line 260: | Line 213: | ||
end | end | ||
local function wlEligibility(cargo, tag, flag, partner) | local function wlEligibility(cargo, tag, flag, partner) | ||
if not cargo or not tag then return false, "n/a" end | if not cargo or not tag then return false, "n/a" end | ||
| Line 282: | Line 234: | ||
end | end | ||
local function legalBasis(sector, wl, wl03note) | 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 | if wl then | ||
return "Art III.5 (White-Lane); Annex G; Art X.3 (expedited remedies); HCE Circular WL-03 (" .. wl03note .. ")" | return "Art III.5 (White-Lane); Annex G; Art X.3 (expedited remedies); HCE Circular WL-03 (" .. wl03note .. ")" | ||
| Line 342: | Line 297: | ||
------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ||
-- (E) MAIN GENERATION | -- (E) MAIN GENERATION (now two-pass to cap PH at 10%) | ||
------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ||
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 rows = tonumber(frame and frame.args and frame.args.rows) or 18 | local rows = tonumber(frame and frame.args and frame.args.rows) or 18 | ||
| Line 352: | Line 308: | ||
local showLegend = (frame and frame.args and tostring(frame.args.showlegend) or "yes"):lower() ~= "no" | local showLegend = (frame and frame.args and tostring(frame.args.showlegend) or "yes"):lower() ~= "no" | ||
-- | -- PH toggles | ||
local phOn | local phOn = (frame and frame.args and tostring(frame.args.ph) or "on"):lower() ~= "off" | ||
local | local phRateArg = tonumber(frame and frame.args and frame.args.phrate) or 0.10 -- soft bias | ||
local showPHLeg | 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" | |||
-- Hard cap: <= 10% | |||
local phCap = math.floor(rows * 0.10) | |||
-- Pass 1: build row objects and PH candidates (deterministic) | |||
local rowObjs = {} | |||
for i = 1, rows do | for i = 1, rows do | ||
-- deterministic per-row sampling | |||
math.randomseed(dailySeed + i*97) | |||
local sector = pick(sectors) | local sector = pick(sectors) | ||
local flag = pickWeighted(flags) | local flag = pickWeighted(flags) | ||
| Line 377: | Line 335: | ||
local corridor = wlEligible and wlCorridors[math.random(#wlCorridors)] or nil | local corridor = wlEligible and wlCorridors[math.random(#wlCorridors)] or nil | ||
local obs = wlEligible and observers[math.random(#observers)] or nil | local obs = wlEligible and observers[math.random(#observers)] or nil | ||
local flagStage = getFlagStage(flag.name, globalStage) | local flagStage = getFlagStage(flag.name, globalStage) | ||
-- | -- Enforcement baseline (as before) | ||
local function baseProb() | local function baseProb() | ||
local q = 0.28 | local q = 0.28 | ||
| Line 392: | Line 348: | ||
if wlEligible then q = math.max(q - 0.06, 0.10) end | if wlEligible then q = math.max(q - 0.06, 0.10) end | ||
local isIF = flag.name == "Imperial Federation" | local isIF = flag.name == "Imperial Federation" | ||
local isMT = flag.name:match("^Matamoros") | local isMT = flag.name:match("^Matamoros") | ||
| Line 399: | Line 354: | ||
if flagStage >= 2 then q = q + 0.04 end | if flagStage >= 2 then q = q + 0.04 end | ||
end | end | ||
if flag.name:match("Matamoros") and wl03note == "WL-03 not signed (M-AMBER)" then | if flag.name:match("Matamoros") and wl03note == "WL-03 not signed (M-AMBER)" then | ||
q = q + 0.08 | q = q + 0.08 | ||
end | end | ||
if flag.name == "Bassaridian-flag" then | if flag.name == "Bassaridian-flag" then | ||
q = q - 0.18 | q = q - 0.18 | ||
| Line 415: | Line 366: | ||
end | end | ||
-- riskPush for transparency & later | |||
local riskPush = 0 | local riskPush = 0 | ||
if flag.name:match("Imperial Federation") or flag.name:match("^Matamoros") then riskPush = riskPush + 1 end | if flag.name:match("Imperial Federation") or flag.name:match("^Matamoros") then riskPush = riskPush + 1 end | ||
| Line 455: | Line 374: | ||
if wlEligible then riskPush = math.max(riskPush - 1, 0) end | if wlEligible then riskPush = math.max(riskPush - 1, 0) end | ||
-- PH candidate scoring | |||
local score, phCode, phText = phScore(sector, flag, vessel, cargo, tag, partner) | |||
-- nudge by phrate (tiny bias), still bounded by later cap | |||
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 (cap <= 10%) | |||
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 | ||
end | |||
table.sort(rowObjs, function(a,b) return a.i < b.i end) -- restore original order | |||
else | |||
for _,r in ipairs(rowObjs) do r.phApplied = false end | |||
end | |||
-- Pass 2: render | |||
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 !! Notes") | |||
else | |||
table.insert(out, "! Date !! Sector !! Vessel/Flag !! Action !! Legal Basis !! Outcome !! Appeals/Review (Straits Conventions) !! Case ID !! Triggers !! Notes") | |||
end | |||
for _,r in ipairs(rowObjs) do | |||
math.randomseed(dailySeed + r.i*131) | |||
-- | local action, legal, outcome, notes, appealCol | ||
local dname, dkind | |||
local | |||
if | if r.phApplied then | ||
-- Override: Public Health interdiction replaces normal action | |||
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)" | |||
-- concise context in notes; if WL-eligible, note deferment | |||
local wlNote = r.wlEligible and " | WL transit deferred for health clearance" or "" | |||
notes = string.format("PH measures: %s%s", r.phText or "n/a", wlNote) | |||
else | |||
if r.wlEligible then | |||
dname, dkind = pickServedDistrict(r.corridor) | |||
action = "WL transit / pilotage verification" | |||
legal = legalBasis(r.sector, true, r.wl03note, false) | |||
notes = string.format( | |||
"WL %s (%s) | window %s | District: %s [%s] | Observer: %s | %s | Cargo: %s [%s]; Partner: %s | WL-03: %s", | |||
r.corridor.code, r.corridor.name, r.corridor.win, dname, dkind, (r.obs or "n/a"), | |||
hceRef(getPSSCDate(), r.corridor), r.cargo.name, r.tag.name, r.partner.code, r.wl03note | |||
) | |||
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) | |||
local advisory = "" | |||
local isIF = r.flag.name == "Imperial Federation" | |||
local isMT = r.flag.name:match("^Matamoros") | |||
if (isIF or isMT) and r.flagStage >= 1 then | |||
advisory = " | Audit emphasis (Stage I)" | |||
if r.flagStage >= 2 then advisory = advisory .. " | Market surcharge advisory (Stage II)" end | |||
end | |||
local mAmber = (r.flag.name:match("Matamoros") and r.wl03note == "WL-03 not signed (M-AMBER)") and " | Risk tag: M-AMBER" or "" | |||
notes = string.format("Cargo: %s [%s]; Partner: %s | WL-03: %s%s%s", | |||
r.cargo.name, r.tag.name, r.partner.code, r.wl03note, mAmber, advisory) | |||
end | end | ||
-- Normal enforcement result (PH rows skip this path) | |||
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" | |||
local | |||
else | 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 | ||
end | end | ||
-- | -- IDs and transparency | ||
local caseId = string.format("CASE-%03d.%03d-%02d", dateInfo.year, dateInfo.dayOfYear, i) | local caseId = string.format("CASE-%03d.%03d-%02d", dateInfo.year, dateInfo.dayOfYear, r.i) | ||
local triggers = buildTriggers(flag, sector, cargo, tag, partner, wlEligible, wl03note, riskPush, flagStage) | 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) | |||
-- | -- Append appeals case if reviewable | ||
if outcome:lower():match("hold") or outcome:lower():match("conditions") or outcome:lower():match("monitoring") or outcome:lower():match("denial") then | 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 ~= "N/A" and (appealCol .. " | ")) or "" | ||
| Line 515: | Line 499: | ||
end | end | ||
-- | -- Row color (PH rows always red) | ||
local style = | local style | ||
if r.phApplied then | |||
style = ' style="background-color:#f8d7da;"' | |||
else | |||
local o = outcome:lower() | local o = outcome:lower() | ||
local n = (notes or ""):lower() | local n = (notes or ""):lower() | ||
if o:match("hold") or o:match("denial") or o:match("detention") or o:match("impound") or n:match("extended regulatory review") then | if o:match("hold") or o:match("denial") or o:match("detention") or o:match("impound") or n: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") | |||
or n:match("wl%-ineligible") or n:match("risk tag: m%-amber") or n:match("surcharge advisory") then | or n:match("wl%-ineligible") or n:match("risk tag: m%-amber") or n:match("surcharge advisory") then | ||
style = ' style="background-color:#fff3cd;"' | |||
else | |||
style = ' style="background-color:#d4edda;"' | |||
end | end | ||
end | |||
end | |||
table.insert(out, "|-" .. style) | table.insert(out, "|-" .. style) | ||
if phOn then | if phOn then | ||
table.insert(out, string.format("| %s || %s || %s (%s) || %s || %s || %s || %s || %s || %s || %s || %s", | table.insert(out, string.format("| %s || %s || %s (%s) || %s || %s || %s || %s || %s || %s || %s || %s", | ||
dateInfo.text, sector, vessel, flag.name, action, legal, outcome, appealCol, caseId, phCaseId, phText, triggers, notes)) | dateInfo.text, r.sector, r.vessel, r.flag.name, action, legal, outcome, appealCol, caseId, phCaseId, phText, triggers, notes)) | ||
else | else | ||
table.insert(out, string.format("| %s || %s || %s (%s) || %s || %s || %s || %s || %s || %s", | table.insert(out, string.format("| %s || %s || %s (%s) || %s || %s || %s || %s || %s || %s", | ||
dateInfo.text, sector, vessel, flag.name, action, legal, outcome, appealCol, caseId, triggers, notes)) | dateInfo.text, r.sector, r.vessel, r.flag.name, action, legal, outcome, appealCol, caseId, triggers, notes)) | ||
end | end | ||
end | end | ||
Revision as of 19:29, 28 September 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 totalDaysElapsed = math.floor((now - startDate) / secondsInDay)
local yearFraction = totalDaysElapsed / 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","Sacred Buffer Approaches","Environmental Buffer","Northern Approach TSS","Southern Approach TSS"
}
local flags = {
{name="Bassaridian-flag", weight=6, risky=false},
{name="Moorland", weight=2, risky=false},
{name="Imperial Federation", weight=5, risky=true},
{name="Matamoros (IF)", weight=2, risky=true},
{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 (match MissionaryDaily "Zones")
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
local wlCorridors = {
{ code="WL-NZ-1", name="New Zimian Humanitarian Belt", win="0700–1100", kind="NZ",
districts={"Lyvian Zones","Lucinarian Zones","Brentonian Zones","Ossyani Zones"} },
{ code="WL-GM-2", name="Bayen Relief Spur", win="0900–1300", kind="GM",
districts={"Sinclarian Zones","Nortonian Zones","Wallisian Zones","Alindan Zones","Aijiac Zones"} },
{ code="WL-SEA-3", name="Corum Coastal Shuttle", win="0600–1800", 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)"] = false,
["Imperial Federation"] = true,
["Bassaridian-flag"] = true,
["Neutral Merchant"] = true,
["Moorland"] = true,
["Independent Coastal State"] = true
}
-------------------------------------------------------------------------------
-- (B2) PUBLIC HEALTH: DISEASES (same canon)
-------------------------------------------------------------------------------
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
-- Score + candidate (no side effects). Returns score, code, text or 0,nil,nil
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"
-- geography
if sector == "Environmental Buffer" or sector == "Sacred Buffer Approaches"
or sector:find("Strait") or sector:find("Approach") then score = score + 1 end
-- food/water hygiene
if cname == "Agricultural commodities" or tname == "perishable" then score = score + 2 end
if cname == "Relief supplies (mixed pallets)" then score = score + 1 end
-- solvents/oxidizers/fuels
if cname:find("oxidizers") or cname:find("lubricants") or cname:find("petroleum") or cname:find("fuels") then score = score + 2 end
-- crowd ferries
if vessel == "Ro-Ro ferry" and (tname == "expedited" or tname == "emergency") then score = score + 2 end
-- partner/flag
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 -- rare escalation
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 == "Archipelago Zone" then return pick("HS") end
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
-------------------------------------------------------------------------------
local stageMap = {
["Imperial Federation"] = 1,
["Matamoros (IF)"] = 3,
}
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
local function wlEligibility(cargo, tag, flag, partner)
if not cargo or not tag then return false, "n/a" end
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"
elseif not signed then
return false, "WL-03 not signed (M-AMBER)"
elseif cooling then
return false, "WL-03 cooldown (90 days)"
else
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.60
if tag and (tag.name=="emergency" or tag.name=="perishable") then relief = relief + 0.12 end
if cargo and cargo.dual then relief = relief - 0.15 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 (now two-pass to cap PH at 10%)
-------------------------------------------------------------------------------
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 18
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
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 -- soft bias
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"
-- Hard cap: <= 10%
local phCap = math.floor(rows * 0.10)
-- Pass 1: build row objects and PH candidates (deterministic)
local rowObjs = {}
for i = 1, rows do
-- deterministic per-row sampling
math.randomseed(dailySeed + i*97)
local sector = pick(sectors)
local flag = pickWeighted(flags)
local vessel = pick(vessels)
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)
-- Enforcement baseline (as before)
local function baseProb()
local q = 0.28
if flag.risky then q = q + 0.06 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.03 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.06, 0.10) 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
-- riskPush for transparency & later
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
-- PH candidate scoring
local score, phCode, phText = phScore(sector, flag, vessel, cargo, tag, partner)
-- nudge by phrate (tiny bias), still bounded by later cap
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 (cap <= 10%)
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) -- restore original order
else
for _,r in ipairs(rowObjs) do r.phApplied = false end
end
-- Pass 2: render
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 !! Notes")
else
table.insert(out, "! Date !! Sector !! Vessel/Flag !! Action !! Legal Basis !! Outcome !! Appeals/Review (Straits Conventions) !! Case ID !! Triggers !! Notes")
end
for _,r in ipairs(rowObjs) do
math.randomseed(dailySeed + r.i*131)
local action, legal, outcome, notes, appealCol
local dname, dkind
if r.phApplied then
-- Override: Public Health interdiction replaces normal action
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)"
-- concise context in notes; if WL-eligible, note deferment
local wlNote = r.wlEligible and " | WL transit deferred for health clearance" or ""
notes = string.format("PH measures: %s%s", r.phText or "n/a", wlNote)
else
if r.wlEligible then
dname, dkind = pickServedDistrict(r.corridor)
action = "WL transit / pilotage verification"
legal = legalBasis(r.sector, true, r.wl03note, false)
notes = string.format(
"WL %s (%s) | window %s | District: %s [%s] | Observer: %s | %s | Cargo: %s [%s]; Partner: %s | WL-03: %s",
r.corridor.code, r.corridor.name, r.corridor.win, dname, dkind, (r.obs or "n/a"),
hceRef(getPSSCDate(), r.corridor), r.cargo.name, r.tag.name, r.partner.code, r.wl03note
)
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)
local advisory = ""
local isIF = r.flag.name == "Imperial Federation"
local isMT = r.flag.name:match("^Matamoros")
if (isIF or isMT) and r.flagStage >= 1 then
advisory = " | Audit emphasis (Stage I)"
if r.flagStage >= 2 then advisory = advisory .. " | Market surcharge advisory (Stage II)" end
end
local mAmber = (r.flag.name:match("Matamoros") and r.wl03note == "WL-03 not signed (M-AMBER)") and " | Risk tag: M-AMBER" or ""
notes = string.format("Cargo: %s [%s]; Partner: %s | WL-03: %s%s%s",
r.cargo.name, r.tag.name, r.partner.code, r.wl03note, mAmber, advisory)
end
-- Normal enforcement result (PH rows skip this path)
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
-- IDs and transparency
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)
-- Append appeals case if reviewable
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
-- Row color (PH rows always red)
local style
if r.phApplied then
style = ' style="background-color:#f8d7da;"'
else
local o = outcome:lower()
local n = (notes or ""):lower()
if o:match("hold") or o:match("denial") or o:match("detention") or o:match("impound") or n: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")
or n:match("wl%-ineligible") or n:match("risk tag: m%-amber") or n:match("surcharge advisory") then
style = ' style="background-color:#fff3cd;"'
else
style = ' style="background-color:#d4edda;"'
end
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, notes))
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, notes))
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 showPHLeg and phOn then
table.insert(out, "\n<div style='margin-top:0.3em; font-size:90%;'>")
table.insert(out, "<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