@mikitasazan/notify 1.4.5 → 1.6.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/dist/cli.js CHANGED
@@ -348,7 +348,7 @@ else {
348
348
  type: 'report',
349
349
  project: project(),
350
350
  title: one('title') ?? '(no title)',
351
- period: one('note'),
351
+ // A file card has no period; the caption is its title.
352
352
  lines: []
353
353
  };
354
354
  break;
package/dist/index.d.ts CHANGED
@@ -2,4 +2,5 @@ export type { EventType, NotifyEvent, Project } from './events.ts';
2
2
  export { severity } from './events.ts';
3
3
  export { notify, sendReport } from './send.ts';
4
4
  export { render } from './render.ts';
5
+ export { trend } from './trend.ts';
5
6
  export type { SendResult } from './send.ts';
package/dist/index.js CHANGED
@@ -1,6 +1,10 @@
1
1
  export { severity } from "./events.js";
2
2
  export { notify, sendReport } from "./send.js";
3
- // `render` наружу чтобы отправитель мог положить на диск РОВНО ту карточку,
4
- // которая уехала, а не свою вторую версию текста. Сборка копии вручную уже
5
- // расходилась с отправленным.
3
+ // `render` is exported so a sender can put on disk EXACTLY the card that was
4
+ // sent rather than its own second version of the text. A hand-built copy had
5
+ // already drifted from what went out.
6
6
  export { render } from "./render.js";
7
+ // `trend` is exported for the same reason one floor down: the shape of a
8
+ // number is the package's, not each sender's. Two report senders had grown two
9
+ // dialects for one thing.
10
+ export { trend } from "./trend.js";
package/dist/render.js CHANGED
@@ -345,14 +345,21 @@ const twoBlocks = (run, change) => {
345
345
  // report and asked what "open" was — the answer is the report itself, which was
346
346
  // sitting three lines above as dead text. So line 2 takes an optional URL and
347
347
  // the action text becomes the link: `Report: <a>Analytics for 12.08</a>`.
348
- const typeLine = (icon, type, action, url) => {
349
- // `field` возвращает null на пустом значении, а интерполяция null в шаблон
350
- // печатает слово «null». Так вторая строка карточки становилась `ℹ️ null`
351
- // достижимо через `--json` и прямой вызов на JS, где типов нет.
348
+ // `aside` is the one qualifier an identifier needs to be readable on its own —
349
+ // which day a report covers, which day its arrows are measured against. It
350
+ // rides in brackets ON the type line instead of taking a row of its own,
351
+ // because a row of its own reads as another fact about the subject rather than
352
+ // as part of the name.
353
+ const typeLine = (icon, type, action, url, aside) => {
354
+ // `field` returns null on an empty value, and interpolating null into a
355
+ // template prints the word "null". That is how line 2 of a card became
356
+ // `ℹ️ null` — reachable through `--json` and through a direct call from JS,
357
+ // where there are no types.
352
358
  // `action || 'open'` in the linked case: an empty title must not swallow the
353
359
  // link, which would be the one thing the card cannot afford to lose.
354
360
  const line = url ? fieldLink(type, url, action || 'open') : field(type, action);
355
- return line === null ? `${icon} <b>${esc(cap(type))}</b>` : `${icon} ${line}`;
361
+ const tail = aside ? ` (${esc(aside)})` : '';
362
+ return line === null ? `${icon} <b>${esc(cap(type))}</b>${tail}` : `${icon} ${line}${tail}`;
356
363
  };
357
364
  // `workflowUrl ?? url`: половина отправителей шлёт ссылку на прогон под именем
358
365
  // `--url` — это имя было в пакете раньше и осталось в вызовах. Рендер читал
@@ -386,8 +393,10 @@ const renderDeploy = (e) => {
386
393
  const icon = iconFor(e);
387
394
  const runUrl = e.workflowUrl ?? e.url;
388
395
  return join([
389
- typeLine(icon, 'Deploy', e.status),
390
- fieldLink('Via', runUrl, mechanism(e.workflowName, e.via, runUrl)),
396
+ // Имя того, чем выкатили, — на строке типа. Исход говорят значок и третий
397
+ // тег; повторять его словом нечего, и это тот же закон, что у задачи и у
398
+ // отчёта. Строки `Via` больше нет: она несла это имя этажом ниже.
399
+ typeLine(icon, 'Deploy', mechanism(e.workflowName, e.via, runUrl) ?? e.status, runUrl),
391
400
  ...twoBlocks([field('Target', e.target), field('Reason', e.note)], [fieldLink('Commit', e.commitUrl, e.commit), titleField(e.commitTitle), bodyQuote(e.commitBody)])
392
401
  ]);
393
402
  };
@@ -436,22 +445,15 @@ const renderJob = (e) => {
436
445
  const renderReport = (e) => {
437
446
  if (e.groups && e.groups.length > 0) {
438
447
  const body = e.groups.flatMap((g, i) => (i === 0 ? renderGroup(g) : ['', ...renderGroup(g)]));
439
- // `lines` и `groups` вместе, а не «или»: раньше ветка с группами печатала
440
- // ТОЛЬКО группы, и цифры отчёта молча исчезали. Поймано 25.08.2026 при
441
- // переводе утреннего отчёта PlayHub на типизированное событие.
448
+ // `lines` AND `groups` together, not one or the other: the branch with
449
+ // groups used to print ONLY the groups, and the report's numbers vanished
450
+ // without a word.
442
451
  const numbers = labelled(e.lines);
443
452
  return join([
444
- typeLine(iconFor(e), 'Report', e.title, e.url),
445
- // Период стоит ВПЛОТНУЮ к названию, без пустой строки, по тому же
446
- // закону, что `Via` у выкатки и `Check` у проверки: строка, которая
447
- // уточняет вторую строку, живёт рядом с ней, а не в блоке фактов.
448
- // Владелец: «период пошёл не туда, он же должен быть рядом с датой».
449
- field('Period', e.period),
450
- // Строки БЕЗ группы идут вплотную к шапке, а не отдельным куском под
451
- // пустой строкой: это факты про сам отчёт — период, номер, — а не про
452
- // то, о чём он. Владелец: «number отдельно и период отдельно, непонятно,
453
- // куда их относить». `labelled` сам ставит пустую строку перед первой
454
- // группой, поэтому её здесь больше нет.
453
+ typeLine(iconFor(e), 'Report', e.title, e.url, e.period),
454
+ // Rows with no group of their own sit flush against the header instead of
455
+ // forming a separate slab under a blank line. `labelled` puts the blank
456
+ // line before the first group itself, so there is none here.
455
457
  ...numbers,
456
458
  body.length > 0 ? '' : null,
457
459
  ...body
@@ -461,10 +463,8 @@ const renderReport = (e) => {
461
463
  return join([
462
464
  // Both analytics jobs send a link to the day's snapshot in docs/. It used to
463
465
  // hang off a trailing `Details: open` row; now it is the report's own name.
464
- typeLine(iconFor(e), 'Report', e.title, e.url),
465
- // Вплотную к названиюсм. соседнюю ветку.
466
- field('Period', e.period),
467
- // Вплотную к шапке — см. соседнюю ветку.
466
+ typeLine(iconFor(e), 'Report', e.title, e.url, e.period),
467
+ // Flush against the header see the branch above.
468
468
  ...labelled(e.lines),
469
469
  items.length > 0 ? '' : null,
470
470
  ...items
@@ -478,61 +478,54 @@ const renderCi = (e) => {
478
478
  const icon = iconFor(e);
479
479
  const runUrl = e.workflowUrl ?? e.url;
480
480
  return join([
481
- typeLine(icon, 'CI', e.status),
482
- fieldLink('Check', runUrl, mechanism(e.workflowName, undefined, runUrl)),
481
+ typeLine(icon, 'CI', mechanism(e.workflowName, undefined, runUrl) ?? e.status, runUrl),
483
482
  ...twoBlocks([field('Actor', e.actor), field('Reason', e.note)], [fieldLink('Commit', e.commitUrl, e.commit), titleField(e.commitTitle), bodyQuote(e.commitBody)])
484
483
  ]);
485
484
  };
485
+ // A pull request and an issue are identified the way GitHub itself identifies
486
+ // them: `#118 <title>`, one string, and it is the link. It used to take three
487
+ // rows — the action on line 2, `Number:` under it, `Title:` under that — so
488
+ // the thing the card is about could not be read without reading three lines.
489
+ // The action is not repeated in words: the icon carries it, and no two actions
490
+ // of one type share an icon.
491
+ const named = (number, title) => title ? `#${number} ${title}` : `#${number}`;
486
492
  const renderPr = (e) => join([
487
- typeLine(iconFor(e), 'PR', e.action),
488
- '',
489
- // Идентификатор первым, заголовок под ним: так вещь читается «#118, вот
490
- // такая», а не «вот такая, кстати #118» — и так её пишет сам GitHub.
491
- fieldLink('Number', e.url, `#${e.number}`),
492
- titleField(e.title),
493
+ typeLine(iconFor(e), 'PR', named(e.number, e.title), e.url),
493
494
  bodyQuote(e.body),
494
- // Без пустой строки перед автором: у задачи её нет, и одно и то же поле
495
- // не должно стоять по-разному в двух соседних карточках. Пустая строка в
496
- // этом формате означает «дальше указатель, куда пойти» — автор не он.
497
495
  field('Author', e.author),
498
496
  field('Reviewer', e.reviewer)
499
497
  ]);
500
498
  const renderIssue = (e) => join([
501
- typeLine(iconFor(e), 'Issue', e.action),
502
- '',
503
- fieldLink('Number', e.url, `#${e.number}`),
504
- titleField(e.title),
499
+ typeLine(iconFor(e), 'Issue', named(e.number, e.title), e.url),
505
500
  bodyQuote(e.body),
506
501
  field('Author', e.author),
507
502
  field('Assignee', e.assignee)
508
503
  ]);
504
+ // The incident's own title IS line 2, exactly as an issue's is. It used to say
505
+ // the word `open` there — which the 🚨 already says, and no other card repeats
506
+ // its icon in words — with the real title one row below under a `Title:` label.
507
+ //
508
+ // `detail` is a diagnosis of several lines (vault greps three of them plus a
509
+ // log path). It used to go through `field`, which keeps only the first line, so
510
+ // every alarm this package ever sent arrived gutted. It is quoted now, the same
511
+ // shape a commit body takes.
509
512
  const renderIncident = (e) => join([
510
- typeLine(iconFor(e), 'Incident', 'open'),
511
- '',
512
- // `detail` is a diagnosis of several lines (vault greps three of them plus a
513
- // log path). It used to go through `field`, which keeps only the first line,
514
- // so every alarm this package ever sent arrived gutted. Same shape as a
515
- // commit now: short label, full text quoted under it.
516
- // Ярлык `Title`, а не `Reason`: у аварии заголовок — такой же заголовок,
517
- // как у коммита и задачи, и называться в одной карточке он должен так же.
518
- // Same rule as the report: the link rides on the incident's own title
519
- // rather than on a trailing row whose only text is the word `open`.
520
- fieldLink('Title', e.url, e.title),
513
+ typeLine(iconFor(e), 'Incident', e.title, e.url),
521
514
  e.detail && e.detail !== e.title ? note(e.detail) : null,
522
515
  e.logs ? '' : null,
523
516
  fieldCode('Logs', e.logs)
524
517
  ]);
525
- // Раньше всё это склеивалось в одну строку `Reason:` через тире: «имя — no
526
- // reports — expected X, last seen Y». Каждая другая карточка кладёт факт на
527
- // свою строку с ярлыком, и владелец справедливо спросил, зачем тут отдельный
528
- // формат. Отдельного формата больше нет.
529
518
  // A session in trouble. Same law as every other card: identifier first, then
530
519
  // the facts as fields, then his own words as a quote — never as a field, which
531
520
  // keeps one line and clipped the name of the very session the card is about.
521
+ //
522
+ // A session has no name, so line 2 says what happened to it. The 36-character
523
+ // id is not printed: he cannot type it, cannot search it and cannot act on it.
524
+ // It is still in the card — inside the `rm` command at the bottom, which is the
525
+ // one place it is of any use.
532
526
  const renderSession = (e) => join([
533
527
  typeLine(iconFor(e), 'Session', e.action),
534
528
  '',
535
- field('Id', e.id),
536
529
  field('Project', e.workdir),
537
530
  field('Reason', e.reason),
538
531
  e.opened ? '' : null,
@@ -544,9 +537,12 @@ const renderHeartbeatMiss = (e) => {
544
537
  const icon = iconFor(e);
545
538
  const action = e.recovered ? 'ok' : 'miss';
546
539
  return join([
547
- typeLine(icon, 'Heartbeat', action),
548
- '',
549
- field('Task', e.job),
540
+ // The task's name on the type line, exactly as a job card carries it. The
541
+ // `Task:` row said the same thing a floor below. No sender in any
542
+ // repository builds this event any more — the silence watchdog sends an
543
+ // ordinary job with `--status silent` — but a machine still running the old
544
+ // copy of that watchdog can, and the card it gets must obey the template.
545
+ typeLine(icon, 'Heartbeat', e.job, undefined, action),
550
546
  field('Reason', e.note),
551
547
  field('Expected', e.expected),
552
548
  field(e.recovered ? 'Last run' : 'Last seen', e.lastSeen)
@@ -0,0 +1,23 @@
1
+ /**
2
+ * One number, written the one way every card writes a number.
3
+ *
4
+ * Two report senders each grew their own dialect: one printed `485 ▲207` and
5
+ * `0 =`, the other printed `210 +3` for a real comparison and `+37` for a
6
+ * plain count of what happened today — a plus sign in front of a number that
7
+ * was never compared to anything. Inside a single group `Removed: 3` stood
8
+ * next to `Added: +6`, the same kind of fact in two shapes.
9
+ *
10
+ * So the shape is not a sender's business any more. It lives here, one
11
+ * implementation, and every report calls it:
12
+ *
13
+ * trend(210, 207) → '210 ▲3'
14
+ * trend(202, 207) → '202 ▼5'
15
+ * trend(0, 0) → '0 ='
16
+ * trend(37) → '37' nothing to compare to, so no mark
17
+ * trend(4.4, 3.6, '%') → '4.4% ▲0.8' the mark is in the unit left of it
18
+ *
19
+ * The rule the owner asked for, in one line: where there is data to compare
20
+ * against, the arrow is printed; where there is none, nothing is printed —
21
+ * never a sign that only looks like a comparison.
22
+ */
23
+ export declare const trend: (now: number, was?: number, unit?: string) => string;
package/dist/trend.js ADDED
@@ -0,0 +1,39 @@
1
+ /**
2
+ * One number, written the one way every card writes a number.
3
+ *
4
+ * Two report senders each grew their own dialect: one printed `485 ▲207` and
5
+ * `0 =`, the other printed `210 +3` for a real comparison and `+37` for a
6
+ * plain count of what happened today — a plus sign in front of a number that
7
+ * was never compared to anything. Inside a single group `Removed: 3` stood
8
+ * next to `Added: +6`, the same kind of fact in two shapes.
9
+ *
10
+ * So the shape is not a sender's business any more. It lives here, one
11
+ * implementation, and every report calls it:
12
+ *
13
+ * trend(210, 207) → '210 ▲3'
14
+ * trend(202, 207) → '202 ▼5'
15
+ * trend(0, 0) → '0 ='
16
+ * trend(37) → '37' nothing to compare to, so no mark
17
+ * trend(4.4, 3.6, '%') → '4.4% ▲0.8' the mark is in the unit left of it
18
+ *
19
+ * The rule the owner asked for, in one line: where there is data to compare
20
+ * against, the arrow is printed; where there is none, nothing is printed —
21
+ * never a sign that only looks like a comparison.
22
+ */
23
+ /** Integers stay integers; anything else keeps one decimal. */
24
+ const fmt = (n) => (Number.isInteger(n) ? String(n) : n.toFixed(1));
25
+ /**
26
+ * Two values that round to the same first decimal are equal: `4.42%` against
27
+ * `4.44%` is not movement, it is noise, and `▲0.0` reads as a lie.
28
+ */
29
+ const same = (a, b) => Math.abs(a - b) < 0.05;
30
+ export const trend = (now, was, unit = '') => {
31
+ const head = `${fmt(now)}${unit}`;
32
+ if (was === undefined || !Number.isFinite(was)) {
33
+ return head;
34
+ }
35
+ if (same(now, was)) {
36
+ return `${head} =`;
37
+ }
38
+ return now > was ? `${head} ▲${fmt(now - was)}` : `${head} ▼${fmt(was - now)}`;
39
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikitasazan/notify",
3
- "version": "1.4.5",
3
+ "version": "1.6.0",
4
4
  "description": "Единая типизированная отправка Telegram-уведомлений (форум-темы, маршрутизация, ретраи) для всех проектов",
5
5
  "type": "module",
6
6
  "license": "MIT",