Incident Report
Staged incident reporting against the clock a regime imposes. An incident is opened with the regime it falls under, and the deadlines for the initial, intermediate and final reports follow from that: four hours for a DORA initial notification, twenty-four for GDPR, NIS2, the CRA and HIPAA, seventy-two hours for every intermediate report, and a final report at fourteen, thirty or sixty days depending on the regime.
One incident often reports under more than one regime, which is why the deadlines belong to the incident rather than the deployment. The regime is named when the incident is opened, so an operator is not computing dates during an incident.
Every deadline is measured from detectedAt, the moment the organization became aware, and not from the moment the incident was recorded. That is what the regimes say, and it means an incident entered two hours after detection already has two hours gone.
The deadline clock watches the open incidents and notifies as each deadline approaches, at half, three quarters and nine tenths of the window by default. It stops tracking an incident once its submission is acknowledged, so what remains is what is still outstanding.
b.incident.report.create(opts?) #
{
deadlines: object, // initial / intermediate / final, in ms
onStage: object, // function (event) per stage; see the prose for its fields
persist: object, // function (record) per write; not read back
now: object, // function returning epoch ms; for tests
audit: boolean, // false silences the audit events
}
Open an incident register. The returned object answers open, recordInitial, recordIntermediate, recordFinal, get, list and status.
status() answers across the register, not per incident: total, how many are open and closed, and a late count for each of the three stages. Which stage one incident owes next comes from get(id).
open(spec) requires two fields. regime decides the deadlines: gdpr, nis2, dora, cra and hipaa are known, and any other string is accepted and takes the default twenty-four hour, seventy-two hour and thirty-day windows; an empty or non-string one raises incident-report/bad-regime, because an incident with no regime has no deadline. detectedAt must be a finite epoch-millisecond timestamp and has no default (incident-report/bad-detected-at): every deadline is computed from it, so a guessed value would produce the wrong one.
scope, summary and impact are recorded when given and stored as null otherwise. They are what the filings are written from, so an incident opened without them has a deadline and nothing to report against it.
deadlines overrides any of the three windows, per register.
persist is an outbound sink, not storage the register reads back: it is called with each record as it is written, and create always starts empty. There is no restoration path, and open is not one, since it mints a new id, empties stages and clears closedAt: replaying a persisted record through it would turn a filed incident back into an outstanding one.
So a restart loses get, list, status and the deadline clock for every incident opened before it. An incident usually outlasts the process handling it, so treat what persist receives as the record of what happened, and read it with the deployment's own tooling rather than expecting the register to come back.
onStage is called as each stage is recorded, with one event object carrying incidentId, stage, dueBy, late and regime, plus fields, the payload that stage was recorded with. This is where a deployment wires its own notification; late is the field worth routing on.
A stage keeps a structured copy of the fields it was filed with, not the object that was passed, so mutating or reusing that object afterwards does not change what the record says was filed. onStage still receives the object as given.
The copy holds the data rather than the classes around it. Dates, Maps, Sets and cycles survive; a Buffer comes back as a Uint8Array over the same bytes, and a value whose toJSON lives on its prototype comes back as a plain object, so serializing the record can differ from serializing what was passed. Pass plain data for anything a filing will be rendered from.
A payload holding something that cannot be copied at all, such as a function, is kept by reference instead of being emptied, since losing the filing's contents is worse than sharing the object.
audit is false to stay silent, true or omitted for the framework chain, or a configuration object such as { sink: yourAudit } to send the events somewhere else. Anything else raises incident-report/bad-opt: a string or a number was read as "not false" and left the events on while the configuration said they were off.
var b = require("@blamejs/core");
var incidents = b.incident.report.create();
var rec = await incidents.open({
regime: "dora",
detectedAt: Date.now(),
scope: "payments-api",
summary: "Elevated error rate from one provider",
impact: "degraded",
});
await incidents.recordInitial(rec.id, { reportedTo: "BaFin" });
incidents.status(); // → { total, open, closed, late: { … } }
b.incident.report.createDeadlineClock(opts?) #
{
notify: object, // object with send(message); receives each alert
approachThresholds: array, // fractions in (0,1); default [0.5, 0.75, 0.9]
intervalMs: number, // default 1 minute
autoStart: boolean, // false leaves it to tick() or start()
now: object, // function returning epoch ms; for tests
audit: boolean, // false silences the audit events
}
Watch open incidents and notify before each reporting deadline. The clock answers track, untrack, acknowledgeSubmission, tick, start, stop and status.
track(record) starts watching one incident. Each tick measures how much of the window has elapsed and notifies for the highest threshold that has been crossed and not yet fired, once per threshold per stage: half, three quarters and nine tenths of the window by default. A tick that jumps past more than one threshold therefore sends one alert naming the highest, not one per threshold. A threshold outside the open interval between 0 and 1 raises incident-report/bad-threshold, since a threshold at 0 fires immediately and one at 1 fires only once the deadline has passed.
acknowledgeSubmission is how an incident stops being chased: once the stage is submitted, its deadline no longer raises an alert. autoStart is on, so the clock begins ticking every minute when it is created; tick() drives it by hand instead, which is how it is tested. status() answers { tracked, running, intervalMs }, which is the clock's own state rather than any incident's.
autoStart and audit must be booleans, raising incident-report/bad-opt otherwise. Anything else was read as "not false", so a typo left the timer running and the audit events on while the configuration said they were off.
var b = require("@blamejs/core");
var clock = b.incident.report.createDeadlineClock({
autoStart: false,
notify: { send: function (m) { console.warn(m.summary); } },
});
clock.track(rec);
clock.tick(); // → notifies the highest threshold crossed
clock.status(); // → { tracked, running, intervalMs }
clock.acknowledgeSubmission(rec.id, "initial");
clock.stop();