@mikitasazan/notify 1.11.0 → 1.12.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/README.md +17 -0
- package/dist/cli-flags.js +2 -1
- package/dist/cli.js +44 -6
- package/dist/events.d.ts +74 -17
- package/dist/lint.js +56 -15
- package/dist/render.d.ts +8 -5
- package/dist/render.js +249 -90
- package/dist/send.d.ts +7 -0
- package/dist/send.js +165 -10
- package/dist/trend.d.ts +9 -6
- package/dist/trend.js +21 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -158,6 +158,23 @@ notify report --project playhub --json < payload.json # весь объект
|
|
|
158
158
|
никогда, только новый тип события**. Тогда старый вызывающий код и новый
|
|
159
159
|
пакет всегда совместимы.
|
|
160
160
|
|
|
161
|
+
## Тег сменился 18.08.2026 у шести отправителей
|
|
162
|
+
|
|
163
|
+
Коммит `85564fc` в mac-config дал явный `--key` шести местам вызова, которые
|
|
164
|
+
раньше отправляли карточку БЕЗ ключа — тег брался из `--job`/`--title`
|
|
165
|
+
автоматически (`slug()`, см. `src/render.ts`). Без этой таблицы старую
|
|
166
|
+
красную карточку в Telegram не найти поиском по новому тегу — она там под
|
|
167
|
+
старым.
|
|
168
|
+
|
|
169
|
+
| Отправитель | Старый тег (авто из русского названия) | Новый тег (из `--key`) |
|
|
170
|
+
|---|---|---|
|
|
171
|
+
| `arvent-eval-report.sh`, отчёт по эвалу | `#eval_качество_ответов_бота` | `#arvent_eval` |
|
|
172
|
+
| `arvent-eval-report.sh`, файл диалогов | нет — шёл мимо пакета, сырым `sendDocument` Bot API | `#arvent_eval_dialogues` |
|
|
173
|
+
| `daily-digest.sh`, дайджест задач (`job`) | `#дайджест_задач` | `#daily_digest` |
|
|
174
|
+
| `daily-digest.sh`, дайджест задач (`report`) | `#дайджест_задач` (то же слово, другой тип-тег: `#report` вместо `#job`) | `#daily_digest` |
|
|
175
|
+
| `daily-digest.sh`, отключённые Actions | `#github_actions_выключены` | `#actions_off` |
|
|
176
|
+
| `daily-digest.sh`, Config doctor | `#config_doctor` | `#config_doctor` — не изменился: `slug()` приводит и старое, и новое к одной строке |
|
|
177
|
+
|
|
161
178
|
## Чего в пакете нет и почему
|
|
162
179
|
|
|
163
180
|
- Очереди, брокера, демона — десятки сообщений в день, ретрай в памяти
|
package/dist/cli-flags.js
CHANGED
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
export const KNOWN_FLAGS = new Set([
|
|
12
12
|
'action', 'actor', 'assignee', 'author', 'body', 'branch', 'commit',
|
|
13
13
|
'id', 'opened', 'reason', 'workdir',
|
|
14
|
-
'command', 'command-note', 'commit-
|
|
14
|
+
'check', 'command', 'command-note', 'commit-author', 'commit-body', 'commit-title', 'commit-url',
|
|
15
|
+
'detail', 'detail-label',
|
|
15
16
|
'expected', 'filename', 'item',
|
|
16
17
|
'item-group', 'job', 'key', 'last-seen', 'line', 'logs', 'note',
|
|
17
18
|
'aside', 'number', 'path', 'period', 'project', 'reviewer', 'stat', 'status',
|
package/dist/cli.js
CHANGED
|
@@ -24,7 +24,9 @@
|
|
|
24
24
|
import { readFileSync } from 'node:fs';
|
|
25
25
|
import { KNOWN_FLAGS } from "./cli-flags.js";
|
|
26
26
|
import { render } from "./render.js";
|
|
27
|
+
import { lintCard } from "./lint.js";
|
|
27
28
|
import { notify } from "./send.js";
|
|
29
|
+
import { ROUTES } from "./routes.js";
|
|
28
30
|
import { setupTopic } from "./setup.js";
|
|
29
31
|
const log = (msg) => console.error(`[notify] ${msg}`);
|
|
30
32
|
/**
|
|
@@ -41,6 +43,23 @@ const log = (msg) => console.error(`[notify] ${msg}`);
|
|
|
41
43
|
const safe = (v) => String(v ?? '').replace(/\b(sent|failed|skipped)\b/gi, (w) => `${w[0]}·${w.slice(1)}`);
|
|
42
44
|
const args = process.argv.slice(2);
|
|
43
45
|
const command = args[0];
|
|
46
|
+
// `lint-text` and `routes` exist so the nightly audit asks the PACKAGE
|
|
47
|
+
// instead of keeping its own copy of the rules and the routing table — the
|
|
48
|
+
// copies had already drifted twice.
|
|
49
|
+
if (command === 'lint-text') {
|
|
50
|
+
// The finished card HTML on stdin; every fault on stdout, one per line.
|
|
51
|
+
// Empty output means the card obeys the standard. Exit code stays 0 —
|
|
52
|
+
// the CLI-wide contract.
|
|
53
|
+
const text = readFileSync(0, 'utf-8').replace(/\n$/, '');
|
|
54
|
+
for (const fault of lintCard(text)) {
|
|
55
|
+
process.stdout.write(`${fault}\n`);
|
|
56
|
+
}
|
|
57
|
+
process.exit(0);
|
|
58
|
+
}
|
|
59
|
+
if (command === 'routes') {
|
|
60
|
+
process.stdout.write(`${JSON.stringify(ROUTES, null, 2)}\n`);
|
|
61
|
+
process.exit(0);
|
|
62
|
+
}
|
|
44
63
|
if (command === 'setup') {
|
|
45
64
|
const [, chatId, projectKey] = args;
|
|
46
65
|
if (!chatId || !projectKey) {
|
|
@@ -103,7 +122,13 @@ const num = (key) => {
|
|
|
103
122
|
return n;
|
|
104
123
|
};
|
|
105
124
|
/**
|
|
106
|
-
* `--item "text"
|
|
125
|
+
* `--item "text"`, `--item "text|https://link"`, or, since 26.08.2026,
|
|
126
|
+
* `--item "LABEL::text"` (and `LABEL::text|https://link`) for a bold label
|
|
127
|
+
* in front of the row — the same shape a search query's `facts` render,
|
|
128
|
+
* so a list of independent findings (vault's BAD/STALE/DIVERGED lines) does
|
|
129
|
+
* not have to smuggle its own heading inside the text. `::` and not `|`,
|
|
130
|
+
* which the link already owns, and not `:`, which shows up inside real
|
|
131
|
+
* findings ("STALE IN ARCHIVE: ssh-keys.tar.gz.age" already has one).
|
|
107
132
|
*
|
|
108
133
|
* An item's group name cannot be passed the same way `--stat` does it: the
|
|
109
134
|
* bar here is already taken by the link. So `--item-group "Red checks"` is
|
|
@@ -114,9 +139,12 @@ const num = (key) => {
|
|
|
114
139
|
const items = () => {
|
|
115
140
|
const name = flags.get('item-group')?.[0];
|
|
116
141
|
return (flags.get('item') ?? []).map((raw) => {
|
|
117
|
-
const
|
|
118
|
-
const
|
|
119
|
-
|
|
142
|
+
const labelEnd = raw.indexOf('::');
|
|
143
|
+
const label = labelEnd === -1 ? undefined : raw.slice(0, labelEnd);
|
|
144
|
+
const rest = labelEnd === -1 ? raw : raw.slice(labelEnd + 2);
|
|
145
|
+
const idx = rest.lastIndexOf('|');
|
|
146
|
+
const base = idx === -1 ? { text: rest } : { text: rest.slice(0, idx), url: rest.slice(idx + 1) };
|
|
147
|
+
return { ...base, ...(label ? { label } : {}), ...(name ? { group: name } : {}) };
|
|
120
148
|
});
|
|
121
149
|
};
|
|
122
150
|
/**
|
|
@@ -228,6 +256,7 @@ else {
|
|
|
228
256
|
commitUrl: one('commit-url'),
|
|
229
257
|
commitTitle: one('commit-title'),
|
|
230
258
|
commitBody: one('commit-body'),
|
|
259
|
+
commitAuthor: one('commit-author'),
|
|
231
260
|
workflowUrl: one('workflow-url'),
|
|
232
261
|
workflowName: one('workflow-name'),
|
|
233
262
|
url: one('url'),
|
|
@@ -251,6 +280,8 @@ else {
|
|
|
251
280
|
command: one('command'),
|
|
252
281
|
commandNote: one('command-note'),
|
|
253
282
|
logs: one('logs'),
|
|
283
|
+
detail: one('detail'),
|
|
284
|
+
detailLabel: one('detail-label'),
|
|
254
285
|
workflowUrl: one('workflow-url'),
|
|
255
286
|
workflowName: one('workflow-name'),
|
|
256
287
|
url: one('url')
|
|
@@ -279,6 +310,7 @@ else {
|
|
|
279
310
|
commitUrl: one('commit-url'),
|
|
280
311
|
commitTitle: one('commit-title'),
|
|
281
312
|
commitBody: one('commit-body'),
|
|
313
|
+
commitAuthor: one('commit-author'),
|
|
282
314
|
actor: one('actor'),
|
|
283
315
|
note: one('note'),
|
|
284
316
|
workflowUrl: one('workflow-url'),
|
|
@@ -333,6 +365,7 @@ else {
|
|
|
333
365
|
project: project(),
|
|
334
366
|
title: one('title') ?? '(no title)',
|
|
335
367
|
detail: one('detail'),
|
|
368
|
+
items: items(),
|
|
336
369
|
logs: one('logs'),
|
|
337
370
|
url: one('url')
|
|
338
371
|
};
|
|
@@ -357,8 +390,10 @@ else {
|
|
|
357
390
|
type: 'report',
|
|
358
391
|
project: project(),
|
|
359
392
|
title: one('title') ?? '(no title)',
|
|
360
|
-
|
|
361
|
-
lines:
|
|
393
|
+
aside: one('aside') ?? one('period'),
|
|
394
|
+
lines: pairs('line'),
|
|
395
|
+
items: items(),
|
|
396
|
+
url: one('url')
|
|
362
397
|
};
|
|
363
398
|
break;
|
|
364
399
|
default:
|
|
@@ -376,6 +411,9 @@ if (event) {
|
|
|
376
411
|
// --path is applicable to any type too, for the same reason --key is.
|
|
377
412
|
event.path = one('path') ?? event.path;
|
|
378
413
|
event.filename = one('filename') ?? event.filename;
|
|
414
|
+
// --check applies to any type (rule S): the verification command for a
|
|
415
|
+
// card whose event has no canonical URL.
|
|
416
|
+
event.check = one('check') ?? event.check;
|
|
379
417
|
}
|
|
380
418
|
if (event?.filename && !event.path) {
|
|
381
419
|
parseErrors.push('--filename: given without --path, so there is no file to name');
|
package/dist/events.d.ts
CHANGED
|
@@ -12,9 +12,10 @@
|
|
|
12
12
|
*/
|
|
13
13
|
export type Project = 'playhub' | 'one-q' | 'arvent' | 'game-publisher' | 'vault' | 'mac-config' | 'alitools';
|
|
14
14
|
/**
|
|
15
|
-
* A stable machine key for the task — the
|
|
16
|
-
* as `#key` (with no project name: the card already sits
|
|
17
|
-
* project's forum — `targets()` never sends it to someone else's).
|
|
15
|
+
* A stable machine key for the task — the instance tag on the FIRST line of
|
|
16
|
+
* every card, shaped as `#key` (with no project name: the card already sits
|
|
17
|
+
* in its own project's forum — `targets()` never sends it to someone else's).
|
|
18
|
+
* The
|
|
18
19
|
* daily parser uses it to check "is this 🔴 already closed by a later card
|
|
19
20
|
* with the same key?" without comparing human wording, which changes. It
|
|
20
21
|
* is optional: without it, the key is derived from the type and the title
|
|
@@ -36,6 +37,18 @@ type Keyed = {
|
|
|
36
37
|
path?: string;
|
|
37
38
|
/** The file's name in the chat; defaults to the name from `path`. */
|
|
38
39
|
filename?: string;
|
|
40
|
+
/**
|
|
41
|
+
* The verification command — `Check:` in the card's last block, monospaced
|
|
42
|
+
* and tap-to-copy. The standard (v2.1, rule S) wants every card to say
|
|
43
|
+
* where to verify it: a `Source:` link when the event has a canonical URL,
|
|
44
|
+
* this command when the event is local. Usually `config jobs --log <key>`.
|
|
45
|
+
*/
|
|
46
|
+
check?: string;
|
|
47
|
+
/**
|
|
48
|
+
* Set by the send-side suppression, never by a caller: which day of the
|
|
49
|
+
* same unresolved failure this is. Renders as `Still red: day N`.
|
|
50
|
+
*/
|
|
51
|
+
stillRed?: number;
|
|
39
52
|
};
|
|
40
53
|
/**
|
|
41
54
|
* A list item inside a message: a task from a digest, a failed check, a
|
|
@@ -57,11 +70,19 @@ type Keyed = {
|
|
|
57
70
|
* 🆕 came out today, 🔁 came out of the queue, ⚠ did not come out at all.
|
|
58
71
|
* The icon was doing a heading's job.
|
|
59
72
|
*/
|
|
73
|
+
/**
|
|
74
|
+
* `facts` — sub-rows under an item, indented, each `label: value`. For an
|
|
75
|
+
* item whose own value is not one number but several (a search query with
|
|
76
|
+
* its own clicks AND position) — a nested list, not a sentence stuffed into
|
|
77
|
+
* `text`: "0 clicks, pos. 55" was two facts hand-joined into a string, the
|
|
78
|
+
* exact shape that gets pulled apart into its own field everywhere else.
|
|
79
|
+
*/
|
|
60
80
|
export type Item = {
|
|
61
81
|
text: string;
|
|
62
82
|
url?: string;
|
|
63
83
|
label?: string;
|
|
64
84
|
group?: string;
|
|
85
|
+
facts?: Array<[label: string, value: string | number]>;
|
|
65
86
|
};
|
|
66
87
|
export type NotifyEvent = Keyed & (
|
|
67
88
|
/** Shipping code to the server. */
|
|
@@ -72,12 +93,14 @@ export type NotifyEvent = Keyed & (
|
|
|
72
93
|
commit?: string;
|
|
73
94
|
/** A link to the commit — the "commit" row becomes clickable. */
|
|
74
95
|
commitUrl?: string;
|
|
75
|
-
/** The commit's title —
|
|
96
|
+
/** The commit's title — joins the `Commit:` row after the hash, the body follows as a quote. */
|
|
76
97
|
commitTitle?: string;
|
|
77
98
|
/** The commit's body, if there is one — the same quote shape as the title. */
|
|
78
99
|
commitBody?: string;
|
|
100
|
+
/** The commit's author (GitHub login) — the `Author:` row links to their profile. */
|
|
101
|
+
commitAuthor?: string;
|
|
79
102
|
workflowUrl?: string;
|
|
80
|
-
/** The run's name, for the link's visible text
|
|
103
|
+
/** The run's name, for the link's visible text. Without it, the type line itself becomes the link. */
|
|
81
104
|
workflowName?: string;
|
|
82
105
|
url?: string;
|
|
83
106
|
/**
|
|
@@ -161,10 +184,22 @@ export type NotifyEvent = Keyed & (
|
|
|
161
184
|
* A local log path — monospaced, not a link, same as on an incident.
|
|
162
185
|
* It used to be glued onto the end of the reason sentence behind a
|
|
163
186
|
* colon, which is what made a red card read as one long run-on line.
|
|
187
|
+
* Under rule S it is an ADDITION to `check`/a link, never the card's
|
|
188
|
+
* only pointer — a path cannot be tapped, only copied.
|
|
164
189
|
*/
|
|
165
190
|
logs?: string;
|
|
191
|
+
/**
|
|
192
|
+
* A multi-line quoted block with its own caption (`detailLabel`) — the
|
|
193
|
+
* shape the watchdog uses to show the offending card's first lines
|
|
194
|
+
* under `Offender:`. Generic on purpose: any job with a verbatim
|
|
195
|
+
* excerpt to show (someone else's text, not the sender's own words)
|
|
196
|
+
* uses this instead of stuffing it into `note`.
|
|
197
|
+
*/
|
|
198
|
+
detail?: string;
|
|
199
|
+
/** The caption over `detail`; defaults to `Detail`. */
|
|
200
|
+
detailLabel?: string;
|
|
166
201
|
workflowUrl?: string;
|
|
167
|
-
/** The run's name, for the link's visible text
|
|
202
|
+
/** The run's name, for the link's visible text. Without it, the type line itself becomes the link. */
|
|
168
203
|
workflowName?: string;
|
|
169
204
|
/**
|
|
170
205
|
* A fallback name for the run link: half the senders send it as
|
|
@@ -185,19 +220,25 @@ export type NotifyEvent = Keyed & (
|
|
|
185
220
|
* accepts `--period` for it.
|
|
186
221
|
*/
|
|
187
222
|
aside?: string;
|
|
188
|
-
/**
|
|
223
|
+
/**
|
|
224
|
+
* Renders even when `groups` is also set — a report's headline numbers
|
|
225
|
+
* (Pages, People, Impressions…) sit above the grouped section, not
|
|
226
|
+
* replaced by it. `lines` and `groups` answer different questions:
|
|
227
|
+
* "what changed" and "what's in each list".
|
|
228
|
+
*/
|
|
189
229
|
lines?: Array<[label: string, value: string | number, group?: string]>;
|
|
190
230
|
/**
|
|
191
231
|
* A list of items with links — for task digests, where the value is
|
|
192
232
|
* in the names themselves, not in a number. Renders as a separate
|
|
193
|
-
* block after `lines`.
|
|
233
|
+
* block after `lines`. Ignored (not merged, not an error) when `groups`
|
|
234
|
+
* is also set — no live sender sets both today.
|
|
194
235
|
*/
|
|
195
236
|
items?: Item[];
|
|
196
237
|
/**
|
|
197
238
|
* Named groups (a task board: Ready/In Progress/Not on the board;
|
|
198
|
-
* analytics:
|
|
199
|
-
* items. Replaces `
|
|
200
|
-
*
|
|
239
|
+
* analytics: Top search queries) — each with its own heading and list
|
|
240
|
+
* of items. Replaces `items` when set, but `lines` still renders above
|
|
241
|
+
* it — see that field's own doc.
|
|
201
242
|
*/
|
|
202
243
|
groups?: Array<{
|
|
203
244
|
name: string;
|
|
@@ -214,10 +255,12 @@ export type NotifyEvent = Keyed & (
|
|
|
214
255
|
commit?: string;
|
|
215
256
|
/** A link to the commit — the hash becomes clickable. */
|
|
216
257
|
commitUrl?: string;
|
|
217
|
-
/** The commit's title (subject) —
|
|
258
|
+
/** The commit's title (subject) — joins the `Commit:` row after the hash, not a quote. */
|
|
218
259
|
commitTitle?: string;
|
|
219
260
|
/** The commit's body (after the subject) — the same quote shape as the title. */
|
|
220
261
|
commitBody?: string;
|
|
262
|
+
/** The commit's author (GitHub login) — the `Author:` row links to their profile. Distinct from `actor`: on a scheduled run `actor` is whoever is on duty to fix it, not who wrote the code. */
|
|
263
|
+
commitAuthor?: string;
|
|
221
264
|
actor?: string;
|
|
222
265
|
/**
|
|
223
266
|
* Why this run happened, when there is no commit to point at: a nightly
|
|
@@ -226,7 +269,7 @@ export type NotifyEvent = Keyed & (
|
|
|
226
269
|
note?: string;
|
|
227
270
|
/** A link to the run (workflow run) — separate from `url`, a fallback for `workflowUrl`. */
|
|
228
271
|
workflowUrl?: string;
|
|
229
|
-
/** The run's name, for the link's visible text
|
|
272
|
+
/** The run's name, for the link's visible text. Without it, the type line itself becomes the link. */
|
|
230
273
|
workflowName?: string;
|
|
231
274
|
url?: string;
|
|
232
275
|
}
|
|
@@ -242,7 +285,12 @@ export type NotifyEvent = Keyed & (
|
|
|
242
285
|
action: 'opened' | 'approved' | 'changes_requested' | 'merged' | 'closed';
|
|
243
286
|
number: number;
|
|
244
287
|
title: string;
|
|
245
|
-
/**
|
|
288
|
+
/**
|
|
289
|
+
* On `opened`: the PR's own description. On `approved`/
|
|
290
|
+
* `changes_requested`: the reviewer's comment, not the PR's
|
|
291
|
+
* description again — quoted on its own either way; the title joins
|
|
292
|
+
* the `PR:` type line above it, there is no separate field for it.
|
|
293
|
+
*/
|
|
246
294
|
body?: string;
|
|
247
295
|
author?: string;
|
|
248
296
|
reviewer?: string;
|
|
@@ -255,7 +303,7 @@ export type NotifyEvent = Keyed & (
|
|
|
255
303
|
action: 'opened' | 'assigned' | 'closed';
|
|
256
304
|
number: number;
|
|
257
305
|
title: string;
|
|
258
|
-
/** The issue's body — a quote under the `
|
|
306
|
+
/** The issue's body — a quote under the `Issue:` type line, which already carries the title. */
|
|
259
307
|
body?: string;
|
|
260
308
|
author?: string;
|
|
261
309
|
assignee?: string;
|
|
@@ -266,7 +314,16 @@ export type NotifyEvent = Keyed & (
|
|
|
266
314
|
type: 'incident';
|
|
267
315
|
project: Project;
|
|
268
316
|
title: string;
|
|
317
|
+
/** One free-form paragraph — a diagnosis that is genuinely one thought, not several findings glued by newlines. */
|
|
269
318
|
detail?: string;
|
|
319
|
+
/**
|
|
320
|
+
* Several INDEPENDENT findings (a self-check emitting up to three
|
|
321
|
+
* unrelated diagnostic lines, each starting with its own marker word
|
|
322
|
+
* — `BAD`, `STALE`, `DIVERGED`) go here, not into `detail`: a list
|
|
323
|
+
* squeezed into one blockquote read as a wall of text with no
|
|
324
|
+
* category, the marker words doing a label's job inside a value.
|
|
325
|
+
*/
|
|
326
|
+
items?: Item[];
|
|
270
327
|
/** A local path to the logs (not a URL — renders monospaced, to copy, not to click). */
|
|
271
328
|
logs?: string;
|
|
272
329
|
url?: string;
|
|
@@ -287,7 +344,7 @@ export type NotifyEvent = Keyed & (
|
|
|
287
344
|
project: Project;
|
|
288
345
|
/** What happened, as the second line reads it: `Session: burning the limit`. */
|
|
289
346
|
action: string;
|
|
290
|
-
/** The session's own id — the
|
|
347
|
+
/** The session's own id. Never printed as a field — he cannot type it or search it; it only reaches the card inside the `command`'s `rm`. */
|
|
291
348
|
id?: string;
|
|
292
349
|
/** Working directory name, when several sessions opened with a similar line. */
|
|
293
350
|
workdir?: string;
|
|
@@ -320,7 +377,7 @@ export type NotifyEvent = Keyed & (
|
|
|
320
377
|
expected?: string;
|
|
321
378
|
/** The task reported in again — same type, a green card instead of a red one, the key (for matching) does not change. */
|
|
322
379
|
recovered?: boolean;
|
|
323
|
-
/** A ready-made reason sentence
|
|
380
|
+
/** A ready-made reason sentence. Without it there is no `Reason:` row at all — `lastSeen`/`expected` still print, under their own `Schedule` group. */
|
|
324
381
|
note?: string;
|
|
325
382
|
});
|
|
326
383
|
export type EventType = NotifyEvent['type'];
|
package/dist/lint.js
CHANGED
|
@@ -32,8 +32,12 @@ const NOT_A_NAME = new Set([
|
|
|
32
32
|
]);
|
|
33
33
|
/** The five words the third tag is allowed to be, and there is no sixth. */
|
|
34
34
|
const OUTCOMES = new Set(['ok', 'fail', 'off', 'unknown', 'info']);
|
|
35
|
-
/**
|
|
36
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Labels the template retired. Each one used to say what a neighbour said.
|
|
37
|
+
* `Check` LEFT this list in v2.1: rule S brought it back as the standard
|
|
38
|
+
* verification-command row. `Logs` stays retired — the new spelling is `Log`.
|
|
39
|
+
*/
|
|
40
|
+
const RETIRED = ['Title', 'Number', 'State', 'Via', 'Logs', 'Task', 'Id', 'Period'];
|
|
37
41
|
/**
|
|
38
42
|
* Reads a finished card and returns what is wrong with it, in the owner's
|
|
39
43
|
* terms. An empty array means the card obeys the standard.
|
|
@@ -45,8 +49,23 @@ export const lintCard = (html) => {
|
|
|
45
49
|
if (tags.length !== 3 || !tags.every((t) => t.startsWith('#'))) {
|
|
46
50
|
found.push(`line 1 is "${rows[0] ?? ''}" — it must be exactly three tags`);
|
|
47
51
|
}
|
|
48
|
-
else
|
|
49
|
-
|
|
52
|
+
else {
|
|
53
|
+
if (!OUTCOMES.has(tags[2].slice(1))) {
|
|
54
|
+
found.push(`the outcome tag is "${tags[2]}" — the vocabulary is ok, fail, off, unknown, info`);
|
|
55
|
+
}
|
|
56
|
+
// The charset is checked HERE, not trusted to slug(): slug keeps any
|
|
57
|
+
// Unicode letter, so a Russian-named job produces a Cyrillic tag with no
|
|
58
|
+
// complaint anywhere — confirmed against render.ts on 31.08.2026.
|
|
59
|
+
for (const t of tags) {
|
|
60
|
+
if (!/^#[a-z0-9_]+$/.test(t)) {
|
|
61
|
+
found.push(`the tag "${t}" carries characters outside [a-z0-9_] — tags are English, lowercase`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// A dated tag groups nothing: every day mints a new one and the filter
|
|
65
|
+
// the tags exist for never collects two cards.
|
|
66
|
+
if (/(_|^#)20\d{2}_\d{2}_\d{2}$/.test(tags[1]) || /_20\d{6}$/.test(tags[1])) {
|
|
67
|
+
found.push(`the instance tag "${tags[1]}" ends in a date — a dated tag groups nothing`);
|
|
68
|
+
}
|
|
50
69
|
}
|
|
51
70
|
if (tags[1] === '#' || tags[1] === '#_') {
|
|
52
71
|
found.push('the instance tag is empty — it groups nothing and pairs with nothing');
|
|
@@ -98,17 +117,39 @@ export const lintCard = (html) => {
|
|
|
98
117
|
break;
|
|
99
118
|
}
|
|
100
119
|
}
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
120
|
+
// Rule L (v2.1): everything the SYSTEM says is English. Cyrillic is
|
|
121
|
+
// allowed only as QUOTED CONTENT — text that exists in Russian outside the
|
|
122
|
+
// card: a blockquote (commit bodies, issue bodies, an offender's lines),
|
|
123
|
+
// the text of a link (issue titles in digests), and the title slot on
|
|
124
|
+
// line 2. Everywhere else — a label, a bare field value, a tag, a command —
|
|
125
|
+
// it is system text and a fault.
|
|
126
|
+
const quotedStripped = html
|
|
127
|
+
.replace(/<blockquote[^>]*>[\s\S]*?<\/blockquote>/g, '')
|
|
128
|
+
.replace(/<a href="[^"]*">[^<]*<\/a>/g, '')
|
|
129
|
+
// A commit's title rides the `Commit:` row after the hash — it is a
|
|
130
|
+
// commit message, the canonical quoted content.
|
|
131
|
+
.replace(/^<b>Commit:<\/b>.*$/gm, '')
|
|
132
|
+
// List items carry content (issue titles in a digest, findings) — the
|
|
133
|
+
// row's own text is the subject's, not the system's.
|
|
134
|
+
.replace(/^(?:•|\d+\.) .*$/gm, '');
|
|
135
|
+
const strippedRows = quotedStripped.split('\n');
|
|
136
|
+
// Line 2's value after the label is the one non-quoted slot allowed to
|
|
137
|
+
// carry a title written in Russian (issue/PR/report/incident titles).
|
|
138
|
+
if (strippedRows[1]) {
|
|
139
|
+
strippedRows[1] = strippedRows[1].replace(/(<b>[^<]+:<\/b>|<b>[^<]+<\/b>).*/, '$1');
|
|
140
|
+
}
|
|
141
|
+
if (/[а-яё]/i.test(strippedRows.join('\n'))) {
|
|
142
|
+
found.push('Cyrillic outside quoted content — system text is English (rule L)');
|
|
143
|
+
}
|
|
144
|
+
// Rule S (v2.1): a card that reports trouble must say where to verify it —
|
|
145
|
+
// a `Check:` command, a `Source:` link, a `To do:` command, or any link at
|
|
146
|
+
// all. A `Log:` path alone is not enough: a path cannot be tapped, and a
|
|
147
|
+
// card whose only pointer needs a file manager is a card with no pointer.
|
|
148
|
+
// `#fail` and `#unknown` both: a silent task has no log, but `config jobs
|
|
149
|
+
// --log <key>` answers it too.
|
|
150
|
+
const broke = (rows[0] ?? '').includes('#fail') || (rows[0] ?? '').includes('#unknown');
|
|
151
|
+
if (broke && !html.includes('<b>Check:</b>') && !html.includes('<b>To do:</b>') && !html.includes('<a href=')) {
|
|
152
|
+
found.push('a trouble card with no check command and no link — nowhere to look (rule S)');
|
|
112
153
|
}
|
|
113
154
|
return found;
|
|
114
155
|
};
|
package/dist/render.d.ts
CHANGED
|
@@ -36,7 +36,7 @@ export declare const esc: (v: unknown) => string;
|
|
|
36
36
|
* Telegram replies `400 can't parse entities`, and we treat a 4xx as a
|
|
37
37
|
* permanent error and do not retry — the message disappeared for good.
|
|
38
38
|
*/
|
|
39
|
-
export declare const clampMessage: (text: string, limit?: number) => string;
|
|
39
|
+
export declare const clampMessage: (text: string, limit?: number, marker?: string) => string;
|
|
40
40
|
export declare const slug: (raw: string) => string;
|
|
41
41
|
/**
|
|
42
42
|
* The instance tag: exactly which concrete event this is (branch,
|
|
@@ -81,10 +81,13 @@ export declare const OUTCOME_TAG: Readonly<Record<Icon, OutcomeTag>>;
|
|
|
81
81
|
export declare const outcomeTag: (e: NotifyEvent) => OutcomeTag;
|
|
82
82
|
/**
|
|
83
83
|
* Renders an event into finished HTML text, cut to Telegram's limit.
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
84
|
+
*
|
|
85
|
+
* Assembly is TAIL-FIRST (v2.1): the parts that must survive any cut — the
|
|
86
|
+
* tag line (the human filter and the parser's machine key), the pointer
|
|
87
|
+
* block (`Log`/`Check`/`Source`) and the cut marker — are measured before
|
|
88
|
+
* the body is clamped, and the body gets what is left. Under the old order
|
|
89
|
+
* the pointer was part of the body, so the longest cards lost exactly the
|
|
90
|
+
* line saying where to look.
|
|
88
91
|
*/
|
|
89
92
|
export declare const render: (e: NotifyEvent) => string;
|
|
90
93
|
export {};
|