Module:StraitsDaily: Difference between revisions

From MicrasWiki
Jump to navigationJump to search
NewZimiaGov (talk | contribs)
No edit summary
NewZimiaGov (talk | contribs)
No edit summary
 
(24 intermediate revisions by the same user not shown)
Line 1: Line 1:
-------------------------------------------------------------------------------
-- Module: StraitsDaily
-- Module: StraitsDaily
-- Purpose: Daily, in-world report of Strait of Haifa enforcement
-- Purpose: Daily, in-world report of Strait of Haifa enforcement [[Straits Conventions of 52.06 PSSC]]


local p = {}
local p = {}
Line 10: Line 9:
local function getPSSCDate()
local function getPSSCDate()
     local startDate = os.time({year = 1999, month = 8, day = 6})
     local startDate = os.time({year = 1999, month = 8, day = 6})
     local secondsInDay = 86400
     local secondsInDay = 86400
     local daysPerYear = 183
     local daysPerYear   = 183
     local now = os.time()
     local now           = os.time()
     local totalDaysElapsed = math.floor((now - startDate) / secondsInDay)
     local totalDays    = math.floor((now - startDate) / secondsInDay)


     local yearFraction = totalDaysElapsed / daysPerYear
     local yearFraction = totalDays / daysPerYear
     local psscYear     = math.floor(yearFraction)
     local psscYear     = math.floor(yearFraction)
     local dayOfYear   = math.floor((yearFraction - psscYear) * daysPerYear) + 1
     local dayOfYear     = math.floor((yearFraction - psscYear) * daysPerYear) + 1


     local month, dayOfMonth
     local month, dayOfMonth
Line 37: Line 36:
local sectors = {
local sectors = {
     "Strait South Inbound","Strait South Outbound","Strait North Inbound","Strait North Outbound",
     "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"
     "Archipelago Zone Inbound", "Archipelago Zone Outbound", "Sacred Buffer Approaches",
    "Environmental Buffer","Northern Approach TSS","Southern Approach TSS",
    "Northern Sea of Storms","Western Sea of Storms","Southern Sea of Storms",
}
}


-- Registries / flags (explicit IF/Matamoros allowed; bias is subtle and framed as process):
local flags = {
local flags = {
     {name="Bassaridian-flag",                     weight=6, risky=false},
     {name="Bassaridian-flag", weight=6, risky=false},
     {name="Moorland",                             weight=2, risky=false},
     {name="Moorland", weight=2, risky=false},
     {name="Imperial Federation",                 weight=5, risky=true}, -- slight audit bias later
     {name="Imperial Federation", weight=5, risky=false},
     {name="Matamoros (IF)",                       weight=2, risky=true}, -- slight audit bias later
     {name="Matamoros (IF)", weight=2, risky=false},
     {name="Neutral Merchant",                     weight=4, risky=false},
     {name="Neutral Merchant", weight=4, risky=false},
     {name="Distant Partner (Euranidom)",         weight=3, risky=true},
     {name="Diaspora Stakeholders- Euranidom", weight=3, risky=true},
     {name="Distant Partner (Corumia)",           weight=3, risky=true},
     {name="Diaspora Stakeholders- Corumia", weight=3, risky=true},
     {name="Distant Partner (Southern Keltiania)", weight=3, risky=true},
     {name="Diaspora Stakeholders- South Keltiania", weight=3, risky=true},
     {name="Consortium Registry",                 weight=2, risky=true},
     {name="Consortium Registry", weight=2, risky=true},
     {name="Other Coastal State",                 weight=7, risky=false},
     {name="Independent Coastal State", weight=7, risky=false}
 
}
}


Line 59: Line 60:
}
}


-- High-risk cargo categories (dual-use emphasized). Weight ~appearance frequency; risk amplifies enforcement odds.
local cargoTypes = {
local cargoTypes = {
     {name="Medical reagents (dual-use precursors)",         weight=3, risk=0.14, dual=true},
     {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="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="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="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="Satellite components / sensors (dual-use)", weight=1, risk=0.18, dual=true},
     {name="Aviation fuels & additives",                       weight=3, risk=0.12, dual=false},
    {name="Industrial fasteners (coded)", weight=2, risk=0.12, dual=true},
     {name="Refined petroleum products",                       weight=4, risk=0.08, dual=false},
    {name="Hydraulic lubricants (dual-use)", weight=2, risk=0.13, dual=true},
     {name="Relief supplies (mixed pallets)",                 weight=5, risk=0.06, dual=false},
    {name="High-frequency alloys (coded)", weight=1, risk=0.14, dual=true},
     {name="Agricultural commodities",                         weight=5, risk=0.04, dual=false},
     {name="Aviation fuels & additives", weight=3, risk=0.12, dual=false},
     {name="Finished consumer goods",                           weight=6, risk=0.03, 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}
}
}


-- Shipment tags; "emergency" / "expedited" are purposely given small extra risk to justify audits.
local cargoTags = {
local cargoTags = {
     {name="standard",  weight=6, risk=0.00},
     {name="standard",  weight=6, risk=0.00},
     {name="expedited", weight=3, risk=0.05},
     {name="expedited", weight=3, risk=0.05},
     {name="emergency", weight=2, risk=0.07}
     {name="emergency", weight=2, risk=0.07},
    {name="perishable",weight=2, risk=0.06}
}
}


-- Indirect logistics partners (numerically coded). Some profiles have higher latent risk.
-- (Internally: a few profiles correlate with carriers servicing IF/Matamoros-linked ports.)
local partnerProfiles = {
local partnerProfiles = {
     {code="P-07", weight=4, risk=0.00},
     {code="P-07", weight=4, risk=0.00, recentLift=false},
     {code="P-12", weight=3, risk=0.03},
     {code="P-12", weight=3, risk=0.03, recentLift=false},
     {code="P-17", weight=2, risk=0.09}, -- elevated scrutiny
     {code="P-17", weight=3, risk=0.12, recentLift=true},
     {code="P-22", weight=2, risk=0.11}, -- elevated scrutiny
     {code="P-22", weight=3, risk=0.14, recentLift=true},
     {code="P-31", weight=3, risk=0.05},
     {code="P-31", weight=3, risk=0.05, recentLift=false},
     {code="P-38", weight=2, risk=0.10}, -- elevated scrutiny
     {code="P-38", weight=2, risk=0.10, recentLift=false},
     {code="P-44", weight=4, risk=0.02},
     {code="P-44", weight=4, risk=0.02, recentLift=false},
     {code="P-53", weight=3, risk=0.04}
     {code="P-53", weight=3, risk=0.04, recentLift=false}
}
 
-- Corum districts (for WL district annotations)
local corumDistricts = {
    {name="Lyvian Zones",    kind="NZ"},
    {name="Lucinarian Zones", kind="NZ"},
    {name="Brentonian Zones", kind="NZ"},
    {name="Ossyani Zones",    kind="NZ"},
    {name="Sinclarian Zones", kind="GM"},
    {name="Nortonian Zones",  kind="GM"},
    {name="Wallisian Zones",  kind="GM"},
    {name="Alindan Zones",    kind="GM"},
    {name="Aijiac Zones",    kind="GM"},
}
local districtKindByName = {}
for _,d in ipairs(corumDistricts) do districtKindByName[d.name] = d.kind end
 
-- WL corridors (labels/windows)
local wlCorridors = {
    { code="WL-NZ-1", name="New Zimian Humanitarian Belt", win="0700–1800 (expanded)", kind="NZ",
      districts={"Lyvian Zones","Lucinarian Zones","Brentonian Zones","Ossyani Zones"} },
    { code="WL-GM-2", name="Bayen Relief Spur", win="0800–1900 (expanded)", kind="GM",
      districts={"Sinclarian Zones","Nortonian Zones","Wallisian Zones","Alindan Zones","Aijiac Zones"} },
    { code="WL-SEA-3", name="Corum Coastal Shuttle", win="0600–2000 (expanded)", kind="Mixed",
      districts={"Wallisian Zones","Nortonian Zones","Lyvian Zones"} }
}
}


local clauses = {
local observers = {
     {key="Pilotage & Traffic Scheme", cite="SC §A2–A4"},
     "Neutral temple auditor","Independent port compliance inspector",
     {key="Tonnage & Warship Limits",  cite="SC §B1–B4"},
     "Community elder (witness)","Straits Convention monitor (civilian)"
    {key="Submarine Surface/Declare", cite="SC §C2–C3"},
    {key="Environmental Buffer",      cite="SC §D1–D3"},
    {key="Sacred Site Stand-off",    cite="SC §E1–E2"},
    {key="Emergency/Leviathan",      cite="SC §L1–L4"},
    {key="Right-of-Visit (Suspicion)",cite="SC §R1–R3"}
}
}


-- Actions with emphasis on manifest/partner scrutiny & procedural audits
local wl03Signers = {
local actions = {
     ["Matamoros (IF)"] = true,
     {name="Documents & pilotage verification",              weight=5, basis="Pilotage & Traffic Scheme"},
     ["Imperial Federation"] = true,
    {name="Tonnage/armament declaration check",            weight=4, basis="Tonnage & Warship Limits"},
     ["Bassaridian-flag"] = true,
     {name="Cargo manifest reconciliation audit",            weight=4, basis="Right-of-Visit (Suspicion)"},
     ["Neutral Merchant"] = true,
     {name="Indirect-partner disclosure & vetting",          weight=3, basis="Right-of-Visit (Suspicion)"},
     ["Moorland"] = true,
    {name="Environmental reroute order",                    weight=3, basis="Environmental Buffer"},
     ["Independent Coastal State"] = true
     {name="Submarine declare signal issued",                weight=2, basis="Submarine Surface/Declare"},
     {name="Procedural calibration spot-check (randomized)", weight=3, basis="Pilotage & Traffic Scheme"},
     {name="SAR escort under Leviathan Protocol",            weight=2, basis="Emergency/Leviathan"}
}
}


-- Outcomes include neutral "random audit" holds to implement soft delays
-------------------------------------------------------------------------------
local outcomes = {
-- (B1.5) SECTOR RULES (allow/block flags and/or vessels per sector)
     "Compliant; cleared",
-------------------------------------------------------------------------------
    "Minor non-compliance; corrected; cleared",
local sectorRules = {
    "Rerouted; passage maintained",
     ["Northern Sea of Storms"] = {
     "Inspection completed; cargo intact",
        flags = { block = { ["Imperial Federation"]=true, ["Matamoros (IF)"]=true, ["Moorland"]=true } },
     "Administrative notice issued; monitoring",
        -- vessels = { allow = { ["Research craft"]=true } }, -- optional example
    "Random audit hold; clearance pending procedural update",
     },
    "Documentation harmonization hold (routine)",
     ["Western Sea of Storms"] = {
     "Escorted to anchorage for formal review",
        flags = { block = { ["Imperial Federation"]=true, ["Matamoros (IF)"]=true, ["Moorland"]=true } },
    "Denied entry pending documents",
    },
    "SAR/aid rendered; voyage resumed",
     ["Southern Sea of Storms"] = {
     "No reports"
        flags = { block = { ["Imperial Federation"]=true, ["Matamoros (IF)"]=true, ["Moorland"]=true } },
     },
}
}


-------------------------------------------------------------------------------
-------------------------------------------------------------------------------
-- (C) UTILITIES
-- (B2) PUBLIC HEALTH (<=10% rows)
-------------------------------------------------------------------------------
-------------------------------------------------------------------------------
local function pickWeighted(list)
local phDiseases = {
     local total=0
  {name="Thalassan Rot",        code="TR", controls="canal flush; rat-proofing; food-handler checks"},
    for _,item in ipairs(list) do total=total+(item.weight or 1) end
  {name="Festival Blush",      code="FB", controls="ring vaccination; crowd spacing; dorm rotation"},
     local roll,cumul=math.random(total),0
  {name="Pilgrim’s Flux",      code="PF", controls="boil notices; cistern audit; ORS distribution"},
     for _,item in ipairs(list) do
  {name="Stygian Dust",        code="SD", controls="respirators; mist carts; guide clinic checks"},
        cumul=cumul+(item.weight or 1)
  {name="Haifan Salt-Sickness", code="HS", controls="indoor rotations; antifungal clinics; dehumidify"},
         if roll<=cumul then return item end
  {name="Norsolyrian Drain",    code="ND", controls="lined ditches; praziquantel; snail abatement"},
  {name="Somniumpolis Lung",    code="SL", controls="dust suppression; respirator fit-checks"},
  {name="Haifan Crimson Fever", code="HCF",controls="larval control; dusk patrols; vector abatement"},
  {name="Strip Yellowing",      code="SY", controls="single-use ladles; vendor relicensing; recall"},
  {name="Eye of Styx",          code="ES", controls="eye-wash stations; visor compliance"},
  {name="Cato’s Curse",        code="CC", controls="PPE audits; solvent/oxidizer exposure controls"},
  {name="White Pestilence",     code="WP", controls="tiered quarantine; convoy assays; cordons"},
  {name="Breath of Nephele",    code="BN", controls="ventilation drills; mask catechism; spacing"},
  {name="Agnian Lockjaw",      code="AL", controls="tetanus boosters; injury clinics"},
}
local phIndex = {}
for _,d in ipairs(phDiseases) do phIndex[d.code] = d end
 
local function phScore(sector, flag, vessel, cargo, tag, partner)
    local score = 0
    local cname = cargo and cargo.name or ""
    local tname = tag and tag.name or "standard"
    if sector == "Environmental Buffer" or sector == "Sacred Buffer Approaches"
      or sector:find("Strait") or sector:find("Approach") then score = score + 1 end
     if cname == "Agricultural commodities" or tname == "perishable" then score = score + 2 end
    if cname == "Relief supplies (mixed pallets)" then score = score + 1 end
     if cname:find("oxidizers") or cname:find("lubricants") or cname:find("petroleum") or cname:find("fuels") then score = score + 2 end
    if vessel == "Ro-Ro ferry" and (tname == "expedited" or tname == "emergency") then score = score + 2 end
    if (partner and partner.risk or 0) >= 0.09 then score = score + 1 end
    if flag and flag.risky then score = score + 0.5 end
 
    local function pick(code)
         local d = phIndex[code]; if not d then return 0,nil,nil end
        return score, d.code, (d.code .. " ("..d.name.."): " .. d.controls)
     end
     end
     return list[#list]
     if score >= 5 then return pick("WP") end
    if (cname == "Agricultural commodities" or tname == "perishable") then return pick("SY") end
    if cname == "Relief supplies (mixed pallets)" then return pick("PF") end
    if sector == "Environmental Buffer" or sector:find("Strait") then return pick("TR") end
    if sector:find("Archipelago Zone") then return pick("HS") end  -- matches Inbound/Outbound
    if (cname:find("oxidizers") or cname:find("lubricants") or cname:find("petroleum") or cname:find("fuels")) then return pick("CC") end
    if vessel == "Ro-Ro ferry" and (tname == "expedited" or tname == "emergency") then return pick("FB") end
    if (partner and partner.risk or 0) >= 0.09 then return pick("BN") end
    return 0,nil,nil
end
end


local function pick(list) return list[math.random(#list)] end
-------------------------------------------------------------------------------
 
-- (C) POLICY STAGES (per flag)
local clauseIndex = (function()
-------------------------------------------------------------------------------
     local t={} for _,c in ipairs(clauses) do t[c.key]=c.cite end; return t
local stageMap = {
end)()
    ["Imperial Federation"] = 1,
    ["Matamoros (IF)"]     = 1,
}
local function getFlagStage(flagName, globalStage)
     local s = stageMap[flagName]
    if s ~= nil then return s end
    return globalStage or 0
end


-------------------------------------------------------------------------------
-------------------------------------------------------------------------------
-- (D) ENFORCEMENT PROBABILITY & FLAG SMOOTHING
-- (D) HELPERS
-------------------------------------------------------------------------------
-------------------------------------------------------------------------------
local function baseProb(flag, sector, cargo, tag, partner)
local function pick(list) return list[math.random(#list)] end
     local p = 0.28
local function pickWeighted(list)
     if flag.risky then p = p + 0.06 end
     local total = 0
     if sector=="Sacred Buffer Approaches" or sector=="Environmental Buffer" then p = p + 0.08 end
     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


    -- High-risk cargo amplification (dual-use + category risk + tag risk)
-- Filter helpers (respect sectorRules; safe fallback if empty)
    p = p + (cargo and (cargo.risk or 0) or 0)
local function isAllowedByRule(ruleTbl, key)
     if cargo and cargo.dual then p = p + 0.03 end
     if not ruleTbl then return true end
     p = p + (tag and (tag.risk or 0) or 0)
    if ruleTbl.allow then return ruleTbl.allow[key] == true end
 
    if ruleTbl.block then return ruleTbl.block[key] ~= true end
     -- Indirect partner latent risk
     return true
     p = p + (partner and (partner.risk or 0) or 0)
end
 
local function flagAllowedInSector(sector, flagName)
    -- Ceiling
     local rules = sectorRules[sector]
     if p > 0.72 then p = 0.72 end
     return isAllowedByRule(rules and rules.flags, flagName)
     return p
end
local function vesselAllowedInSector(sector, vesselName)
     local rules = sectorRules[sector]
     return isAllowedByRule(rules and rules.vessels, vesselName)
end
end
 
local function pickWeightedFiltered(list, predicate)
-- To avoid over-mentioning, impose per-flag cap and discourage repeats.
     local filtered = {}
local function pickFlagWithCaps(flagCounts, maxPerFlag, lastFlagName)
     for _, item in ipairs(list) do
     local tries = 0
         if predicate(item) then table.insert(filtered, item) end
     while tries < 20 do
        local f = pickWeighted(flags)
         if (flagCounts[f.name] or 0) < maxPerFlag then
            if lastFlagName and f.name == lastFlagName and math.random() < 0.5 then
                tries = tries + 1
            else
                return f
            end
        else
            tries = tries + 1
        end
     end
     end
     for _,f in ipairs(flags) do
    if #filtered == 0 then return pickWeighted(list) end
         if (flagCounts[f.name] or 0) < maxPerFlag then return f end
    return pickWeighted(filtered)
end
local function pickFiltered(list, predicate)
    local filtered = {}
     for _, item in ipairs(list) do
         if predicate(item) then table.insert(filtered, item) end
     end
     end
     return flags[#flags]
     if #filtered == 0 then return pick(list) end
    return pick(filtered)
end
end


-------------------------------------------------------------------------------
-- FULL WL expansion switch
-- (E) EVENT GENERATION
local fullWL = true
-------------------------------------------------------------------------------
local function makeEvent(dateText, state)
    local sector  = pick(sectors)
    local flag    = pickFlagWithCaps(state.flagCounts, state.maxPerFlag, state.lastFlag)
    local vessel  = pick(vessels)
    local cargo  = pickWeighted(cargoTypes)
    local tag    = pickWeighted(cargoTags)
    local partner = pickWeighted(partnerProfiles)


    state.flagCounts[flag.name] = (state.flagCounts[flag.name] or 0) + 1
-- WL eligibility (expanded)
     state.lastFlag = flag.name
local function wlEligibility(cargo, tag, flag, partner)
     if not cargo or not tag then return false, "n/a" end


     local pEnf = baseProb(flag, sector, cargo, tag, partner)
     local disqualify = false
    if cargo.dual then disqualify = true end
    if flag and flag.name == "Matamoros (IF)" and not wl03Signers["Matamoros (IF)"] then disqualify = true end


     -- Most transits are routine; if not enforced, record compliant passage
     if disqualify then
    if math.random() > pEnf then
        local signed = wl03Signers[flag.name]
         return {
         if signed == nil then signed = true end
            date=dateText, sector=sector,
        local cooling = (partner and partner.recentLift) or false
            vessel=vessel.." ("..flag.name..")",
        if not signed then return false, "WL-03 not signed (M-AMBER)" end
            action="Routine transit", legal="N/A",
        if cooling then return false, "WL-03 cooldown (90 days)" end
            outcome="No reports",
        return false, "Not eligible cargo/tag"
            notes=string.format("Cargo: %s [%s]; Partner: %s — Compliant passage",
                                cargo.name, tag.name, partner.code)
        }
     end
     end


     -- Choose action & legal
     if fullWL then
     local act  = pickWeighted(actions)
        return true, "WL-03 compliant"
    local cite = clauseIndex[act.basis] or "SC §—"
     else
        local green = (cargo.name == "Relief supplies (mixed pallets)" or cargo.name == "Agricultural commodities")
        local amber = (tag.name == "expedited" or tag.name == "emergency" or tag.name == "perishable")
        local baseEligible = ((green or amber) and not cargo.dual)
        local signed = wl03Signers[flag.name]; if signed == nil then signed = true end
        local cooling = (partner and partner.recentLift) or false
        if not baseEligible then return false, "Not eligible cargo/tag" end
        if not signed then return false, "WL-03 not signed (M-AMBER)" end
        if cooling then return false, "WL-03 cooldown (90 days)" end
        return true, "WL-03 compliant"
    end
end


    -- Outcome selection with subtle bias: IF/Matamoros + high-risk cargo/tags/partner → procedural holds more likely
local function legalBasis(sector, wl, wl03note, phApplied)
    local out = pick(outcomes)
     if phApplied then
     local riskPush = 0
        return "Art VII (Public Health Safeguards); Annex D; HCE PH-Bulletin"
    if (flag.name=="Imperial Federation" or flag.name=="Matamoros (IF)") then
        riskPush = riskPush + 1
     end
     end
     if (cargo.dual or (cargo.risk >= 0.12)) then riskPush = riskPush + 1 end
     if wl then
    if (tag.name ~= "standard") then riskPush = riskPush + 1 end
        return "Art III.5 (White-Lane); Annex G; Art X.3 (expedited remedies); HCE Circular WL-03 (" .. wl03note .. ")"
    if (partner.risk >= 0.09) then riskPush = riskPush + 1 end
 
    if riskPush >= 2 and math.random() < 0.65 then
        -- steer to gentle, non-escalatory delay outcomes
        local delayOutcomes = {
            "Random audit hold; clearance pending procedural update",
            "Documentation harmonization hold (routine)",
            "Administrative notice issued; monitoring"
        }
        out = delayOutcomes[math.random(#delayOutcomes)]
     end
     end
 
     if sector == "Sacred Buffer Approaches" or sector == "Environmental Buffer" then
    -- Guardrail: cap strong outcomes (denied/anchorage review) total per day
         return "Art III.2.3 (Sacred quiet zones); Annex A (delims); Art VII; Annex D"
     if (out=="Denied entry pending documents" or out=="Escorted to anchorage for formal review")
      and state.strongCount >= state.strongCap then
         out = "Administrative notice issued; monitoring"
    elseif out=="Denied entry pending documents" or out=="Escorted to anchorage for formal review" then
        state.strongCount = state.strongCount + 1
     end
     end
    return "Art IV.1–1.2 (PSC); Annex F (SCT/SAC procedures)"
end


    -- Sub-rules & notes
local function appealsStatus(sector, wl, tag, cargo)
    local notes = string.format("Cargo: %s [%s]; Partner: %s — ", cargo.name, tag.name, partner.code)
     if wl then
     if vessel=="Undeclared submarine" then
         local relief = 0.70
         act  = {name="Submarine declare signal issued", basis="Submarine Surface/Declare"}
        if tag and (tag.name=="emergency" or tag.name=="perishable") then relief = relief + 0.10 end
         cite = clauseIndex[act.basis]
         if cargo and cargo.dual then relief = relief - 0.20 end
         notes=notes.."Undeclared contact directed to surface and declare per procedure"
         local r = math.random()
         if out=="No reports" then out="Administrative notice issued; monitoring" end
         local outcome = (r <= relief * 0.6) and "clearance granted"
    elseif vessel=="Declared submarine" then
                      or (r <= relief)      and "expedited with conditions"
         notes=notes.."Declared sub transited in daylight per SC §C2–C3"
                      or "conditions imposed"
     elseif sector=="Sacred Buffer Approaches" then
         return string.format("HCE Review → SCT (72h) → SAC (≤5d) — %s (Art X.3; Annex G; WL-03 liaison)", outcome)
         notes=notes.."Stand-off distance verified for sacred sites"
     else
    elseif sector=="Environmental Buffer" then
        local level = (sector=="Sacred Buffer Approaches" or sector=="Environmental Buffer") and "SAC Referral"
         notes=notes.."Sensitive habitat protection enforced during passage"
                      or ((math.random()<0.5) and "SCT Admin Review" or "PSC reconciliation")
         local outcome = (level=="PSC reconciliation") and "notice acknowledged"
                        or ((math.random()<0.5) and "clearance granted" or "conditions imposed")
        local cite = (level=="SAC Referral") and "Annex F (SAC)" or "Annex F (SCT)"
         return string.format("%s — %s (%s)", level, outcome, cite)
     end
     end
end


    -- IF/Matamoros narrative softener for delays (keep language neutral)
local function hceRef(dateInfo, corridor)
     if (flag.name=="Imperial Federation" or flag.name=="Matamoros (IF)")
     return string.format("HCE-%03d.%03d-%s", dateInfo.year, dateInfo.dayOfYear, corridor.code)
      and (out=="Random audit hold; clearance pending procedural update"
end
            or out=="Documentation harmonization hold (routine)") then
        notes = notes.."; sampled under routine process calibration"
    end


     if out~="No reports" then state.enfCount = state.enfCount + 1 end
local function pickServedDistrict(corr)
     local dname = corr.districts[math.random(#corr.districts)]
    local kind  = districtKindByName[dname] or "NZ"
    return dname, kind
end


     return {
local function buildTriggers(flag, sector, cargo, tag, partner, wlEligible, wl03note, riskPush, flagStage)
        date=dateText, sector=sector,
     local t = {}
        vessel=vessel.." ("..flag.name..")",
    if flag.risky then table.insert(t, "risky flag") end
        action=act.name, legal=(act.basis.." — "..cite),
    if sector == "Sacred Buffer Approaches" or sector == "Environmental Buffer" then
        outcome=out, notes=notes
        table.insert(t, "sacred/ecology buffer")
     }
    end
    if cargo and cargo.dual then table.insert(t, "dual-use cargo") end
    if cargo and (cargo.risk or 0) >= 0.12 then table.insert(t, "cargo risk ≥0.12") end
    if tag and tag.name ~= "standard" then table.insert(t, "non-standard tag: "..tag.name) end
    if partner and (partner.risk or 0) >= 0.09 then table.insert(t, "partner risk ≥0.09") end
    if wlEligible then table.insert(t, "WL pre-cleared") end
    if wl03note == "WL-03 not signed (M-AMBER)" then table.insert(t, "WL-03 unsigned (M-AMBER)") end
    if wl03note == "WL-03 cooldown (90 days)" then table.insert(t, "WL-03 cooldown") end
    if flagStage and flagStage > 0 then table.insert(t, "policy stage "..flagStage) end
    if riskPush and riskPush >= 2 then table.insert(t, "multi-trigger escalation") end
    if #t == 0 then return "none" end
     return table.concat(t, "; ")
end
end


-------------------------------------------------------------------------------
-------------------------------------------------------------------------------
-- (F) PUBLIC: makeTable
-- (E) MAIN GENERATION (PH <=10%; no Notes column)
-------------------------------------------------------------------------------
-------------------------------------------------------------------------------
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 36
    local globalStage = tonumber(frame and frame.args and frame.args.policy_stage) or 0
    local showLegend  = (frame and frame.args and tostring(frame.args.showlegend) or "yes"):lower() ~= "no"
 
    -- PH toggles (cap fixed at 10%)
    local phOn      = (frame and frame.args and tostring(frame.args.ph) or "on"):lower() ~= "off"
    local phRateArg  = tonumber(frame and frame.args and frame.args.phrate) or 0.10
    if phRateArg < 0 then phRateArg = 0 end
    if phRateArg > 0.10 then phRateArg = 0.10 end
    local showPHLeg  = (frame and frame.args and tostring(frame.args.showphlegend) or "yes"):lower() ~= "no"
 
    local phCap = math.floor(rows * 0.10)
 
    -- Pass 1: compose rows and score PH
    local rowObjs = {}
    for i = 1, rows do
        math.randomseed(dailySeed + i*97)
 
        local sector  = pick(sectors)
 
        -- NEW: sector-aware picks
        local flag    = pickWeightedFiltered(flags, function(f)
            return flagAllowedInSector(sector, f.name)
        end)
        local vessel  = pickFiltered(vessels, function(v)
            return vesselAllowedInSector(sector, v)
        end)
 
        local cargo  = pickWeighted(cargoTypes)
        local tag    = pickWeighted(cargoTags)
        local partner = pickWeighted(partnerProfiles)
 
        local wlOK, wl03note = wlEligibility(cargo, tag, flag, partner)
        local wlEligible = wlOK
        local corridor = wlEligible and wlCorridors[math.random(#wlCorridors)] or nil
        local obs = wlEligible and observers[math.random(#observers)] or nil
        local flagStage = getFlagStage(flag.name, globalStage)
 
        local function baseProb()
            local q = 0.20
            if flag.risky then q = q + 0.05 end
            if sector == "Sacred Buffer Approaches" or sector == "Environmental Buffer" then q = q + 0.08 end
            q = q + (cargo and cargo.risk or 0)
            if cargo and cargo.dual then q = q + 0.12 end
            q = q + (tag and tag.risk or 0)
            q = q + (partner and partner.risk or 0)
            if wlEligible then q = math.max(q - 0.08, 0.08) end
 
            local isIF = flag.name == "Imperial Federation"
            local isMT = flag.name:match("^Matamoros")
            if (isIF or isMT) and not wlEligible then
                if flagStage >= 1 then q = q + 0.04 end
                if flagStage >= 2 then q = q + 0.04 end
            end
            if flag.name:match("Matamoros") and wl03note == "WL-03 not signed (M-AMBER)" then
                q = q + 0.08
            end
            if flag.name == "Bassaridian-flag" then
                q = q - 0.18
                if wlEligible then q = q - 0.04 end
                if q < 0.02 then q = 0.02 end
                if q > 0.08 then q = 0.08 end
            end
            return math.min(q, 0.72)
        end
 
        local riskPush = 0
        if flag.name:match("Imperial Federation") or flag.name:match("^Matamoros") then riskPush = riskPush + 1 end
        if cargo.dual or (cargo.risk or 0) >= 0.12 then riskPush = riskPush + 1 end
        if tag.name ~= "standard" then riskPush = riskPush + 1 end
        if (partner.risk or 0) >= 0.09 then riskPush = riskPush + 1 end
        if wlEligible then riskPush = math.max(riskPush - 1, 0) end
 
        local score, phCode, phText = phScore(sector, flag, vessel, cargo, tag, partner)
        score = score + phRateArg * 0.5
 
        rowObjs[i] = {
            i=i, sector=sector, flag=flag, vessel=vessel, cargo=cargo, tag=tag, partner=partner,
            wlEligible = wlEligible, wl03note = wl03note, corridor = corridor, obs = obs,
            flagStage = flagStage, enforceProb = baseProb(), riskPush = riskPush,
            phScore = score, phCode = phCode, phText = phText
        }
    end
 
    -- Select top K PH rows
    if phOn and phCap > 0 then
        table.sort(rowObjs, function(a,b) return a.phScore > b.phScore end)
        local applied = 0
        for _,r in ipairs(rowObjs) do
            if r.phCode and applied < phCap and r.phScore > 0 then
                r.phApplied = true
                applied = applied + 1
            else
                r.phApplied = false
            end
        end
        table.sort(rowObjs, function(a,b) return a.i < b.i end)
    else
        for _,r in ipairs(rowObjs) do r.phApplied = false end
    end


     local ROWS = tonumber(frame and frame.args and frame.args.rows) or 18
     -- Pass 2: render (no Notes column)
     local out = {}
     local out = {}
     table.insert(out, '{| class="wikitable sortable"')
     table.insert(out, "{| class='wikitable sortable'")
     table.insert(out, '! Date !! Sector !! Vessel/Flag !! Action !! Legal Basis !! Outcome !! Notes')
    if phOn then
        table.insert(out, "! Date !! Sector !! Vessel/Flag !! Action !! Legal Basis !! Outcome !! Appeals/Review (Straits Conventions) !! Case ID !! PH Case !! Public Health Advisory !! Triggers")
     else
        table.insert(out, "! Date !! Sector !! Vessel/Flag !! Action !! Legal Basis !! Outcome !! Appeals/Review (Straits Conventions) !! Case ID !! Triggers")
    end


     -- Per-flag cap: ~35% of rows per registry max (rounded up)
     for _,r in ipairs(rowObjs) do
    local maxPerFlag = math.max(1, math.ceil(ROWS * 0.35))
        math.randomseed(dailySeed + r.i*131)


    local state = {
        local action, legal, outcome, appealCol
        strongCount = 0,           -- denied/anchorage combined
         local dname, dkind
        strongCap  = 3,          -- daily cap
        flagCounts  = {},
        lastFlag    = nil,
         enfCount    = 0,
        maxPerFlag  = maxPerFlag
    }


    local events = {}
        if r.phApplied then
    for i=1, ROWS do
            action  = "Public health interdiction ("..r.phCode..")"
         local ev = makeEvent(dateInfo.text, state)
            legal  = legalBasis(r.sector, false, r.wl03note, true)
        table.insert(events, ev)
            outcome = "Public health hold; extended review"
    end
            appealCol = "SAC Referral — health safeguards (Annex D)"
         else
            if r.wlEligible then
                dname, dkind = pickServedDistrict(r.corridor)
                action = "WL transit / pilotage verification"
                legal  = legalBasis(r.sector, true, r.wl03note, false)
            else
                action = (r.wl03note == "WL-03 not signed (M-AMBER)" or r.wl03note == "WL-03 cooldown (90 days)")
                        and "WL-ineligible: standard PSC applied"
                        or  "Cargo manifest reconciliation audit"
                legal  = legalBasis(r.sector, false, r.wl03note, false)
            end


    -- Balance guardrail: keep total enforcement ≤ 40% of rows
            local enforce = r.enforceProb
    local maxEnf = math.floor(ROWS * 0.40)
            if math.random() > enforce then
    if state.enfCount > maxEnf then
                outcome  = r.wlEligible and "Cleared WL window" or "No reports"
        local toReduce = state.enfCount - maxEnf
                appealCol = r.wlEligible and "N/A (WL cleared)" or "N/A"
        for _,ev in ipairs(events) do
             else
             if toReduce <= 0 then break end
                outcome = r.wlEligible and "WL reconciliation check; monitoring" or "Administrative notice issued; monitoring"
            if ev.outcome ~= "No reports" and ev.outcome ~= "Compliant; cleared" then
                if r.riskPush >= 2 and math.random() < 0.65 then
                 ev.outcome = "Compliant; cleared"
                    outcome = (r.wlEligible and "WL hold; clearance pending expedited review" or "Random audit hold; clearance pending")
                 if ev.notes=="—" then
                    if r.flag.name == "Imperial Federation" or r.flag.name:match("^Matamoros") then
                     ev.notes = "Auto-balanced for daily transparency"
                        outcome = outcome .. " [Extended regulatory review]"
                else
                    end
                     ev.notes = ev.notes.."; auto-balanced"
                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
                 end
                 toReduce = toReduce - 1
                 appealCol = appealCol or appealsStatus(r.sector, r.wlEligible, r.tag, r.cargo)
             end
             end
        end
        local caseId  = string.format("CASE-%03d.%03d-%02d", dateInfo.year, dateInfo.dayOfYear, r.i)
        local phCaseId = "—"
        local phText  = "No concern"
        if phOn and r.phApplied then
            phCaseId = string.format("PH-CASE-%03d.%03d-%02d-%s", dateInfo.year, dateInfo.dayOfYear, r.i, r.phCode or "PH")
            phText  = r.phText or "Public health concern"
        end
        local triggers = buildTriggers(r.flag, r.sector, r.cargo, r.tag, r.partner, r.wlEligible, r.wl03note, r.riskPush, r.flagStage)
        if outcome:lower():match("hold") or outcome:lower():match("conditions") or outcome:lower():match("monitoring") or outcome:lower():match("denial") then
            appealCol = (appealCol ~= "N/A" and (appealCol .. " | ")) or ""
            appealCol = appealCol .. ("Appeals Case: APP-" .. caseId)
        end
        local style
        local o = outcome:lower()
        if r.phApplied or o:match("hold") or o:match("denial") or o:match("detention") or o:match("impound") or o:match("extended regulatory review") then
            style = ' style="background-color:#f8d7da;"'
        elseif o:match("administrative notice") or o:match("monitoring") or o:match("conditions") or o:match("reconciliation") then
            style = ' style="background-color:#fff3cd;"'
        else
            style = ' style="background-color:#d4edda;"'
        end
        table.insert(out, "|-" .. style)
        if phOn then
            table.insert(out, string.format("| %s || %s || %s (%s) || %s || %s || %s || %s || %s || %s || %s || %s",
                dateInfo.text, r.sector, r.vessel, r.flag.name, action, legal, outcome, appealCol, caseId, phCaseId, phText, triggers))
        else
            table.insert(out, string.format("| %s || %s || %s (%s) || %s || %s || %s || %s || %s || %s",
                dateInfo.text, r.sector, r.vessel, r.flag.name, action, legal, outcome, appealCol, caseId, triggers))
         end
         end
     end
     end


     for _,ev in ipairs(events) do
     table.insert(out, "|}")
         table.insert(out, "|-")
    if showLegend then
         table.insert(out, "| "..ev.date.." || "..ev.sector.." || "..ev.vessel.." || "..ev.action.." || "..ev.legal.." || "..ev.outcome.." || "..ev.notes)
         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 phOn then
        table.insert(out, "\n<div style='margin-top:0.3em; font-size:90%;'><b>Public Health Advisory Codes:</b> ")
        local parts = {}
        for _,d in ipairs(phDiseases) do
            table.insert(parts, string.format("<span style='white-space:nowrap;'><b>%s</b> – %s</span>", d.code, d.name))
        end
        table.insert(out, table.concat(parts, " &nbsp; "))
        table.insert(out, "</div>")
     end
     end


    table.insert(out, '|}')
    table.insert(out, '<!-- StraitsDaily v4.1 (high-risk cargo focus; coded partner scrutiny; neutral procedural holds) -->')
     return table.concat(out, "\n")
     return table.concat(out, "\n")
end
end


return p
return p

Latest revision as of 14:03, 13 October 2025

Documentation for this module may be created at Module:StraitsDaily/doc

-- Module: StraitsDaily
-- Purpose: Daily, in-world report of Strait of Haifa enforcement [[Straits Conventions of 52.06 PSSC]]

local p = {}

-------------------------------------------------------------------------------
-- (A) PSSC DATE
-------------------------------------------------------------------------------
local function getPSSCDate()
    local startDate = os.time({year = 1999, month = 8, day = 6})
    local secondsInDay  = 86400
    local daysPerYear   = 183
    local now           = os.time()
    local totalDays     = math.floor((now - startDate) / secondsInDay)

    local yearFraction  = totalDays / daysPerYear
    local psscYear      = math.floor(yearFraction)
    local dayOfYear     = math.floor((yearFraction - psscYear) * daysPerYear) + 1

    local month, dayOfMonth
    if dayOfYear <= 61 then
        month = 1; dayOfMonth = dayOfYear
    elseif dayOfYear <= 122 then
        month = 2; dayOfMonth = dayOfYear - 61
    else
        month = 3; dayOfMonth = dayOfYear - 122
    end

    local dateString = string.format("%d/%d/%d PSSC", dayOfMonth, month, psscYear)
    return { text = dateString, year = psscYear, dayOfYear = dayOfYear }
end

-------------------------------------------------------------------------------
-- (B) STATIC DATA
-------------------------------------------------------------------------------
local sectors = {
    "Strait South Inbound","Strait South Outbound","Strait North Inbound","Strait North Outbound",
    "Archipelago Zone Inbound", "Archipelago Zone Outbound", "Sacred Buffer Approaches",
    "Environmental Buffer","Northern Approach TSS","Southern Approach TSS",
    "Northern Sea of Storms","Western Sea of Storms","Southern Sea of Storms",
}

local flags = {
    {name="Bassaridian-flag", weight=6, risky=false},
    {name="Moorland", weight=2, risky=false},
    {name="Imperial Federation", weight=5, risky=false},
    {name="Matamoros (IF)", weight=2, risky=false},
    {name="Neutral Merchant", weight=4, risky=false},
    {name="Diaspora Stakeholders- Euranidom", weight=3, risky=true},
    {name="Diaspora Stakeholders- Corumia", weight=3, risky=true},
    {name="Diaspora Stakeholders- South Keltiania", weight=3, risky=true},
    {name="Consortium Registry", weight=2, risky=true},
    {name="Independent Coastal State", weight=7, risky=false}

}

local vessels = {
    "Handymax bulker","Product tanker","General cargo ship","Container feeder",
    "Ro-Ro ferry","Research craft","Declared submarine","Undeclared submarine"
}

local cargoTypes = {
    {name="Medical reagents (dual-use precursors)", weight=3, risk=0.14, dual=true},
    {name="Industrial oxidizers / nitrates (dual-use)", weight=2, risk=0.16, dual=true},
    {name="Precision machine tools & CNC spares (dual-use)", weight=3, risk=0.15, dual=true},
    {name="Encrypted electronics & comms modules (dual-use)", weight=2, risk=0.17, dual=true},
    {name="Satellite components / sensors (dual-use)", weight=1, risk=0.18, dual=true},
    {name="Industrial fasteners (coded)", weight=2, risk=0.12, dual=true},
    {name="Hydraulic lubricants (dual-use)", weight=2, risk=0.13, dual=true},
    {name="High-frequency alloys (coded)", weight=1, risk=0.14, dual=true},
    {name="Aviation fuels & additives", weight=3, risk=0.12, dual=false},
    {name="Refined petroleum products", weight=4, risk=0.08, dual=false},
    {name="Relief supplies (mixed pallets)", weight=5, risk=0.06, dual=false},
    {name="Agricultural commodities", weight=5, risk=0.04, dual=false},
    {name="Finished consumer goods", weight=6, risk=0.03, dual=false}
}

local cargoTags = {
    {name="standard",  weight=6, risk=0.00},
    {name="expedited", weight=3, risk=0.05},
    {name="emergency", weight=2, risk=0.07},
    {name="perishable",weight=2, risk=0.06}
}

local partnerProfiles = {
    {code="P-07", weight=4, risk=0.00, recentLift=false},
    {code="P-12", weight=3, risk=0.03, recentLift=false},
    {code="P-17", weight=3, risk=0.12, recentLift=true},
    {code="P-22", weight=3, risk=0.14, recentLift=true},
    {code="P-31", weight=3, risk=0.05, recentLift=false},
    {code="P-38", weight=2, risk=0.10, recentLift=false},
    {code="P-44", weight=4, risk=0.02, recentLift=false},
    {code="P-53", weight=3, risk=0.04, recentLift=false}
}

-- Corum districts (for WL district annotations)
local corumDistricts = {
    {name="Lyvian Zones",     kind="NZ"},
    {name="Lucinarian Zones", kind="NZ"},
    {name="Brentonian Zones", kind="NZ"},
    {name="Ossyani Zones",    kind="NZ"},
    {name="Sinclarian Zones", kind="GM"},
    {name="Nortonian Zones",  kind="GM"},
    {name="Wallisian Zones",  kind="GM"},
    {name="Alindan Zones",    kind="GM"},
    {name="Aijiac Zones",     kind="GM"},
}
local districtKindByName = {}
for _,d in ipairs(corumDistricts) do districtKindByName[d.name] = d.kind end

-- WL corridors (labels/windows)
local wlCorridors = {
    { code="WL-NZ-1", name="New Zimian Humanitarian Belt", win="0700–1800 (expanded)", kind="NZ",
      districts={"Lyvian Zones","Lucinarian Zones","Brentonian Zones","Ossyani Zones"} },
    { code="WL-GM-2", name="Bayen Relief Spur", win="0800–1900 (expanded)", kind="GM",
      districts={"Sinclarian Zones","Nortonian Zones","Wallisian Zones","Alindan Zones","Aijiac Zones"} },
    { code="WL-SEA-3", name="Corum Coastal Shuttle", win="0600–2000 (expanded)", kind="Mixed",
      districts={"Wallisian Zones","Nortonian Zones","Lyvian Zones"} }
}

local observers = {
    "Neutral temple auditor","Independent port compliance inspector",
    "Community elder (witness)","Straits Convention monitor (civilian)"
}

local wl03Signers = {
    ["Matamoros (IF)"] = true,
    ["Imperial Federation"] = true,
    ["Bassaridian-flag"] = true,
    ["Neutral Merchant"] = true,
    ["Moorland"] = true,
    ["Independent Coastal State"] = true
}

-------------------------------------------------------------------------------
-- (B1.5) SECTOR RULES (allow/block flags and/or vessels per sector)
-------------------------------------------------------------------------------
local sectorRules = {
    ["Northern Sea of Storms"] = {
        flags = { block = { ["Imperial Federation"]=true, ["Matamoros (IF)"]=true, ["Moorland"]=true } },
        -- vessels = { allow = { ["Research craft"]=true } }, -- optional example
    },
    ["Western Sea of Storms"] = {
        flags = { block = { ["Imperial Federation"]=true, ["Matamoros (IF)"]=true, ["Moorland"]=true } },
    },
    ["Southern Sea of Storms"] = {
        flags = { block = { ["Imperial Federation"]=true, ["Matamoros (IF)"]=true, ["Moorland"]=true } },
    },
}

-------------------------------------------------------------------------------
-- (B2) PUBLIC HEALTH (<=10% rows)
-------------------------------------------------------------------------------
local phDiseases = {
  {name="Thalassan Rot",        code="TR", controls="canal flush; rat-proofing; food-handler checks"},
  {name="Festival Blush",       code="FB", controls="ring vaccination; crowd spacing; dorm rotation"},
  {name="Pilgrim’s Flux",       code="PF", controls="boil notices; cistern audit; ORS distribution"},
  {name="Stygian Dust",         code="SD", controls="respirators; mist carts; guide clinic checks"},
  {name="Haifan Salt-Sickness", code="HS", controls="indoor rotations; antifungal clinics; dehumidify"},
  {name="Norsolyrian Drain",    code="ND", controls="lined ditches; praziquantel; snail abatement"},
  {name="Somniumpolis Lung",    code="SL", controls="dust suppression; respirator fit-checks"},
  {name="Haifan Crimson Fever", code="HCF",controls="larval control; dusk patrols; vector abatement"},
  {name="Strip Yellowing",      code="SY", controls="single-use ladles; vendor relicensing; recall"},
  {name="Eye of Styx",          code="ES", controls="eye-wash stations; visor compliance"},
  {name="Cato’s Curse",         code="CC", controls="PPE audits; solvent/oxidizer exposure controls"},
  {name="White Pestilence",     code="WP", controls="tiered quarantine; convoy assays; cordons"},
  {name="Breath of Nephele",    code="BN", controls="ventilation drills; mask catechism; spacing"},
  {name="Agnian Lockjaw",       code="AL", controls="tetanus boosters; injury clinics"},
}
local phIndex = {}
for _,d in ipairs(phDiseases) do phIndex[d.code] = d end

local function phScore(sector, flag, vessel, cargo, tag, partner)
    local score = 0
    local cname = cargo and cargo.name or ""
    local tname = tag and tag.name or "standard"
    if sector == "Environmental Buffer" or sector == "Sacred Buffer Approaches"
       or sector:find("Strait") or sector:find("Approach") then score = score + 1 end
    if cname == "Agricultural commodities" or tname == "perishable" then score = score + 2 end
    if cname == "Relief supplies (mixed pallets)" then score = score + 1 end
    if cname:find("oxidizers") or cname:find("lubricants") or cname:find("petroleum") or cname:find("fuels") then score = score + 2 end
    if vessel == "Ro-Ro ferry" and (tname == "expedited" or tname == "emergency") then score = score + 2 end
    if (partner and partner.risk or 0) >= 0.09 then score = score + 1 end
    if flag and flag.risky then score = score + 0.5 end

    local function pick(code)
        local d = phIndex[code]; if not d then return 0,nil,nil end
        return score, d.code, (d.code .. " ("..d.name.."): " .. d.controls)
    end
    if score >= 5 then return pick("WP") end
    if (cname == "Agricultural commodities" or tname == "perishable") then return pick("SY") end
    if cname == "Relief supplies (mixed pallets)" then return pick("PF") end
    if sector == "Environmental Buffer" or sector:find("Strait") then return pick("TR") end
    if sector:find("Archipelago Zone") then return pick("HS") end  -- matches Inbound/Outbound
    if (cname:find("oxidizers") or cname:find("lubricants") or cname:find("petroleum") or cname:find("fuels")) then return pick("CC") end
    if vessel == "Ro-Ro ferry" and (tname == "expedited" or tname == "emergency") then return pick("FB") end
    if (partner and partner.risk or 0) >= 0.09 then return pick("BN") end
    return 0,nil,nil
end

-------------------------------------------------------------------------------
-- (C) POLICY STAGES (per flag)
-------------------------------------------------------------------------------
local stageMap = {
    ["Imperial Federation"] = 1,
    ["Matamoros (IF)"]      = 1,
}
local function getFlagStage(flagName, globalStage)
    local s = stageMap[flagName]
    if s ~= nil then return s end
    return globalStage or 0
end

-------------------------------------------------------------------------------
-- (D) HELPERS
-------------------------------------------------------------------------------
local function pick(list) return list[math.random(#list)] end
local function pickWeighted(list)
    local total = 0
    for _, item in ipairs(list) do total = total + (item.weight or 1) end
    local roll, cumul = math.random(total), 0
    for _, item in ipairs(list) do
        cumul = cumul + (item.weight or 1)
        if roll <= cumul then return item end
    end
    return list[#list]
end

-- Filter helpers (respect sectorRules; safe fallback if empty)
local function isAllowedByRule(ruleTbl, key)
    if not ruleTbl then return true end
    if ruleTbl.allow then return ruleTbl.allow[key] == true end
    if ruleTbl.block then return ruleTbl.block[key] ~= true end
    return true
end
local function flagAllowedInSector(sector, flagName)
    local rules = sectorRules[sector]
    return isAllowedByRule(rules and rules.flags, flagName)
end
local function vesselAllowedInSector(sector, vesselName)
    local rules = sectorRules[sector]
    return isAllowedByRule(rules and rules.vessels, vesselName)
end
local function pickWeightedFiltered(list, predicate)
    local filtered = {}
    for _, item in ipairs(list) do
        if predicate(item) then table.insert(filtered, item) end
    end
    if #filtered == 0 then return pickWeighted(list) end
    return pickWeighted(filtered)
end
local function pickFiltered(list, predicate)
    local filtered = {}
    for _, item in ipairs(list) do
        if predicate(item) then table.insert(filtered, item) end
    end
    if #filtered == 0 then return pick(list) end
    return pick(filtered)
end

-- FULL WL expansion switch
local fullWL = true

-- WL eligibility (expanded)
local function wlEligibility(cargo, tag, flag, partner)
    if not cargo or not tag then return false, "n/a" end

    local disqualify = false
    if cargo.dual then disqualify = true end
    if flag and flag.name == "Matamoros (IF)" and not wl03Signers["Matamoros (IF)"] then disqualify = true end

    if disqualify then
        local signed = wl03Signers[flag.name]
        if signed == nil then signed = true end
        local cooling = (partner and partner.recentLift) or false
        if not signed then return false, "WL-03 not signed (M-AMBER)" end
        if cooling then return false, "WL-03 cooldown (90 days)" end
        return false, "Not eligible cargo/tag"
    end

    if fullWL then
        return true, "WL-03 compliant"
    else
        local green = (cargo.name == "Relief supplies (mixed pallets)" or cargo.name == "Agricultural commodities")
        local amber = (tag.name == "expedited" or tag.name == "emergency" or tag.name == "perishable")
        local baseEligible = ((green or amber) and not cargo.dual)
        local signed = wl03Signers[flag.name]; if signed == nil then signed = true end
        local cooling = (partner and partner.recentLift) or false
        if not baseEligible then return false, "Not eligible cargo/tag" end
        if not signed then return false, "WL-03 not signed (M-AMBER)" end
        if cooling then return false, "WL-03 cooldown (90 days)" end
        return true, "WL-03 compliant"
    end
end

local function legalBasis(sector, wl, wl03note, phApplied)
    if phApplied then
        return "Art VII (Public Health Safeguards); Annex D; HCE PH-Bulletin"
    end
    if wl then
        return "Art III.5 (White-Lane); Annex G; Art X.3 (expedited remedies); HCE Circular WL-03 (" .. wl03note .. ")"
    end
    if sector == "Sacred Buffer Approaches" or sector == "Environmental Buffer" then
        return "Art III.2.3 (Sacred quiet zones); Annex A (delims); Art VII; Annex D"
    end
    return "Art IV.1–1.2 (PSC); Annex F (SCT/SAC procedures)"
end

local function appealsStatus(sector, wl, tag, cargo)
    if wl then
        local relief = 0.70
        if tag and (tag.name=="emergency" or tag.name=="perishable") then relief = relief + 0.10 end
        if cargo and cargo.dual then relief = relief - 0.20 end
        local r = math.random()
        local outcome = (r <= relief * 0.6) and "clearance granted"
                      or (r <= relief)       and "expedited with conditions"
                      or "conditions imposed"
        return string.format("HCE Review → SCT (72h) → SAC (≤5d) — %s (Art X.3; Annex G; WL-03 liaison)", outcome)
    else
        local level = (sector=="Sacred Buffer Approaches" or sector=="Environmental Buffer") and "SAC Referral"
                      or ((math.random()<0.5) and "SCT Admin Review" or "PSC reconciliation")
        local outcome = (level=="PSC reconciliation") and "notice acknowledged"
                        or ((math.random()<0.5) and "clearance granted" or "conditions imposed")
        local cite = (level=="SAC Referral") and "Annex F (SAC)" or "Annex F (SCT)"
        return string.format("%s — %s (%s)", level, outcome, cite)
    end
end

local function hceRef(dateInfo, corridor)
    return string.format("HCE-%03d.%03d-%s", dateInfo.year, dateInfo.dayOfYear, corridor.code)
end

local function pickServedDistrict(corr)
    local dname = corr.districts[math.random(#corr.districts)]
    local kind  = districtKindByName[dname] or "NZ"
    return dname, kind
end

local function buildTriggers(flag, sector, cargo, tag, partner, wlEligible, wl03note, riskPush, flagStage)
    local t = {}
    if flag.risky then table.insert(t, "risky flag") end
    if sector == "Sacred Buffer Approaches" or sector == "Environmental Buffer" then
        table.insert(t, "sacred/ecology buffer")
    end
    if cargo and cargo.dual then table.insert(t, "dual-use cargo") end
    if cargo and (cargo.risk or 0) >= 0.12 then table.insert(t, "cargo risk ≥0.12") end
    if tag and tag.name ~= "standard" then table.insert(t, "non-standard tag: "..tag.name) end
    if partner and (partner.risk or 0) >= 0.09 then table.insert(t, "partner risk ≥0.09") end
    if wlEligible then table.insert(t, "WL pre-cleared") end
    if wl03note == "WL-03 not signed (M-AMBER)" then table.insert(t, "WL-03 unsigned (M-AMBER)") end
    if wl03note == "WL-03 cooldown (90 days)" then table.insert(t, "WL-03 cooldown") end
    if flagStage and flagStage > 0 then table.insert(t, "policy stage "..flagStage) end
    if riskPush and riskPush >= 2 then table.insert(t, "multi-trigger escalation") end
    if #t == 0 then return "none" end
    return table.concat(t, "; ")
end

-------------------------------------------------------------------------------
-- (E) MAIN GENERATION (PH <=10%; no Notes column)
-------------------------------------------------------------------------------
function p.makeTable(frame)
    local dateInfo = getPSSCDate()
    local dailySeed = dateInfo.year*1000 + dateInfo.dayOfYear
    math.randomseed(dailySeed)

    local rows        = tonumber(frame and frame.args and frame.args.rows) or 36
    local globalStage = tonumber(frame and frame.args and frame.args.policy_stage) or 0
    local showLegend  = (frame and frame.args and tostring(frame.args.showlegend) or "yes"):lower() ~= "no"

    -- PH toggles (cap fixed at 10%)
    local phOn       = (frame and frame.args and tostring(frame.args.ph) or "on"):lower() ~= "off"
    local phRateArg  = tonumber(frame and frame.args and frame.args.phrate) or 0.10
    if phRateArg < 0 then phRateArg = 0 end
    if phRateArg > 0.10 then phRateArg = 0.10 end
    local showPHLeg  = (frame and frame.args and tostring(frame.args.showphlegend) or "yes"):lower() ~= "no"

    local phCap = math.floor(rows * 0.10)

    -- Pass 1: compose rows and score PH
    local rowObjs = {}
    for i = 1, rows do
        math.randomseed(dailySeed + i*97)

        local sector  = pick(sectors)

        -- NEW: sector-aware picks
        local flag    = pickWeightedFiltered(flags, function(f)
            return flagAllowedInSector(sector, f.name)
        end)
        local vessel  = pickFiltered(vessels, function(v)
            return vesselAllowedInSector(sector, v)
        end)

        local cargo   = pickWeighted(cargoTypes)
        local tag     = pickWeighted(cargoTags)
        local partner = pickWeighted(partnerProfiles)

        local wlOK, wl03note = wlEligibility(cargo, tag, flag, partner)
        local wlEligible = wlOK
        local corridor = wlEligible and wlCorridors[math.random(#wlCorridors)] or nil
        local obs = wlEligible and observers[math.random(#observers)] or nil
        local flagStage = getFlagStage(flag.name, globalStage)

        local function baseProb()
            local q = 0.20
            if flag.risky then q = q + 0.05 end
            if sector == "Sacred Buffer Approaches" or sector == "Environmental Buffer" then q = q + 0.08 end
            q = q + (cargo and cargo.risk or 0)
            if cargo and cargo.dual then q = q + 0.12 end
            q = q + (tag and tag.risk or 0)
            q = q + (partner and partner.risk or 0)
            if wlEligible then q = math.max(q - 0.08, 0.08) end

            local isIF = flag.name == "Imperial Federation"
            local isMT = flag.name:match("^Matamoros")
            if (isIF or isMT) and not wlEligible then
                if flagStage >= 1 then q = q + 0.04 end
                if flagStage >= 2 then q = q + 0.04 end
            end
            if flag.name:match("Matamoros") and wl03note == "WL-03 not signed (M-AMBER)" then
                q = q + 0.08
            end
            if flag.name == "Bassaridian-flag" then
                q = q - 0.18
                if wlEligible then q = q - 0.04 end
                if q < 0.02 then q = 0.02 end
                if q > 0.08 then q = 0.08 end
            end
            return math.min(q, 0.72)
        end

        local riskPush = 0
        if flag.name:match("Imperial Federation") or flag.name:match("^Matamoros") then riskPush = riskPush + 1 end
        if cargo.dual or (cargo.risk or 0) >= 0.12 then riskPush = riskPush + 1 end
        if tag.name ~= "standard" then riskPush = riskPush + 1 end
        if (partner.risk or 0) >= 0.09 then riskPush = riskPush + 1 end
        if wlEligible then riskPush = math.max(riskPush - 1, 0) end

        local score, phCode, phText = phScore(sector, flag, vessel, cargo, tag, partner)
        score = score + phRateArg * 0.5

        rowObjs[i] = {
            i=i, sector=sector, flag=flag, vessel=vessel, cargo=cargo, tag=tag, partner=partner,
            wlEligible = wlEligible, wl03note = wl03note, corridor = corridor, obs = obs,
            flagStage = flagStage, enforceProb = baseProb(), riskPush = riskPush,
            phScore = score, phCode = phCode, phText = phText
        }
    end

    -- Select top K PH rows
    if phOn and phCap > 0 then
        table.sort(rowObjs, function(a,b) return a.phScore > b.phScore end)
        local applied = 0
        for _,r in ipairs(rowObjs) do
            if r.phCode and applied < phCap and r.phScore > 0 then
                r.phApplied = true
                applied = applied + 1
            else
                r.phApplied = false
            end
        end
        table.sort(rowObjs, function(a,b) return a.i < b.i end)
    else
        for _,r in ipairs(rowObjs) do r.phApplied = false end
    end

    -- Pass 2: render (no Notes column)
    local out = {}
    table.insert(out, "{| class='wikitable sortable'")
    if phOn then
        table.insert(out, "! Date !! Sector !! Vessel/Flag !! Action !! Legal Basis !! Outcome !! Appeals/Review (Straits Conventions) !! Case ID !! PH Case !! Public Health Advisory !! Triggers")
    else
        table.insert(out, "! Date !! Sector !! Vessel/Flag !! Action !! Legal Basis !! Outcome !! Appeals/Review (Straits Conventions) !! Case ID !! Triggers")
    end

    for _,r in ipairs(rowObjs) do
        math.randomseed(dailySeed + r.i*131)

        local action, legal, outcome, appealCol
        local dname, dkind

        if r.phApplied then
            action  = "Public health interdiction ("..r.phCode..")"
            legal   = legalBasis(r.sector, false, r.wl03note, true)
            outcome = "Public health hold; extended review"
            appealCol = "SAC Referral — health safeguards (Annex D)"
        else
            if r.wlEligible then
                dname, dkind = pickServedDistrict(r.corridor)
                action = "WL transit / pilotage verification"
                legal  = legalBasis(r.sector, true, r.wl03note, false)
            else
                action = (r.wl03note == "WL-03 not signed (M-AMBER)" or r.wl03note == "WL-03 cooldown (90 days)")
                         and "WL-ineligible: standard PSC applied"
                         or  "Cargo manifest reconciliation audit"
                legal  = legalBasis(r.sector, false, r.wl03note, false)
            end

            local enforce = r.enforceProb
            if math.random() > enforce then
                outcome  = r.wlEligible and "Cleared WL window" or "No reports"
                appealCol = r.wlEligible and "N/A (WL cleared)" or "N/A"
            else
                outcome = r.wlEligible and "WL reconciliation check; monitoring" or "Administrative notice issued; monitoring"
                if r.riskPush >= 2 and math.random() < 0.65 then
                    outcome = (r.wlEligible and "WL hold; clearance pending expedited review" or "Random audit hold; clearance pending")
                    if r.flag.name == "Imperial Federation" or r.flag.name:match("^Matamoros") then
                        outcome = outcome .. " [Extended regulatory review]"
                    end
                end
                local isIF = r.flag.name == "Imperial Federation"
                local isMT = r.flag.name:match("^Matamoros")
                if r.flagStage >= 3 and (isIF or isMT) and not r.wlEligible and r.riskPush >= 2 then
                    outcome  = "Denial of passage (policy Stage III)"
                    appealCol = "SCT injunction eligible | Appeals Case pending"
                end
                if r.flag.name == "Bassaridian-flag" and outcome:match("hold") then
                    outcome  = r.wlEligible and "WL reconciliation complete; cleared same window" or "Administrative reconciliation complete; cleared"
                    appealCol = r.wlEligible and "N/A (WL cleared)" or "N/A"
                end
                appealCol = appealCol or appealsStatus(r.sector, r.wlEligible, r.tag, r.cargo)
            end
        end

        local caseId   = string.format("CASE-%03d.%03d-%02d", dateInfo.year, dateInfo.dayOfYear, r.i)
        local phCaseId = "—"
        local phText   = "No concern"
        if phOn and r.phApplied then
            phCaseId = string.format("PH-CASE-%03d.%03d-%02d-%s", dateInfo.year, dateInfo.dayOfYear, r.i, r.phCode or "PH")
            phText   = r.phText or "Public health concern"
        end
        local triggers = buildTriggers(r.flag, r.sector, r.cargo, r.tag, r.partner, r.wlEligible, r.wl03note, r.riskPush, r.flagStage)

        if outcome:lower():match("hold") or outcome:lower():match("conditions") or outcome:lower():match("monitoring") or outcome:lower():match("denial") then
            appealCol = (appealCol ~= "N/A" and (appealCol .. " | ")) or ""
            appealCol = appealCol .. ("Appeals Case: APP-" .. caseId)
        end

        local style
        local o = outcome:lower()
        if r.phApplied or o:match("hold") or o:match("denial") or o:match("detention") or o:match("impound") or o:match("extended regulatory review") then
            style = ' style="background-color:#f8d7da;"'
        elseif o:match("administrative notice") or o:match("monitoring") or o:match("conditions") or o:match("reconciliation") then
            style = ' style="background-color:#fff3cd;"'
        else
            style = ' style="background-color:#d4edda;"'
        end

        table.insert(out, "|-" .. style)
        if phOn then
            table.insert(out, string.format("| %s || %s || %s (%s) || %s || %s || %s || %s || %s || %s || %s || %s",
                dateInfo.text, r.sector, r.vessel, r.flag.name, action, legal, outcome, appealCol, caseId, phCaseId, phText, triggers))
        else
            table.insert(out, string.format("| %s || %s || %s (%s) || %s || %s || %s || %s || %s || %s",
                dateInfo.text, r.sector, r.vessel, r.flag.name, action, legal, outcome, appealCol, caseId, triggers))
        end
    end

    table.insert(out, "|}")
    if showLegend then
        table.insert(out, "\n<div style='margin-top:0.5em; font-size:90%;'>")
        table.insert(out, "<b>Legend:</b> ")
        table.insert(out, "<span style='background:#d4edda;padding:2px 6px;border:1px solid #c3e6cb;'>Green</span> = Cleared/Compliant &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 phOn then
        table.insert(out, "\n<div style='margin-top:0.3em; font-size:90%;'><b>Public Health Advisory Codes:</b> ")
        local parts = {}
        for _,d in ipairs(phDiseases) do
            table.insert(parts, string.format("<span style='white-space:nowrap;'><b>%s</b> – %s</span>", d.code, d.name))
        end
        table.insert(out, table.concat(parts, " &nbsp; "))
        table.insert(out, "</div>")
    end

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

return p