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 2: Line 2:
-- Module: StraitsDaily
-- Module: StraitsDaily
-- Purpose: Daily, in-world report of Strait of Haifa enforcement
-- Purpose: Daily, in-world report of Strait of Haifa enforcement
-- Changes in this version:
--  • No mention of Florian cargo or the Imperial Federation.
--  • Explicit inclusion of Moorland and Bassaridian-flag vessels.
--  • Frequency smoothing + per-flag caps to avoid over-mentioning any registry.
--  • Non-escalatory language, outcome caps, and balance guardrails retained.
-------------------------------------------------------------------------------


local p = {}
local p = {}
Line 42: Line 36:
-------------------------------------------------------------------------------
-------------------------------------------------------------------------------
local sectors = {
local sectors = {
     "Strait South Inbound", "Strait South Outbound", "Strait North Inbound","Strait North Outbound", "Archipelago Zone",
     "Strait South Inbound","Strait South Outbound","Strait North Inbound","Strait North Outbound",
    "Sacred Buffer Approaches","Environmental Buffer","Northern Approach TSS","Southern Approach TSS"
    "Archipelago Zone","Sacred Buffer Approaches","Environmental Buffer","Northern Approach TSS","Southern Approach TSS"
}
}


-- Registries / flags:
-- Registries / flags (explicit IF/Matamoros allowed; bias is subtle and framed as process):
--  - Explicit: Moorland, Bassaridian-flag
--  - Generic/neutral: Neutral Merchant, Distant Partner (Euranidom), Distant Partner (Corumia), Distant Partner (Southern Keltiania), Consortium Registry, Other Coastal State
-- Per-flag 'risky' toggles are modest, to stay non-escalatory.
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},
     {name="Imperial Federation",                 weight=5, risky=true}, -- slight audit bias later
     {name="Matamoros (IF)",             weight=2, risky=true},
     {name="Matamoros (IF)",                       weight=2, risky=true}, -- slight audit bias later
     {name="Neutral Merchant",           weight=4, risky=false},
     {name="Neutral Merchant",                     weight=4, risky=false},
     {name="Distant Partner (Euranidom)",weight=3, risky=true},
     {name="Distant Partner (Euranidom)",         weight=3, risky=true},
     {name="Distant Partner (Corumia)", weight=3, risky=true},
     {name="Distant Partner (Corumia)",           weight=3, risky=true},
     {name="Distant Partner (Southern Keltiania)", weight=3, risky=true},
     {name="Distant Partner (Southern 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="Other Coastal State",                 weight=7, risky=false},
}
}


Line 66: Line 57:
     "Handymax bulker","Product tanker","General cargo ship","Container feeder",
     "Handymax bulker","Product tanker","General cargo ship","Container feeder",
     "Ro-Ro ferry","Research craft","Declared submarine","Undeclared submarine"
     "Ro-Ro ferry","Research craft","Declared submarine","Undeclared submarine"
}
-- High-risk cargo categories (dual-use emphasized). Weight ~appearance frequency; risk amplifies enforcement odds.
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="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}
}
-- Shipment tags; "emergency" / "expedited" are purposely given small extra risk to justify audits.
local cargoTags = {
    {name="standard",  weight=6, risk=0.00},
    {name="expedited", weight=3, risk=0.05},
    {name="emergency", weight=2, risk=0.07}
}
-- 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 = {
    {code="P-07", weight=4, risk=0.00},
    {code="P-12", weight=3, risk=0.03},
    {code="P-17", weight=2, risk=0.09}, -- elevated scrutiny
    {code="P-22", weight=2, risk=0.11}, -- elevated scrutiny
    {code="P-31", weight=3, risk=0.05},
    {code="P-38", weight=2, risk=0.10}, -- elevated scrutiny
    {code="P-44", weight=4, risk=0.02},
    {code="P-53", weight=3, risk=0.04}
}
}


Line 78: Line 103:
}
}


-- Actions with emphasis on manifest/partner scrutiny & procedural audits
local actions = {
local actions = {
     {name="Documents & pilotage verification",     weight=5, basis="Pilotage & Traffic Scheme"},
     {name="Documents & pilotage verification",             weight=5, basis="Pilotage & Traffic Scheme"},
     {name="Tonnage/armament declaration check",     weight=4, basis="Tonnage & Warship Limits"},
     {name="Tonnage/armament declaration check",             weight=4, basis="Tonnage & Warship Limits"},
     {name="Environmental reroute order",            weight=3, basis="Environmental Buffer"},
     {name="Cargo manifest reconciliation audit",            weight=4, basis="Right-of-Visit (Suspicion)"},
     {name="Sacred buffer stand-off adjustment",    weight=3, basis="Sacred Site Stand-off"},
     {name="Indirect-partner disclosure & vetting",         weight=3, basis="Right-of-Visit (Suspicion)"},
     {name="Submarine declare signal issued",       weight=2, basis="Submarine Surface/Declare"},
     {name="Environmental reroute order",                    weight=3, basis="Environmental Buffer"},
     {name="Right-of-visit boarding (lawful suspicion)", weight=2, basis="Right-of-Visit (Suspicion)"},
     {name="Submarine declare signal issued",               weight=2, basis="Submarine Surface/Declare"},
     {name="SAR escort under Leviathan Protocol",   weight=2, basis="Emergency/Leviathan"}
     {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 = {
local outcomes = {
     "Compliant; cleared",
     "Compliant; cleared",
Line 94: Line 122:
     "Inspection completed; cargo intact",
     "Inspection completed; cargo intact",
     "Administrative notice issued; monitoring",
     "Administrative notice issued; monitoring",
    "Random audit hold; clearance pending procedural update",
    "Documentation harmonization hold (routine)",
     "Escorted to anchorage for formal review",
     "Escorted to anchorage for formal review",
     "Denied entry pending documents",
     "Denied entry pending documents",
Line 113: Line 143:
     return list[#list]
     return list[#list]
end
end
local function pick(list) return list[math.random(#list)] end
local function pick(list) return list[math.random(#list)] end
local clauseIndex = (function()
local clauseIndex = (function()
     local t={} for _,c in ipairs(clauses) do t[c.key]=c.cite end; return t
     local t={} for _,c in ipairs(clauses) do t[c.key]=c.cite end; return t
Line 121: Line 153:
-- (D) ENFORCEMENT PROBABILITY & FLAG SMOOTHING
-- (D) ENFORCEMENT PROBABILITY & FLAG SMOOTHING
-------------------------------------------------------------------------------
-------------------------------------------------------------------------------
local function baseProb(flag, sector)
local function baseProb(flag, sector, cargo, tag, partner)
     local p = 0.30
     local p = 0.28
     if flag.risky then p = p + 0.10 end         -- mild, non-accusatory bias
     if flag.risky then p = p + 0.06 end
     if sector=="Sacred Buffer Approaches" or sector=="Environmental Buffer" then
     if sector=="Sacred Buffer Approaches" or sector=="Environmental Buffer" then p = p + 0.08 end
        p = p + 0.10
 
     end
    -- High-risk cargo amplification (dual-use + category risk + tag risk)
     if p>0.60 then p=0.60 end
    p = p + (cargo and (cargo.risk or 0) or 0)
    if cargo and cargo.dual then p = p + 0.03 end
    p = p + (tag and (tag.risk or 0) or 0)
 
    -- Indirect partner latent risk
     p = p + (partner and (partner.risk or 0) or 0)
 
    -- Ceiling
     if p > 0.72 then p = 0.72 end
     return p
     return p
end
end
Line 137: Line 177:
         local f = pickWeighted(flags)
         local f = pickWeighted(flags)
         if (flagCounts[f.name] or 0) < maxPerFlag then
         if (flagCounts[f.name] or 0) < maxPerFlag then
            -- discourage immediate repeats 50% of the time
             if lastFlagName and f.name == lastFlagName and math.random() < 0.5 then
             if lastFlagName and f.name == lastFlagName and math.random() < 0.5 then
                 tries = tries + 1
                 tries = tries + 1
Line 147: Line 186:
         end
         end
     end
     end
    -- fallback to any with room
     for _,f in ipairs(flags) do
     for _,f in ipairs(flags) do
         if (flagCounts[f.name] or 0) < maxPerFlag then return f end
         if (flagCounts[f.name] or 0) < maxPerFlag then return f end
Line 158: Line 196:
-------------------------------------------------------------------------------
-------------------------------------------------------------------------------
local function makeEvent(dateText, state)
local function makeEvent(dateText, state)
     local sector = pick(sectors)
     local sector = pick(sectors)
     local flag   = pickFlagWithCaps(state.flagCounts, state.maxPerFlag, state.lastFlag)
     local flag   = pickFlagWithCaps(state.flagCounts, state.maxPerFlag, state.lastFlag)
     local vessel = pick(vessels)
     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
     state.flagCounts[flag.name] = (state.flagCounts[flag.name] or 0) + 1
     state.lastFlag = flag.name
     state.lastFlag = flag.name


     local pEnf = baseProb(flag, sector)
     local pEnf = baseProb(flag, sector, cargo, tag, partner)
 
    -- Most transits are routine; if not enforced, record compliant passage
     if math.random() > pEnf then
     if math.random() > pEnf then
         return {
         return {
Line 170: Line 214:
             vessel=vessel.." ("..flag.name..")",
             vessel=vessel.." ("..flag.name..")",
             action="Routine transit", legal="N/A",
             action="Routine transit", legal="N/A",
             outcome="No reports", notes="Compliant passage"
             outcome="No reports",
            notes=string.format("Cargo: %s [%s]; Partner: %s — Compliant passage",
                                cargo.name, tag.name, partner.code)
         }
         }
     end
     end


     local act   = pickWeighted(actions)
    -- Choose action & legal
     local cite = clauseIndex[act.basis] or "SC §—"
     local act = pickWeighted(actions)
     local out   = pick(outcomes)
     local cite = clauseIndex[act.basis] or "SC §—"
 
    -- Outcome selection with subtle bias: IF/Matamoros + high-risk cargo/tags/partner → procedural holds more likely
     local out = pick(outcomes)
    local riskPush = 0
    if (flag.name=="Imperial Federation" or flag.name=="Matamoros (IF)") then
        riskPush = riskPush + 1
    end
    if (cargo.dual or (cargo.risk >= 0.12)) then riskPush = riskPush + 1 end
    if (tag.name ~= "standard") then riskPush = riskPush + 1 end
    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


     -- Guardrail: cap strong outcomes (denied/anchorage review) total per day
     -- Guardrail: cap strong outcomes (denied/anchorage review) total per day
Line 186: Line 252:
     end
     end


     -- Sub rules & notes
     -- Sub-rules & notes
     local notes = "—"
     local notes = string.format("Cargo: %s [%s]; Partner: %s — ", cargo.name, tag.name, partner.code)
     if vessel=="Undeclared submarine" then
     if vessel=="Undeclared submarine" then
         act  = {name="Submarine declare signal issued", basis="Submarine Surface/Declare"}
         act  = {name="Submarine declare signal issued", basis="Submarine Surface/Declare"}
         cite = clauseIndex[act.basis]
         cite = clauseIndex[act.basis]
         notes= "Undeclared contact directed to surface and declare per procedure"
         notes=notes.."Undeclared contact directed to surface and declare per procedure"
         if out=="No reports" then out="Administrative notice issued; monitoring" end
         if out=="No reports" then out="Administrative notice issued; monitoring" end
     elseif vessel=="Declared submarine" then
     elseif vessel=="Declared submarine" then
         notes = "Declared sub transited in daylight per SC §C2–C3"
         notes=notes.."Declared sub transited in daylight per SC §C2–C3"
     elseif sector=="Sacred Buffer Approaches" then
     elseif sector=="Sacred Buffer Approaches" then
         notes = "Stand-off distance verified for sacred sites"
         notes=notes.."Stand-off distance verified for sacred sites"
     elseif sector=="Environmental Buffer" then
     elseif sector=="Environmental Buffer" then
         notes = "Sensitive habitat protection enforced during passage"
         notes=notes.."Sensitive habitat protection enforced during passage"
    end
 
    -- IF/Matamoros narrative softener for delays (keep language neutral)
    if (flag.name=="Imperial Federation" or flag.name=="Matamoros (IF)")
      and (out=="Random audit hold; clearance pending procedural update"
            or out=="Documentation harmonization hold (routine)") then
        notes = notes.."; sampled under routine process calibration"
     end
     end


    -- Count enforcement share for later balancing
     if out~="No reports" then state.enfCount = state.enfCount + 1 end
     if out~="No reports" then state.enfCount = state.enfCount + 1 end


Line 233: Line 305:
         lastFlag    = nil,
         lastFlag    = nil,
         enfCount    = 0,
         enfCount    = 0,
         maxPerFlag  = maxPerFlag   -- << ADD THIS LINE
         maxPerFlag  = maxPerFlag
     }
     }


Line 250: Line 322:
             if ev.outcome ~= "No reports" and ev.outcome ~= "Compliant; cleared" then
             if ev.outcome ~= "No reports" and ev.outcome ~= "Compliant; cleared" then
                 ev.outcome = "Compliant; cleared"
                 ev.outcome = "Compliant; cleared"
                 ev.notes   = (ev.notes=="—" and "Auto-balanced for daily transparency") or (ev.notes.."; auto-balanced")
                 if ev.notes=="—" then
                    ev.notes = "Auto-balanced for daily transparency"
                else
                    ev.notes = ev.notes.."; auto-balanced"
                end
                 toReduce = toReduce - 1
                 toReduce = toReduce - 1
             end
             end
Line 262: Line 338:


     table.insert(out, '|}')
     table.insert(out, '|}')
     table.insert(out, '<!-- StraitsDaily v3.0 (no explicit IF/Floria; Moorland+Bassaridian included; per-flag cap & smoothing) -->')
     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

Revision as of 16:01, 6 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

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"
}

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

-- High-risk cargo categories (dual-use emphasized). Weight ~appearance frequency; risk amplifies enforcement odds.
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="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}
}

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

-- 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 = {
    {code="P-07", weight=4, risk=0.00},
    {code="P-12", weight=3, risk=0.03},
    {code="P-17", weight=2, risk=0.09}, -- elevated scrutiny
    {code="P-22", weight=2, risk=0.11}, -- elevated scrutiny
    {code="P-31", weight=3, risk=0.05},
    {code="P-38", weight=2, risk=0.10}, -- elevated scrutiny
    {code="P-44", weight=4, risk=0.02},
    {code="P-53", weight=3, risk=0.04}
}

local clauses = {
    {key="Pilotage & Traffic Scheme", cite="SC §A2–A4"},
    {key="Tonnage & Warship Limits",  cite="SC §B1–B4"},
    {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 actions = {
    {name="Documents & pilotage verification",              weight=5, basis="Pilotage & Traffic Scheme"},
    {name="Tonnage/armament declaration check",             weight=4, basis="Tonnage & Warship Limits"},
    {name="Cargo manifest reconciliation audit",            weight=4, basis="Right-of-Visit (Suspicion)"},
    {name="Indirect-partner disclosure & vetting",          weight=3, basis="Right-of-Visit (Suspicion)"},
    {name="Environmental reroute order",                    weight=3, basis="Environmental Buffer"},
    {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 = {
    "Compliant; cleared",
    "Minor non-compliance; corrected; cleared",
    "Rerouted; passage maintained",
    "Inspection completed; cargo intact",
    "Administrative notice issued; monitoring",
    "Random audit hold; clearance pending procedural update",
    "Documentation harmonization hold (routine)",
    "Escorted to anchorage for formal review",
    "Denied entry pending documents",
    "SAR/aid rendered; voyage resumed",
    "No reports"
}

-------------------------------------------------------------------------------
-- (C) UTILITIES
-------------------------------------------------------------------------------
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 pick(list) return list[math.random(#list)] end

local clauseIndex = (function()
    local t={} for _,c in ipairs(clauses) do t[c.key]=c.cite end; return t
end)()

-------------------------------------------------------------------------------
-- (D) ENFORCEMENT PROBABILITY & FLAG SMOOTHING
-------------------------------------------------------------------------------
local function baseProb(flag, sector, cargo, tag, partner)
    local p = 0.28
    if flag.risky then p = p + 0.06 end
    if sector=="Sacred Buffer Approaches" or sector=="Environmental Buffer" then p = p + 0.08 end

    -- High-risk cargo amplification (dual-use + category risk + tag risk)
    p = p + (cargo and (cargo.risk or 0) or 0)
    if cargo and cargo.dual then p = p + 0.03 end
    p = p + (tag and (tag.risk or 0) or 0)

    -- Indirect partner latent risk
    p = p + (partner and (partner.risk or 0) or 0)

    -- Ceiling
    if p > 0.72 then p = 0.72 end
    return p
end

-- To avoid over-mentioning, impose per-flag cap and discourage repeats.
local function pickFlagWithCaps(flagCounts, maxPerFlag, lastFlagName)
    local tries = 0
    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
    for _,f in ipairs(flags) do
        if (flagCounts[f.name] or 0) < maxPerFlag then return f end
    end
    return flags[#flags]
end

-------------------------------------------------------------------------------
-- (E) EVENT GENERATION
-------------------------------------------------------------------------------
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
    state.lastFlag = flag.name

    local pEnf = baseProb(flag, sector, cargo, tag, partner)

    -- Most transits are routine; if not enforced, record compliant passage
    if math.random() > pEnf then
        return {
            date=dateText, sector=sector,
            vessel=vessel.." ("..flag.name..")",
            action="Routine transit", legal="N/A",
            outcome="No reports",
            notes=string.format("Cargo: %s [%s]; Partner: %s — Compliant passage",
                                cargo.name, tag.name, partner.code)
        }
    end

    -- Choose action & legal
    local act  = pickWeighted(actions)
    local cite = clauseIndex[act.basis] or "SC §—"

    -- Outcome selection with subtle bias: IF/Matamoros + high-risk cargo/tags/partner → procedural holds more likely
    local out = pick(outcomes)
    local riskPush = 0
    if (flag.name=="Imperial Federation" or flag.name=="Matamoros (IF)") then
        riskPush = riskPush + 1
    end
    if (cargo.dual or (cargo.risk >= 0.12)) then riskPush = riskPush + 1 end
    if (tag.name ~= "standard") then riskPush = riskPush + 1 end
    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

    -- Guardrail: cap strong outcomes (denied/anchorage review) total per day
    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

    -- Sub-rules & notes
    local notes = string.format("Cargo: %s [%s]; Partner: %s — ", cargo.name, tag.name, partner.code)
    if vessel=="Undeclared submarine" then
        act  = {name="Submarine declare signal issued", basis="Submarine Surface/Declare"}
        cite = clauseIndex[act.basis]
        notes=notes.."Undeclared contact directed to surface and declare per procedure"
        if out=="No reports" then out="Administrative notice issued; monitoring" end
    elseif vessel=="Declared submarine" then
        notes=notes.."Declared sub transited in daylight per SC §C2–C3"
    elseif sector=="Sacred Buffer Approaches" then
        notes=notes.."Stand-off distance verified for sacred sites"
    elseif sector=="Environmental Buffer" then
        notes=notes.."Sensitive habitat protection enforced during passage"
    end

    -- IF/Matamoros narrative softener for delays (keep language neutral)
    if (flag.name=="Imperial Federation" or flag.name=="Matamoros (IF)")
       and (out=="Random audit hold; clearance pending procedural update"
            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

    return {
        date=dateText, sector=sector,
        vessel=vessel.." ("..flag.name..")",
        action=act.name, legal=(act.basis.." — "..cite),
        outcome=out, notes=notes
    }
end

-------------------------------------------------------------------------------
-- (F) PUBLIC: makeTable
-------------------------------------------------------------------------------
function p.makeTable(frame)
    local dateInfo = getPSSCDate()
    math.randomseed(dateInfo.year*1000 + dateInfo.dayOfYear)

    local ROWS = tonumber(frame and frame.args and frame.args.rows) or 18
    local out = {}
    table.insert(out, '{| class="wikitable sortable"')
    table.insert(out, '! Date !! Sector !! Vessel/Flag !! Action !! Legal Basis !! Outcome !! Notes')

    -- Per-flag cap: ~35% of rows per registry max (rounded up)
    local maxPerFlag = math.max(1, math.ceil(ROWS * 0.35))

    local state = {
        strongCount = 0,           -- denied/anchorage combined
        strongCap   = 3,           -- daily cap
        flagCounts  = {},
        lastFlag    = nil,
        enfCount    = 0,
        maxPerFlag  = maxPerFlag
    }

    local events = {}
    for i=1, ROWS do
        local ev = makeEvent(dateInfo.text, state)
        table.insert(events, ev)
    end

    -- Balance guardrail: keep total enforcement ≤ 40% of rows
    local maxEnf = math.floor(ROWS * 0.40)
    if state.enfCount > maxEnf then
        local toReduce = state.enfCount - maxEnf
        for _,ev in ipairs(events) do
            if toReduce <= 0 then break end
            if ev.outcome ~= "No reports" and ev.outcome ~= "Compliant; cleared" then
                ev.outcome = "Compliant; cleared"
                if ev.notes=="—" then
                    ev.notes = "Auto-balanced for daily transparency"
                else
                    ev.notes = ev.notes.."; auto-balanced"
                end
                toReduce = toReduce - 1
            end
        end
    end

    for _,ev in ipairs(events) do
        table.insert(out, "|-")
        table.insert(out, "| "..ev.date.." || "..ev.sector.." || "..ev.vessel.." || "..ev.action.." || "..ev.legal.." || "..ev.outcome.." || "..ev.notes)
    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")
end

return p