Module:StraitsDaily: Difference between revisions

From MicrasWiki
Jump to navigationJump to search
NewZimiaGov (talk | contribs)
No edit summary
NewZimiaGov (talk | contribs)
No edit summary
Line 1: Line 1:
-------------------------------------------------------------------------------
-- Module: StraitsDaily
-- Module: StraitsDaily
-- Purpose: Daily, in-world report of Strait of Haifa enforcement
-- Purpose: Daily, in-world report of Strait of Haifa enforcement
Line 40: Line 39:
}
}


-- 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=true},
     {name="Matamoros (IF)",                      weight=2, risky=true}, -- slight audit bias later
     {name="Matamoros (IF)",                      weight=2, risky=true},
     {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},
Line 51: Line 49:
     {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 59: Line 57:
}
}


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


Line 145: Line 106:


local function pick(list) return list[math.random(#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
-- (D) ENFORCEMENT LOGIC
-------------------------------------------------------------------------------
-------------------------------------------------------------------------------
local function baseProb(flag, sector, cargo, tag, partner)
local function baseProb(flag, sector, cargo, tag, partner)
Line 157: Line 114:
     if flag.risky then p = p + 0.06 end
     if flag.risky then p = p + 0.06 end
     if sector=="Sacred Buffer Approaches" or sector=="Environmental Buffer" then p = p + 0.08 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)
     p = p + (cargo and (cargo.risk or 0) or 0)
     if cargo and cargo.dual then p = p + 0.03 end
     if cargo and cargo.dual then p = p + 0.03 end
     p = p + (tag and (tag.risk or 0) or 0)
     p = p + (tag and (tag.risk or 0) or 0)
    -- Indirect partner latent risk
     p = p + (partner and (partner.risk or 0) or 0)
     p = p + (partner and (partner.risk or 0) or 0)
    -- Ceiling
     if p > 0.72 then p = 0.72 end
     if p > 0.72 then p = 0.72 end
     return p
     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
end


-------------------------------------------------------------------------------
-------------------------------------------------------------------------------
-- (E) EVENT GENERATION
-- (E) MAIN 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)
function p.makeTable(frame)
Line 296: Line 134:
     table.insert(out, '! Date !! Sector !! Vessel/Flag !! Action !! Legal Basis !! Outcome !! Notes')
     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 maxPerFlag = math.max(1, math.ceil(ROWS * 0.35))
     local state = {
     local state = {
         strongCount = 0,           -- denied/anchorage combined
         strongCount = 0, strongCap = 3, flagCounts = {}, lastFlag = nil,
        strongCap   = 3,           -- daily cap
         enfCount = 0, maxPerFlag = maxPerFlag
        flagCounts = {},
        lastFlag   = nil,
         enfCount   = 0,
        maxPerFlag = maxPerFlag
     }
     }


     local events = {}
     local function makeEvent(dateText)
    for i=1, ROWS do
        local sector = pick(sectors)
         local ev = makeEvent(dateInfo.text, state)
        local flag = pickWeighted(flags)
         table.insert(events, ev)
        local vessel = pick(vessels)
    end
        local cargo = pickWeighted(cargoTypes)
        local tag = pickWeighted(cargoTags)
        local partner = pickWeighted(partnerProfiles)
 
        local prob = baseProb(flag, sector, cargo, tag, partner)
        if math.random() > prob then
            return {date=dateText, sector=sector, vessel=vessel.." ("..flag.name..")",
                    action="Routine transit", legal="N/A", outcome="No reports",
                    notes="Cargo: "..cargo.name.." ["..tag.name.."]; Partner: "..partner.code.." — Compliant passage"}
        end
 
        local action = pickWeighted(actions)
        local legal = action.basis.." — "..(clauses[action.basis] or "SC §—")
        local outcome = pick(outcomes)
 
         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 >= 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
            local delays = {
                "Random audit hold; clearance pending procedural update",
                "Documentation harmonization hold (routine)",
                "Administrative notice issued; monitoring"
            }
            outcome = delays[math.random(#delays)]
        end


    -- Balance guardrail: keep total enforcement ≤ 40% of rows
        if outcome:match("anchorage") or outcome:match("Denied entry") then
    local maxEnf = math.floor(ROWS * 0.40)
             if state.strongCount >= state.strongCap then
    if state.enfCount > maxEnf then
                 outcome = "Administrative notice issued; monitoring"
        local toReduce = state.enfCount - maxEnf
            else
        for _,ev in ipairs(events) do
                 state.strongCount = state.strongCount + 1
            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
         end
        if (flag.name == "Imperial Federation" or flag.name == "Matamoros (IF)") and
          (outcome:match("audit hold") or outcome:match("harmonization")) then
            outcome = outcome .. " [Extended regulatory review]"
        end
        state.enfCount = state.enfCount + 1
        local note = "Cargo: "..cargo.name.." ["..tag.name.."]; Partner: "..partner.code
        return {date=dateText, sector=sector, vessel=vessel.." ("..flag.name..")",
                action=action.name, legal=legal, outcome=outcome, notes=note}
     end
     end


     for _,ev in ipairs(events) do
     for i=1, ROWS do
        local ev = makeEvent(dateInfo.text)
         table.insert(out, "|-")
         table.insert(out, "|-")
         table.insert(out, "| "..ev.date.." || "..ev.sector.." || "..ev.vessel.." || "..ev.action.." || "..ev.legal.." || "..ev.outcome.." || "..ev.notes)
         table.insert(out, "| "..ev.date.." || "..ev.sector.." || "..ev.vessel.." || "..ev.action.." || "..ev.legal.." || "..ev.outcome.." || "..ev.notes)
Line 338: Line 201:


     table.insert(out, '|}')
     table.insert(out, '|}')
     table.insert(out, '<!-- StraitsDaily v4.1 (high-risk cargo focus; coded partner scrutiny; neutral procedural holds) -->')
     table.insert(out, '<!-- StraitsDaily v5.0: Shelf-life tags, military-adjacent profiling, enhanced IF/Matamoros visibility -->')
     return table.concat(out, "\n")
     return table.concat(out, "\n")
end
end


return p
return p

Revision as of 16:48, 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"
}

local flags = {
    {name="Bassaridian-flag",                     weight=6, risky=false},
    {name="Moorland",                             weight=2, risky=false},
    {name="Imperial Federation",                  weight=5, risky=true},
    {name="Matamoros (IF)",                       weight=2, risky=true},
    {name="Neutral Merchant",                     weight=4, risky=false},
    {name="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"
}

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

-------------------------------------------------------------------------------
-- (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

-------------------------------------------------------------------------------
-- (D) ENFORCEMENT LOGIC
-------------------------------------------------------------------------------
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
    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)
    p = p + (partner and (partner.risk or 0) or 0)
    if p > 0.72 then p = 0.72 end
    return p
end

-------------------------------------------------------------------------------
-- (E) MAIN EVENT GENERATION
-------------------------------------------------------------------------------
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')

    local maxPerFlag = math.max(1, math.ceil(ROWS * 0.35))
    local state = {
        strongCount = 0, strongCap = 3, flagCounts = {}, lastFlag = nil,
        enfCount = 0, maxPerFlag = maxPerFlag
    }

    local function makeEvent(dateText)
        local sector = pick(sectors)
        local flag = pickWeighted(flags)
        local vessel = pick(vessels)
        local cargo = pickWeighted(cargoTypes)
        local tag = pickWeighted(cargoTags)
        local partner = pickWeighted(partnerProfiles)

        local prob = baseProb(flag, sector, cargo, tag, partner)
        if math.random() > prob then
            return {date=dateText, sector=sector, vessel=vessel.." ("..flag.name..")",
                    action="Routine transit", legal="N/A", outcome="No reports",
                    notes="Cargo: "..cargo.name.." ["..tag.name.."]; Partner: "..partner.code.." — Compliant passage"}
        end

        local action = pickWeighted(actions)
        local legal = action.basis.." — "..(clauses[action.basis] or "SC §—")
        local outcome = pick(outcomes)

        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 >= 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
            local delays = {
                "Random audit hold; clearance pending procedural update",
                "Documentation harmonization hold (routine)",
                "Administrative notice issued; monitoring"
            }
            outcome = delays[math.random(#delays)]
        end

        if outcome:match("anchorage") or outcome:match("Denied entry") then
            if state.strongCount >= state.strongCap then
                outcome = "Administrative notice issued; monitoring"
            else
                state.strongCount = state.strongCount + 1
            end
        end

        if (flag.name == "Imperial Federation" or flag.name == "Matamoros (IF)") and 
           (outcome:match("audit hold") or outcome:match("harmonization")) then
            outcome = outcome .. " [Extended regulatory review]"
        end

        state.enfCount = state.enfCount + 1

        local note = "Cargo: "..cargo.name.." ["..tag.name.."]; Partner: "..partner.code
        return {date=dateText, sector=sector, vessel=vessel.." ("..flag.name..")",
                action=action.name, legal=legal, outcome=outcome, notes=note}
    end

    for i=1, ROWS do
        local ev = makeEvent(dateInfo.text)
        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 v5.0: Shelf-life tags, military-adjacent profiling, enhanced IF/Matamoros visibility -->')
    return table.concat(out, "\n")
end

return p