@deployanyway/excuse-js 0.4.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/MIGRATION.md +6 -0
- package/README.md +35 -0
- package/dist/index.cjs +58 -0
- package/examples/basic.js +4 -0
- package/examples/own-it.mjs +13 -0
- package/index.d.cts +34 -0
- package/index.d.ts +34 -0
- package/package.json +7 -4
- package/src/cli.js +34 -1
- package/src/incident.js +83 -0
- package/src/index.js +1 -0
- package/src/input.js +15 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.0
|
|
4
|
+
|
|
5
|
+
- Own It: incident updates with accountable facts.
|
|
6
|
+
- Typed API, CLI integration, runnable codebase example and meaningful workflow tests.
|
|
7
|
+
- Stable contracts and migration guidance; original humor stays around accurate facts.
|
|
8
|
+
|
|
3
9
|
## 0.4.0
|
|
4
10
|
|
|
5
11
|
132 original workplace-safe excuses: twelve distinct phrases in each of eleven categories. Seeded batches avoid repeats until all twelve phrases in the category have been used; larger batches cycle. Reports pair the joke with an honest next step. The jokes are for humor, not hiding incidents or misleading teammates.
|
package/MIGRATION.md
CHANGED
|
@@ -9,3 +9,9 @@ Install 0.3.0 with npm. Seeds and exact humorous wording are version-specific. D
|
|
|
9
9
|
132 original phrases across eleven categories; twelve unique items before batch cycling; independent listExcuses catalogs and CLI --catalog; specific conflict-resolution next step.
|
|
10
10
|
|
|
11
11
|
Seeded selections are version-specific and may differ after catalog expansion. Pin the version for exact reproducibility. Existing user messages and API entry points remain supported.
|
|
12
|
+
|
|
13
|
+
## 0.4.0 to stable 1.0.0
|
|
14
|
+
|
|
15
|
+
Incident drafting is additive and does not change seeded excuse APIs. Public incident output defaults to humor off. Existing excuse/--report behavior remains explicitly humorous.
|
|
16
|
+
|
|
17
|
+
See README for exact contracts, bounds and failure behavior.
|
package/README.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# excuse-js
|
|
2
2
|
|
|
3
|
+
## Own It: incident updates with accountable facts (1.0.0)
|
|
4
|
+
|
|
5
|
+
incidentUpdate(facts, options) drafts an honest status update from supplied service, impact, action, owner and nextUpdate. Missing fields stay explicit; it invents no impact, remediation, owner or ETA. Default public output contains no comic relief.
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
import { incidentUpdate } from "@deployanyway/excuse-js";
|
|
9
|
+
const update = incidentUpdate({
|
|
10
|
+
status: "identified",
|
|
11
|
+
service: "API",
|
|
12
|
+
impact: "Some requests return 503",
|
|
13
|
+
action: "Rolling back the latest release",
|
|
14
|
+
owner: "On-call developer",
|
|
15
|
+
nextUpdate: "2026-10-09T02:15:00Z",
|
|
16
|
+
});
|
|
17
|
+
if (!update.complete) console.error("Missing facts:", update.missing);
|
|
18
|
+
console.log(update.message); // draft only; no message is sent
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Statuses: investigating (default), identified, monitoring, resolved. nextUpdate and optional now use UTC ISO timestamps. Missing unresolved nextUpdate is reported; a resolved status does not require a next update. overdue marks a scheduled update in the past. complete reflects required facts, not a verification that an incident is fixed. Strings are bounded and terminal control characters are removed. Result fields are copied and expose message/missing/complete/overdue.
|
|
22
|
+
|
|
23
|
+
For internal updates only, opt into {audience: 'internal', humor: true, seed: 'demo', category: 'production'}. Comic relief is labeled separately and leaves facts unchanged. Public + humor throws rather than quietly adding a joke. Internal jokes are original catalog content; exact wording is version-specific.
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
printf '%s' '{"service":"API","impact":"Some requests fail","action":"Investigating","owner":"On-call","nextUpdate":"2026-10-09T02:15:00Z"}' | excuse-js --incident --json --require-complete
|
|
27
|
+
node node_modules/@deployanyway/excuse-js/examples/own-it.mjs incident.json
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
--incident accepts JSON stdin (256 KiB), --audience public/internal, --humor, --seed, --now and --json. --require-complete exits 1 when facts are missing; default draft output exits 0 and invalid input exits 2. Neither the API nor CLI posts an update. Review facts before sharing.
|
|
31
|
+
|
|
32
|
+
## Stable v1 contract
|
|
33
|
+
|
|
34
|
+
Node 22.13+ or Node 24. MIT licensed. CLI flags, structured fields, ESM/CommonJS exports and declarations are covered by tests and installed-package checks. Existing 0.4 APIs remain available except the explicitly documented doggo-log redaction/text-context changes. Future incompatible public API changes require a major release; callers should consume structured fields rather than parse jokes. Exact humorous wording and seeded catalog choices are version-specific. No telemetry, external API keys or network service is needed for core use.
|
|
35
|
+
|
|
36
|
+
Run npm test, npm run lint, npm run format:check, npm run coverage, npm run test:types and npm run verify:package from a source checkout. Runnable examples are shipped under examples/. The root demo is https://deployanyway.github.io/.
|
|
37
|
+
|
|
3
38
|
## A meeting-sized library (0.4.0)
|
|
4
39
|
|
|
5
40
|
132 original workplace-safe excuses: twelve distinct phrases in each of eleven categories. Seeded batches avoid repeats until all twelve phrases in the category have been used; larger batches cycle. Reports pair the joke with an honest next step. The jokes are for humor, not hiding incidents or misleading teammates.
|
package/dist/index.cjs
CHANGED
|
@@ -24,6 +24,7 @@ __export(index_exports, {
|
|
|
24
24
|
excuse: () => excuse,
|
|
25
25
|
excuseBatch: () => excuseBatch,
|
|
26
26
|
excuseReport: () => excuseReport,
|
|
27
|
+
incidentUpdate: () => incidentUpdate,
|
|
27
28
|
listExcuses: () => listExcuses
|
|
28
29
|
});
|
|
29
30
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -186,6 +187,62 @@ var phrases = {
|
|
|
186
187
|
]
|
|
187
188
|
};
|
|
188
189
|
|
|
190
|
+
// src/incident.js
|
|
191
|
+
var iso = (value, label) => {
|
|
192
|
+
if (typeof value !== "string" || !/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d{1,3})?Z$/.test(value) || !Number.isFinite(Date.parse(value)) || new Date(value).toISOString().slice(0, 19) !== value.slice(0, 19))
|
|
193
|
+
throw new TypeError(`${label} must be a UTC ISO timestamp.`);
|
|
194
|
+
return value;
|
|
195
|
+
};
|
|
196
|
+
var clean = (value) => Array.from(value).filter(
|
|
197
|
+
(char) => char.charCodeAt(0) >= 32 || char === "\n" || char === " "
|
|
198
|
+
).join("");
|
|
199
|
+
function incidentUpdate(facts = {}, options = {}) {
|
|
200
|
+
if (!facts || typeof facts !== "object" || Array.isArray(facts))
|
|
201
|
+
throw new TypeError("Incident facts must be an object.");
|
|
202
|
+
if (!options || typeof options !== "object" || Array.isArray(options))
|
|
203
|
+
throw new TypeError("Options must be an object.");
|
|
204
|
+
const status = facts.status ?? "investigating", audience = options.audience ?? "public", humor = options.humor ?? false;
|
|
205
|
+
if (!["investigating", "identified", "monitoring", "resolved"].includes(status))
|
|
206
|
+
throw new RangeError("Unsupported incident status.");
|
|
207
|
+
if (!["public", "internal"].includes(audience) || typeof humor !== "boolean")
|
|
208
|
+
throw new TypeError("Choose public/internal audience and boolean humor.");
|
|
209
|
+
if (humor && audience !== "internal")
|
|
210
|
+
throw new TypeError("Comic relief requires an internal audience.");
|
|
211
|
+
const result = { status, audience, missing: [], overdue: false };
|
|
212
|
+
for (const key of ["service", "impact", "action", "owner"]) {
|
|
213
|
+
if (facts[key] !== void 0 && facts[key] !== null && (typeof facts[key] !== "string" || !facts[key].trim() || facts[key].length > 2e3))
|
|
214
|
+
throw new TypeError(
|
|
215
|
+
`${key} must be a nonempty string up to 2000 characters.`
|
|
216
|
+
);
|
|
217
|
+
result[key] = facts[key] === void 0 || facts[key] === null ? null : clean(facts[key].trim());
|
|
218
|
+
if (result[key] === null) result.missing.push(key);
|
|
219
|
+
}
|
|
220
|
+
const now = iso(options.now ?? (/* @__PURE__ */ new Date()).toISOString(), "now");
|
|
221
|
+
result.nextUpdate = facts.nextUpdate === void 0 || facts.nextUpdate === null ? null : iso(facts.nextUpdate, "nextUpdate");
|
|
222
|
+
if (result.nextUpdate === null && status !== "resolved")
|
|
223
|
+
result.missing.push("nextUpdate");
|
|
224
|
+
result.overdue = status !== "resolved" && result.nextUpdate !== null && Date.parse(result.nextUpdate) < Date.parse(now);
|
|
225
|
+
const lines = [
|
|
226
|
+
`Status: ${status}`,
|
|
227
|
+
`Service: ${result.service ?? "Not provided"}`,
|
|
228
|
+
`Impact: ${result.impact ?? "Not provided \u2014 verify before claiming an impact"}`,
|
|
229
|
+
`Current action: ${result.action ?? "Not provided"}`,
|
|
230
|
+
`Owner: ${result.owner ?? "Not assigned"}`,
|
|
231
|
+
`Next update: ${result.nextUpdate ?? (status === "resolved" ? "Not scheduled (status marked resolved)" : "Not scheduled")}${result.overdue ? " \u2014 OVERDUE" : ""}`
|
|
232
|
+
];
|
|
233
|
+
if (result.missing.length)
|
|
234
|
+
lines.push("Missing facts: " + result.missing.join(", "));
|
|
235
|
+
result.complete = result.missing.length === 0;
|
|
236
|
+
if (humor) {
|
|
237
|
+
result.comicRelief = excuse(options.category ?? "production", {
|
|
238
|
+
seed: options.seed
|
|
239
|
+
});
|
|
240
|
+
lines.push("\nComic relief (internal only): " + result.comicRelief);
|
|
241
|
+
}
|
|
242
|
+
result.message = lines.join("\n");
|
|
243
|
+
return result;
|
|
244
|
+
}
|
|
245
|
+
|
|
189
246
|
// src/index.js
|
|
190
247
|
var nextSteps = {
|
|
191
248
|
"merge-conflict": "Understand both changes, resolve the conflict deliberately, then run tests for the combined behavior. Git does not accept a coin toss.",
|
|
@@ -267,5 +324,6 @@ function listExcuses(category = "deployment") {
|
|
|
267
324
|
excuse,
|
|
268
325
|
excuseBatch,
|
|
269
326
|
excuseReport,
|
|
327
|
+
incidentUpdate,
|
|
270
328
|
listExcuses
|
|
271
329
|
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { incidentUpdate } from "@deployanyway/excuse-js";
|
|
3
|
+
try {
|
|
4
|
+
const facts = process.argv[2]
|
|
5
|
+
? JSON.parse(readFileSync(process.argv[2], "utf8"))
|
|
6
|
+
: {};
|
|
7
|
+
const draft = incidentUpdate(facts);
|
|
8
|
+
console.log(draft.message);
|
|
9
|
+
process.exitCode = draft.complete ? 0 : 1;
|
|
10
|
+
} catch (error) {
|
|
11
|
+
console.error(error.message);
|
|
12
|
+
process.exitCode = 2;
|
|
13
|
+
}
|
package/index.d.cts
CHANGED
|
@@ -30,3 +30,37 @@ export function excuseReport(
|
|
|
30
30
|
): ExcuseReport;
|
|
31
31
|
|
|
32
32
|
export function listExcuses(category?: Category): string[];
|
|
33
|
+
export type IncidentStatus =
|
|
34
|
+
"investigating" | "identified" | "monitoring" | "resolved";
|
|
35
|
+
export interface IncidentFacts {
|
|
36
|
+
status?: IncidentStatus;
|
|
37
|
+
service?: string | null;
|
|
38
|
+
impact?: string | null;
|
|
39
|
+
action?: string | null;
|
|
40
|
+
owner?: string | null;
|
|
41
|
+
nextUpdate?: string | null;
|
|
42
|
+
}
|
|
43
|
+
export interface IncidentOptions extends ExcuseOptions {
|
|
44
|
+
audience?: "public" | "internal";
|
|
45
|
+
humor?: boolean;
|
|
46
|
+
category?: Category;
|
|
47
|
+
now?: string;
|
|
48
|
+
}
|
|
49
|
+
export interface IncidentUpdate {
|
|
50
|
+
complete: boolean;
|
|
51
|
+
status: IncidentStatus;
|
|
52
|
+
audience: "public" | "internal";
|
|
53
|
+
service: string | null;
|
|
54
|
+
impact: string | null;
|
|
55
|
+
action: string | null;
|
|
56
|
+
owner: string | null;
|
|
57
|
+
nextUpdate: string | null;
|
|
58
|
+
missing: string[];
|
|
59
|
+
overdue: boolean;
|
|
60
|
+
comicRelief?: string;
|
|
61
|
+
message: string;
|
|
62
|
+
}
|
|
63
|
+
export function incidentUpdate(
|
|
64
|
+
facts?: IncidentFacts,
|
|
65
|
+
options?: IncidentOptions,
|
|
66
|
+
): IncidentUpdate;
|
package/index.d.ts
CHANGED
|
@@ -30,3 +30,37 @@ export function excuseReport(
|
|
|
30
30
|
): ExcuseReport;
|
|
31
31
|
|
|
32
32
|
export function listExcuses(category?: Category): string[];
|
|
33
|
+
export type IncidentStatus =
|
|
34
|
+
"investigating" | "identified" | "monitoring" | "resolved";
|
|
35
|
+
export interface IncidentFacts {
|
|
36
|
+
status?: IncidentStatus;
|
|
37
|
+
service?: string | null;
|
|
38
|
+
impact?: string | null;
|
|
39
|
+
action?: string | null;
|
|
40
|
+
owner?: string | null;
|
|
41
|
+
nextUpdate?: string | null;
|
|
42
|
+
}
|
|
43
|
+
export interface IncidentOptions extends ExcuseOptions {
|
|
44
|
+
audience?: "public" | "internal";
|
|
45
|
+
humor?: boolean;
|
|
46
|
+
category?: Category;
|
|
47
|
+
now?: string;
|
|
48
|
+
}
|
|
49
|
+
export interface IncidentUpdate {
|
|
50
|
+
complete: boolean;
|
|
51
|
+
status: IncidentStatus;
|
|
52
|
+
audience: "public" | "internal";
|
|
53
|
+
service: string | null;
|
|
54
|
+
impact: string | null;
|
|
55
|
+
action: string | null;
|
|
56
|
+
owner: string | null;
|
|
57
|
+
nextUpdate: string | null;
|
|
58
|
+
missing: string[];
|
|
59
|
+
overdue: boolean;
|
|
60
|
+
comicRelief?: string;
|
|
61
|
+
message: string;
|
|
62
|
+
}
|
|
63
|
+
export function incidentUpdate(
|
|
64
|
+
facts?: IncidentFacts,
|
|
65
|
+
options?: IncidentOptions,
|
|
66
|
+
): IncidentUpdate;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deployanyway/excuse-js",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Seeded developer humor and factual incident status updates with owners and next steps. Finally, a dependency that takes the blame responsibly.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": {
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
"dist",
|
|
28
28
|
"index.d.ts",
|
|
29
29
|
"index.d.cts",
|
|
30
|
-
"MIGRATION.md"
|
|
30
|
+
"MIGRATION.md",
|
|
31
|
+
"examples"
|
|
31
32
|
],
|
|
32
33
|
"engines": {
|
|
33
34
|
"node": ">=22.13"
|
|
@@ -74,7 +75,9 @@
|
|
|
74
75
|
"deployment",
|
|
75
76
|
"typescript",
|
|
76
77
|
"humor",
|
|
77
|
-
"deployanyway"
|
|
78
|
+
"deployanyway",
|
|
79
|
+
"incident-update",
|
|
80
|
+
"status-update"
|
|
78
81
|
],
|
|
79
82
|
"main": "./dist/index.cjs",
|
|
80
83
|
"types": "./index.d.ts",
|
package/src/cli.js
CHANGED
|
@@ -7,7 +7,9 @@ import {
|
|
|
7
7
|
excuseBatch,
|
|
8
8
|
excuseReport,
|
|
9
9
|
listExcuses,
|
|
10
|
+
incidentUpdate,
|
|
10
11
|
} from "./index.js";
|
|
12
|
+
import { readStdin } from "./input.js";
|
|
11
13
|
|
|
12
14
|
try {
|
|
13
15
|
const { values, positionals } = parseArgs({
|
|
@@ -21,11 +23,16 @@ try {
|
|
|
21
23
|
json: { type: "boolean" },
|
|
22
24
|
report: { type: "boolean" },
|
|
23
25
|
catalog: { type: "boolean" },
|
|
26
|
+
incident: { type: "boolean" },
|
|
27
|
+
"require-complete": { type: "boolean" },
|
|
28
|
+
audience: { type: "string" },
|
|
29
|
+
humor: { type: "boolean" },
|
|
30
|
+
now: { type: "string" },
|
|
24
31
|
},
|
|
25
32
|
});
|
|
26
33
|
if (values.help) {
|
|
27
34
|
console.log(
|
|
28
|
-
"Usage: excuse-js [category] [--seed text] [--count 3] [--json]\n\nDefault category: deployment. --report pairs the joke with an honest next step.\n\nOptions:\n --seed text Repeatable selection (always a string seed)\n --count 1..20 Prepare a batch for the meeting\n --json Structured string or batch\n --catalog Show every phrase in a category\n --list List categories\n -h, --help Show help\n -v, --version Show version\n\nExample: excuse-js deployment --seed demo\nExit codes: 0 success; 2 invalid arguments.",
|
|
35
|
+
"Usage: excuse-js [category] [--seed text] [--count 3] [--json]\n\n--incident reads factual incident JSON from stdin. --audience public|internal, --humor (internal only), --now UTC-ISO and --require-complete support accountable drafts. No message is sent.\n\nDefault category: deployment. --report pairs the joke with an honest next step.\n\nOptions:\n --seed text Repeatable selection (always a string seed)\n --count 1..20 Prepare a batch for the meeting\n --json Structured string or batch\n --catalog Show every phrase in a category\n --list List categories\n -h, --help Show help\n -v, --version Show version\n\nExample: excuse-js deployment --seed demo\nExit codes: 0 success/draft; 1 incomplete required incident; 2 invalid arguments.",
|
|
29
36
|
);
|
|
30
37
|
} else if (values.version) {
|
|
31
38
|
console.log(
|
|
@@ -33,6 +40,32 @@ try {
|
|
|
33
40
|
readFileSync(new URL("../package.json", import.meta.url), "utf8"),
|
|
34
41
|
).version,
|
|
35
42
|
);
|
|
43
|
+
} else if (values.incident) {
|
|
44
|
+
if (
|
|
45
|
+
positionals.length ||
|
|
46
|
+
values.list ||
|
|
47
|
+
values.catalog ||
|
|
48
|
+
values.report ||
|
|
49
|
+
values.count !== undefined
|
|
50
|
+
)
|
|
51
|
+
throw new TypeError(
|
|
52
|
+
"--incident reads JSON stdin and cannot combine with category, list, catalog, report or count.",
|
|
53
|
+
);
|
|
54
|
+
const result = incidentUpdate(JSON.parse(await readStdin()), {
|
|
55
|
+
audience: values.audience ?? "public",
|
|
56
|
+
humor: values.humor ?? false,
|
|
57
|
+
seed: values.seed,
|
|
58
|
+
...(values.now !== undefined ? { now: values.now } : {}),
|
|
59
|
+
});
|
|
60
|
+
console.log(values.json ? JSON.stringify(result, null, 2) : result.message);
|
|
61
|
+
if (values["require-complete"] && !result.complete) process.exitCode = 1;
|
|
62
|
+
} else if (
|
|
63
|
+
values.audience !== undefined ||
|
|
64
|
+
values.humor ||
|
|
65
|
+
values.now !== undefined ||
|
|
66
|
+
values["require-complete"]
|
|
67
|
+
) {
|
|
68
|
+
throw new TypeError("--audience, --humor and --now require --incident.");
|
|
36
69
|
} else if (values.list) {
|
|
37
70
|
if (
|
|
38
71
|
positionals.length ||
|
package/src/incident.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { excuse } from "./index.js";
|
|
2
|
+
const iso = (value, label) => {
|
|
3
|
+
if (
|
|
4
|
+
typeof value !== "string" ||
|
|
5
|
+
!/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d{1,3})?Z$/.test(value) ||
|
|
6
|
+
!Number.isFinite(Date.parse(value)) ||
|
|
7
|
+
new Date(value).toISOString().slice(0, 19) !== value.slice(0, 19)
|
|
8
|
+
)
|
|
9
|
+
throw new TypeError(`${label} must be a UTC ISO timestamp.`);
|
|
10
|
+
return value;
|
|
11
|
+
};
|
|
12
|
+
const clean = (value) =>
|
|
13
|
+
Array.from(value)
|
|
14
|
+
.filter(
|
|
15
|
+
(char) => char.charCodeAt(0) >= 32 || char === "\n" || char === "\t",
|
|
16
|
+
)
|
|
17
|
+
.join("");
|
|
18
|
+
/** Draft factual incident communication; public output never adds a joke. */
|
|
19
|
+
export function incidentUpdate(facts = {}, options = {}) {
|
|
20
|
+
if (!facts || typeof facts !== "object" || Array.isArray(facts))
|
|
21
|
+
throw new TypeError("Incident facts must be an object.");
|
|
22
|
+
if (!options || typeof options !== "object" || Array.isArray(options))
|
|
23
|
+
throw new TypeError("Options must be an object.");
|
|
24
|
+
const status = facts.status ?? "investigating",
|
|
25
|
+
audience = options.audience ?? "public",
|
|
26
|
+
humor = options.humor ?? false;
|
|
27
|
+
if (
|
|
28
|
+
!["investigating", "identified", "monitoring", "resolved"].includes(status)
|
|
29
|
+
)
|
|
30
|
+
throw new RangeError("Unsupported incident status.");
|
|
31
|
+
if (!["public", "internal"].includes(audience) || typeof humor !== "boolean")
|
|
32
|
+
throw new TypeError("Choose public/internal audience and boolean humor.");
|
|
33
|
+
if (humor && audience !== "internal")
|
|
34
|
+
throw new TypeError("Comic relief requires an internal audience.");
|
|
35
|
+
const result = { status, audience, missing: [], overdue: false };
|
|
36
|
+
for (const key of ["service", "impact", "action", "owner"]) {
|
|
37
|
+
if (
|
|
38
|
+
facts[key] !== undefined &&
|
|
39
|
+
facts[key] !== null &&
|
|
40
|
+
(typeof facts[key] !== "string" ||
|
|
41
|
+
!facts[key].trim() ||
|
|
42
|
+
facts[key].length > 2000)
|
|
43
|
+
)
|
|
44
|
+
throw new TypeError(
|
|
45
|
+
`${key} must be a nonempty string up to 2000 characters.`,
|
|
46
|
+
);
|
|
47
|
+
result[key] =
|
|
48
|
+
facts[key] === undefined || facts[key] === null
|
|
49
|
+
? null
|
|
50
|
+
: clean(facts[key].trim());
|
|
51
|
+
if (result[key] === null) result.missing.push(key);
|
|
52
|
+
}
|
|
53
|
+
const now = iso(options.now ?? new Date().toISOString(), "now");
|
|
54
|
+
result.nextUpdate =
|
|
55
|
+
facts.nextUpdate === undefined || facts.nextUpdate === null
|
|
56
|
+
? null
|
|
57
|
+
: iso(facts.nextUpdate, "nextUpdate");
|
|
58
|
+
if (result.nextUpdate === null && status !== "resolved")
|
|
59
|
+
result.missing.push("nextUpdate");
|
|
60
|
+
result.overdue =
|
|
61
|
+
status !== "resolved" &&
|
|
62
|
+
result.nextUpdate !== null &&
|
|
63
|
+
Date.parse(result.nextUpdate) < Date.parse(now);
|
|
64
|
+
const lines = [
|
|
65
|
+
`Status: ${status}`,
|
|
66
|
+
`Service: ${result.service ?? "Not provided"}`,
|
|
67
|
+
`Impact: ${result.impact ?? "Not provided — verify before claiming an impact"}`,
|
|
68
|
+
`Current action: ${result.action ?? "Not provided"}`,
|
|
69
|
+
`Owner: ${result.owner ?? "Not assigned"}`,
|
|
70
|
+
`Next update: ${result.nextUpdate ?? (status === "resolved" ? "Not scheduled (status marked resolved)" : "Not scheduled")}${result.overdue ? " — OVERDUE" : ""}`,
|
|
71
|
+
];
|
|
72
|
+
if (result.missing.length)
|
|
73
|
+
lines.push("Missing facts: " + result.missing.join(", "));
|
|
74
|
+
result.complete = result.missing.length === 0;
|
|
75
|
+
if (humor) {
|
|
76
|
+
result.comicRelief = excuse(options.category ?? "production", {
|
|
77
|
+
seed: options.seed,
|
|
78
|
+
});
|
|
79
|
+
lines.push("\nComic relief (internal only): " + result.comicRelief);
|
|
80
|
+
}
|
|
81
|
+
result.message = lines.join("\n");
|
|
82
|
+
return result;
|
|
83
|
+
}
|
package/src/index.js
CHANGED
package/src/input.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Bounded UTF-8 input for incident facts, independent of terminal output. */
|
|
2
|
+
export async function readStdin(stream = process.stdin) {
|
|
3
|
+
if (stream.isTTY) throw new TypeError("Pipe incident facts as JSON.");
|
|
4
|
+
let size = 0;
|
|
5
|
+
const chunks = [];
|
|
6
|
+
for await (const chunk of stream) {
|
|
7
|
+
const bytes = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
8
|
+
size += bytes.length;
|
|
9
|
+
if (size > 262144) throw new RangeError("stdin exceeds 256 KiB.");
|
|
10
|
+
chunks.push(bytes);
|
|
11
|
+
}
|
|
12
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
13
|
+
if (!text.trim()) throw new TypeError("Provide incident facts as JSON.");
|
|
14
|
+
return text;
|
|
15
|
+
}
|