@mikitasazan/notify 1.25.0 → 1.26.1

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/dist/render.js CHANGED
@@ -21,14 +21,24 @@
21
21
  */
22
22
  import { DISPLAY, ICON, iconFor } from "./events.js";
23
23
  /**
24
- * A label gets a capital letter — but NOT a name that is deliberately
25
- * written lowercase: `iOS` was turning into `IOS`. The signal is a capital
26
- * second letter.
24
+ * A label gets a capital letter — but NOT a name. Two shapes of a name,
25
+ * both verbatim:
26
+ *
27
+ * 1. An inner-cap spelling a capital would flatten: `iOS` was turning into
28
+ * `IOS`. The signal is a capital second letter.
29
+ * 2. `<name> #N` — a tracked thing named by number. In the tasks digest the
30
+ * name is the repository's own (`zabukai-app #33`), and the one-name rule
31
+ * keeps a repository's name symbol for symbol; this printed
32
+ * «Zabukai-app #33» (owner, 12.09.2026). The ` #` is structural, not a
33
+ * spelling guess — prose labels never carry one.
27
34
  */
28
35
  const cap = (s) => {
29
36
  if (s.length === 0 || /^[a-z][A-Z]/.test(s)) {
30
37
  return s;
31
38
  }
39
+ if (/ #[#\w]/.test(s)) {
40
+ return s;
41
+ }
32
42
  return s.charAt(0).toUpperCase() + s.slice(1);
33
43
  };
34
44
  /** Escapes EVERYTHING that comes from outside — only the template adds tags. */
package/dist/send.d.ts CHANGED
@@ -5,6 +5,21 @@ export declare const dedupe: (e: NotifyEvent, now?: number) => {
5
5
  action: "send" | "suppress";
6
6
  stillRed?: number;
7
7
  };
8
+ /**
9
+ * One slot per OFFENDER — project, type and the card's own instance key —
10
+ * not per type (15.09.2026). Keyed by type, the red about a broken weekly
11
+ * script went green the moment ANY other script of that type sent a clean
12
+ * card, the reactor deleted the red and closed its issue, and next week the
13
+ * same red counted as a first day again: an issue never filed. The slot is
14
+ * both the watchdog card's key and the entry in the red memory below.
15
+ */
16
+ export declare const brokenSlot: (e: NotifyEvent) => string;
17
+ /** Exported for tests: remember that this offender's red is in the feed. */
18
+ export declare const rememberBroken: (slot: string, now?: number) => void;
19
+ /** Exported for tests: true once — the slot was red and is forgotten now. */
20
+ export declare const forgetBroken: (slot: string) => boolean;
21
+ /** Exported for tests: the green that pairs with brokenCardEvent's red — same key, same project. */
22
+ export declare const healedCardEvent: (e: NotifyEvent) => NotifyEvent;
8
23
  /** Exported for tests: the watchdog's own card must provably pass the lint. */
9
24
  export declare const brokenCardEvent: (e: NotifyEvent, faults: string[], offenderHtml: string) => NotifyEvent;
10
25
  export declare const notify: (e: NotifyEvent) => Promise<SendResult>;
package/dist/send.js CHANGED
@@ -412,6 +412,62 @@ const journal = (line) => {
412
412
  // the journal is best-effort: a card must never be lost to bookkeeping
413
413
  }
414
414
  };
415
+ /**
416
+ * One slot per OFFENDER — project, type and the card's own instance key —
417
+ * not per type (15.09.2026). Keyed by type, the red about a broken weekly
418
+ * script went green the moment ANY other script of that type sent a clean
419
+ * card, the reactor deleted the red and closed its issue, and next week the
420
+ * same red counted as a first day again: an issue never filed. The slot is
421
+ * both the watchdog card's key and the entry in the red memory below.
422
+ */
423
+ export const brokenSlot = (e) => `notify-broken-${String(e.type)}-${String(e.project)}-${eventKey(e)}`;
424
+ const brokenStatePath = () => join(dirname(statePath()), 'notify-broken.red.json');
425
+ const readBroken = () => {
426
+ try {
427
+ const parsed = JSON.parse(readFileSync(brokenStatePath(), 'utf8'));
428
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
429
+ }
430
+ catch {
431
+ return {};
432
+ }
433
+ };
434
+ const writeBroken = (state) => {
435
+ try {
436
+ const file = brokenStatePath();
437
+ mkdirSync(dirname(file), { recursive: true });
438
+ const tmp = `${file}.tmp${process.pid}`;
439
+ writeFileSync(tmp, JSON.stringify(state, null, 1));
440
+ renameSync(tmp, file);
441
+ }
442
+ catch {
443
+ // memory is best-effort: a lost entry costs one green, never a card
444
+ }
445
+ };
446
+ /** Exported for tests: remember that this offender's red is in the feed. */
447
+ export const rememberBroken = (slot, now = Date.now()) => {
448
+ const state = readBroken();
449
+ state[slot] = new Date(now).toISOString().slice(0, 10);
450
+ writeBroken(state);
451
+ };
452
+ /** Exported for tests: true once — the slot was red and is forgotten now. */
453
+ export const forgetBroken = (slot) => {
454
+ const state = readBroken();
455
+ if (!Object.hasOwn(state, slot)) {
456
+ return false;
457
+ }
458
+ delete state[slot];
459
+ writeBroken(state);
460
+ return true;
461
+ };
462
+ /** Exported for tests: the green that pairs with brokenCardEvent's red — same key, same project. */
463
+ export const healedCardEvent = (e) => ({
464
+ type: 'job',
465
+ project: 'mac-config',
466
+ job: 'notify: a card broke the standard',
467
+ status: 'ok',
468
+ note: `the ${String(e.type)} card for ${String(e.project)} passes the standard again`,
469
+ key: brokenSlot(e)
470
+ });
415
471
  /** Exported for tests: the watchdog's own card must provably pass the lint. */
416
472
  export const brokenCardEvent = (e, faults, offenderHtml) => {
417
473
  const offender = offenderHtml
@@ -436,7 +492,7 @@ export const brokenCardEvent = (e, faults, offenderHtml) => {
436
492
  detail: offender || undefined,
437
493
  detailLabel: 'Offender',
438
494
  check: 'config jobs --log notify-broken',
439
- key: `notify-broken-${String(e.type)}`
495
+ key: brokenSlot(e)
440
496
  };
441
497
  };
442
498
  const reportBrokenCard = async (e, faults, offenderHtml) => {
@@ -455,6 +511,19 @@ const reportBrokenCard = async (e, faults, offenderHtml) => {
455
511
  journal(`notify-watchdog: own card failed lint: ${ownFaults.join('; ')}`);
456
512
  }
457
513
  await deliver(targets(broken), html).catch(() => undefined);
514
+ rememberBroken(brokenSlot(e));
515
+ };
516
+ /**
517
+ * The green half of the watchdog: the offender's next clean card heals its
518
+ * own red, and only its own. Sent through `deliver` directly, not `notify`,
519
+ * so a green can never be swallowed by the 20h window a red just used.
520
+ */
521
+ const reportHealedCard = async (e) => {
522
+ if (!forgetBroken(brokenSlot(e))) {
523
+ return;
524
+ }
525
+ const healed = healedCardEvent(e);
526
+ await deliver(targets(healed), render(healed)).catch(() => undefined);
458
527
  };
459
528
  export const notify = async (e) => {
460
529
  // Object.hasOwn, not `in`: `in` walks the prototype chain, and
@@ -486,5 +555,8 @@ export const notify = async (e) => {
486
555
  if (faults.length > 0) {
487
556
  await reportBrokenCard(e, faults, html);
488
557
  }
558
+ else {
559
+ await reportHealedCard(e);
560
+ }
489
561
  return result;
490
562
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikitasazan/notify",
3
- "version": "1.25.0",
3
+ "version": "1.26.1",
4
4
  "description": "Единая типизированная отправка Telegram-уведомлений (форум-темы, маршрутизация, ретраи) для всех проектов",
5
5
  "type": "module",
6
6
  "license": "MIT",