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


-- White-Lane corridors tied to districts
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:
}
}


-- WL-03 participation pledge registry (true = signed; false = unsigned)
local wl03Signers = {
local wl03Signers = {
     ["Matamoros (IF)"] = false,       -- M-AMBER until pledge signed
     ["Matamoros (IF)"] = false,
     ["Imperial Federation"] = true,   -- signed/participating
     ["Imperial Federation"] = true,
     ["Bassaridian-flag"] = true,
     ["Bassaridian-flag"] = true,
     ["Neutral Merchant"] = true,
     ["Neutral Merchant"] = true,
Line 133: Line 130:


-------------------------------------------------------------------------------
-------------------------------------------------------------------------------
-- (B2) PUBLIC HEALTH: DISEASES & CONTROLS (same canon as outbreaks modules)
-- (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


-- Decide if a row merits a PH advisory; return code and text or nil
-- Score + candidate (no side effects). Returns score, code, text or 0,nil,nil
local function healthConcern(sector, flag, vessel, cargo, tag, partner, riskPush, rnd, baseRate)
local function phScore(sector, flag, vessel, cargo, tag, partner)
    -- baseRate is a small background chance when triggers are weak (default 0.35)
     local score = 0
     local score = 0
    -- Sector effects (coastal, sacred, environmental buffers)
    if sector == "Environmental Buffer" or sector == "Sacred Buffer Approaches"
      or sector:find("Strait") or sector:find("Approach") then
        score = score + 1
    end
    -- Cargo/tag effects
     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 == "Aviation fuels & additives" or cname == "Refined petroleum products"
    -- solvents/oxidizers/fuels
      or cname == "Hydraulic lubricants (dual-use)" or cname:find("oxidizers") then score = score + 2 end
     if cname:find("oxidizers") or cname:find("lubricants") or cname:find("petroleum") or cname:find("fuels") then score = score + 2 end
     -- Crowding signals (ferries, expedited/emergency)
     -- 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 risk
     -- 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
    -- Enforcement multi-trigger
    score = score + (riskPush or 0) * 0.5


    -- Choose disease based on dominant trigger theme
     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
        if not d then return 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
    -- Rare escalation: White Pestilence screening on very high scores
     if (cname == "Agricultural commodities" or tname == "perishable") then return pick("SY") end
     if score >= 5 and rnd() < 0.10 then
     if cname == "Relief supplies (mixed pallets)" then return pick("PF") end
        return pick("WP")
     if sector == "Environmental Buffer" or sector:find("Strait") then return pick("TR") end
    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
    -- Food/water hygiene path
     if vessel == "Ro-Ro ferry" and (tname == "expedited" or tname == "emergency") then return pick("FB") end
     if (cname == "Agricultural commodities" or tname == "perishable") and rnd() < 0.75 then
     if (partner and partner.risk or 0) >= 0.09 then return pick("BN") end
        return pick("SY")
     return 0,nil,nil
    end
     if (cname == "Relief supplies (mixed pallets)" or score >= 3) and rnd() < 0.40 then
        return pick("PF")
    end
 
    -- Waterborne/coastal path
     if (sector == "Environmental Buffer" or sector:find("Strait")) and rnd() < 0.50 then
        return pick("TR")
    end
     if sector == "Archipelago Zone" and rnd() < 0.30 then
        return pick("HS")
    end
 
    -- Solvent/oxidizer/industrial exposure
     if (cname:find("oxidizers") or cname:find("lubricants") or cname:find("petroleum") or cname:find("fuels")) and rnd() < 0.70 then
        return pick("CC")
    end
 
    -- Crowd respiratory
     if vessel == "Ro-Ro ferry" and (tname == "expedited" or tname == "emergency") and rnd() < 0.60 then
        return pick("FB")
    end
     if partner and partner.risk >= 0.09 and rnd() < 0.30 then
        return pick("BN")
    end
 
     -- Low background chance
    if rnd() < (baseRate or 0.35) then
        -- pick a mild, generic hygiene signal weighted toward SY/PF
        local r = rnd()
        if r < 0.45 then return pick("SY")
        elseif r < 0.70 then return pick("PF")
        elseif r < 0.85 then return pick("TR")
        else return pick("BN") end
    end
 
    return nil
end
end


-------------------------------------------------------------------------------
-------------------------------------------------------------------------------
-- (C) POLICY STAGES (per-flag mapping)
-- (C) POLICY STAGES
-------------------------------------------------------------------------------
-------------------------------------------------------------------------------
local stageMap = {
local stageMap = {
     ["Imperial Federation"] = 1, -- Stage I (audit emphasis)
     ["Imperial Federation"] = 1,
     ["Matamoros (IF)"]      = 3, -- Stage III (limited denial on high-risk, non-WL rows)
     ["Matamoros (IF)"]      = 3,
}
}
local function getFlagStage(flagName, globalStage)
local function getFlagStage(flagName, globalStage)
Line 260: Line 213:
end
end


-- WL eligibility (Annex G) + WL-03 participation + 90-day cooldown
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()
     math.randomseed(dateInfo.year*1000 + dateInfo.dayOfYear)
     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"


     -- Public health toggles
     -- PH toggles
     local phOn       = (frame and frame.args and tostring(frame.args.ph) or "on"):lower() ~= "off"
     local phOn       = (frame and frame.args and tostring(frame.args.ph) or "on"):lower() ~= "off"
     local phRate      = tonumber(frame and frame.args and frame.args.phrate) or 0.35
     local phRateArg  = tonumber(frame and frame.args and frame.args.phrate) or 0.10  -- soft bias
     local showPHLeg   = (frame and frame.args and tostring(frame.args.showphlegend) or "yes"):lower() ~= "no"
    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 out = {}
     -- Hard cap: <= 10%
     table.insert(out, "{| class='wikitable sortable'")
     local phCap = math.floor(rows * 0.10)
    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


    -- 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
        -- Per-flag stage (overrides global when present)
         local flagStage = getFlagStage(flag.name, globalStage)
         local flagStage = getFlagStage(flag.name, globalStage)


         -- Base enforcement probability
         -- 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


            -- Per-flag policy stages apply to non-WL rows only
             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
            -- M-AMBER effect
             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
            -- Bassaridian-flag compliance
             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


         local enforce = baseProb()
         -- riskPush for transparency & later
        local action, legal, outcome, notes, appealCol
        local dname, dkind
 
        if wlEligible then
            dname, dkind = pickServedDistrict(corridor)
            action = "WL transit / pilotage verification"
            legal  = legalBasis(sector, true, wl03note)
            notes  = string.format(
                "WL %s (%s) | window %s | District: %s [%s] | Observer: %s | %s | Cargo: %s [%s]; Partner: %s | WL-03: %s",
                corridor.code, corridor.name, corridor.win, dname, dkind, (obs or "n/a"),
                hceRef(dateInfo, corridor), cargo.name, tag.name, partner.code, wl03note
            )
        else
            action = (wl03note == "WL-03 not signed (M-AMBER)" or wl03note == "WL-03 cooldown (90 days)")
                    and "WL-ineligible: standard PSC applied"
                    or  "Cargo manifest reconciliation audit"
            legal  = legalBasis(sector, false, wl03note)
 
            local advisory = ""
            local isIF = flag.name == "Imperial Federation"
            local isMT = flag.name:match("^Matamoros")
            if (isIF or isMT) and flagStage >= 1 then
                advisory = " | Audit emphasis (Stage I)"
                if flagStage >= 2 then advisory = advisory .. " | Market surcharge advisory (Stage II)" end
            end
 
            local mAmber = (flag.name:match("Matamoros") and 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",
                                  cargo.name, tag.name, partner.code, wl03note, mAmber, advisory)
        end
 
        -- Compute enforcement result
         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


         if math.random() > enforce then
         -- PH candidate scoring
             outcome  = wlEligible and "Cleared WL window" or "No reports"
        local score, phCode, phText = phScore(sector, flag, vessel, cargo, tag, partner)
             appealCol = wlEligible and "N/A (WL cleared)" or "N/A"
        -- nudge by phrate (tiny bias), still bounded by later cap
         else
        score = score + phRateArg * 0.5
            outcome = wlEligible and "WL reconciliation check; monitoring" or "Administrative notice issued; monitoring"
 
            if riskPush >= 2 and math.random() < 0.65 then
        rowObjs[i] = {
                outcome = (wlEligible and "WL hold; clearance pending expedited review" or "Random audit hold; clearance pending")
            i=i, sector=sector, flag=flag, vessel=vessel, cargo=cargo, tag=tag, partner=partner,
                if flag.name == "Imperial Federation" or flag.name:match("^Matamoros") then
             wlEligible = wlEligible, wl03note = wl03note, corridor = corridor, obs = obs,
                    outcome = outcome .. " [Extended regulatory review]"
             flagStage = flagStage, enforceProb = baseProb(), riskPush = riskPush,
                 end
            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)


             -- Stage III denial logic
        local action, legal, outcome, notes, appealCol
             local isIF = flag.name == "Imperial Federation"
        local dname, dkind
             local isMT = flag.name:match("^Matamoros")
 
             if flagStage >= 3 and (isIF or isMT) and not wlEligible and riskPush >= 2 then
        if r.phApplied then
                 outcome = "Denial of passage (policy Stage III)"
             -- Override: Public Health interdiction replaces normal action
                 appealCol = "SCT injunction eligible | Appeals Case pending"
             action  = "Public health interdiction ("..r.phCode..")"
            end
            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)


            -- Bassaridian-flag safeguard
                local advisory = ""
            if flag.name == "Bassaridian-flag" and outcome:match("hold") then
                local isIF = r.flag.name == "Imperial Federation"
                outcome  = wlEligible and "WL reconciliation complete; cleared same window" or "Administrative reconciliation complete; cleared"
                local isMT = r.flag.name:match("^Matamoros")
                 appealCol = wlEligible and "N/A (WL cleared)" or "N/A"
                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


             appealCol = appealCol or appealsStatus(sector, wlEligible, tag, cargo)
             -- Normal enforcement result (PH rows skip this path)
        end
             local enforce = r.enforceProb
 
             if math.random() > enforce then
        -- Public Health advisory (deterministic per row)
                outcome  = r.wlEligible and "Cleared WL window" or "No reports"
        local phCaseId, phText = "", ""
                 appealCol = r.wlEligible and "N/A (WL cleared)" or "N/A"
        if phOn then
            -- stabilize PH random draws per-row by reseeding on top of the daily seed
             local rowSeed = (dateInfo.year*1000 + dateInfo.dayOfYear) + i*131
             math.randomseed(rowSeed)
            local function rnd() return math.random() end
            local code, text = healthConcern(sector, flag, vessel, cargo, tag, partner, riskPush, rnd, phRate)
            if code and text then
                 phCaseId = string.format("PH-CASE-%03d.%03d-%02d-%s", dateInfo.year, dateInfo.dayOfYear, i, code)
                phText  = text
             else
             else
                 phCaseId = "—"
                 outcome = r.wlEligible and "WL reconciliation check; monitoring" or "Administrative notice issued; monitoring"
                 phText  = "No concern"
                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
            -- restore RNG progression for subsequent steps
            math.randomseed(dateInfo.year*1000 + dateInfo.dayOfYear)
            -- reseed forward to where we were (cheap deterministic shim)
            for _=1,i*7 do math.random() end
         end
         end


         -- Transparency fields
         -- 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)


         -- If there is any reviewable action, bind an appeals case stub
         -- 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


         -- Color-code the row and output
         -- Row color (PH rows always red)
         local style = (function()
         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
                 return ' style="background-color:#f8d7da;"'
                 style = ' style="background-color:#f8d7da;"'
             end
             elseif o:match("administrative notice") or o:match("monitoring") or o:match("conditions") or o:match("reconciliation")
            if 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
                 return ' style="background-color:#fff3cd;"'
                 style = ' style="background-color:#fff3cd;"'
            else
                style = ' style="background-color:#d4edda;"'
             end
             end
            return ' style="background-color:#d4edda;"'
         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 &nbsp; ")
        table.insert(out, "<span style='background:#fff3cd;padding:2px 6px;border:1px solid #ffeeba;'>Yellow</span> = Administrative/Conditional/Monitoring &nbsp; ")
        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, " &nbsp; "))
        table.insert(out, "</div>")
    end

    return table.concat(out, "\n")
end

return p