@jterrazz/typescript 9.2.0 → 9.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jterrazz/typescript",
3
- "version": "9.2.0",
3
+ "version": "9.3.0",
4
4
  "author": "Jean-Baptiste Terrazzoni <contact@jterrazz.com>",
5
5
  "repository": {
6
6
  "type": "git",
package/src/docs.js CHANGED
@@ -48,8 +48,14 @@ const JOURNAL_WORDS = new Set([
48
48
  /** The closed status vocabulary of a decision record. */
49
49
  const STATUSES = new Set(['Proposed', 'Accepted', 'Deprecated']);
50
50
 
51
- /** The fourth status carries the record that replaced it. */
52
- const SUPERSEDED = /^Superseded by ADR-\d{3}$/;
51
+ /**
52
+ * The fourth status carries the record that replaced it, as a link a reader
53
+ * can follow — a citation is a place a human can look, not just a number.
54
+ */
55
+ const SUPERSEDED = /^Superseded by \[ADR-\d{3}\]\([^)]+\)$/;
56
+
57
+ /** The same status named but not linked — the successor exists, the citation does not. */
58
+ const BARE_SUPERSEDED = /^Superseded by ADR-\d{3}$/;
53
59
 
54
60
  /** What a chapter's file name must be: two digits, lowercase words, single hyphens. */
55
61
  const CHAPTER_NAME = /^\d{2}-[a-z\d]+(?:-[a-z\d]+)*\.md$/;
@@ -193,12 +199,17 @@ function auditChapters(report, { chapters, ships }) {
193
199
  }
194
200
 
195
201
  const numbers = chapters.map((chapter) => chapter.number).sort((a, b) => a - b);
196
- const contiguous = numbers.every((number, index) => number === index + 1);
202
+ const hasOperating = numbers.includes(4);
203
+ // 04 is the one number the spine never requires (`docs-operating-missing`
204
+ // Asks for it on its own terms), so a run missing it is still contiguous —
205
+ // Every number from 05 on shifts down one slot to close the gap.
206
+ const expected = (index) => (!hasOperating && index + 1 >= 4 ? index + 2 : index + 1);
207
+ const contiguous = numbers.every((number, index) => number === expected(index));
197
208
  if (numbers.length > 0 && !contiguous) {
198
209
  report(
199
210
  'docs-chapter-numbering',
200
211
  'docs/',
201
- `chapter numbers run ${numbers.map(padded).join(', ')}: they are contiguous from 01, one file per number`,
212
+ `chapter numbers run ${numbers.map(padded).join(', ')}: they are contiguous from 01, one file per number, except that 04 may be absent`,
202
213
  );
203
214
  }
204
215
 
@@ -284,11 +295,11 @@ function auditRecord(report, record, heads) {
284
295
 
285
296
  const status = head.map((line) => DECISION_STATUS.exec(line)?.groups?.status).find(Boolean);
286
297
  if (status === undefined || !(STATUSES.has(status) || SUPERSEDED.test(status))) {
287
- report(
288
- 'docs-decision-status',
289
- record.path,
290
- `${record.path}: **Status:** is none of Proposed, Accepted, Superseded by ADR-NNN, Deprecated`,
291
- );
298
+ const message =
299
+ status !== undefined && BARE_SUPERSEDED.test(status)
300
+ ? `${record.path}: **Status:** names a successor but no link — write Superseded by [ADR-NNN](file.md)`
301
+ : `${record.path}: **Status:** is none of Proposed, Accepted, Superseded by [ADR-NNN](file.md), Deprecated`;
302
+ report('docs-decision-status', record.path, message);
292
303
  }
293
304
  }
294
305
 
@@ -313,6 +324,16 @@ function auditDecisions(report, { files, heads }) {
313
324
  auditRecord(report, record, heads);
314
325
  }
315
326
 
327
+ const numbers = [...new Set(records.map((record) => record.name.slice(0, 3)))].sort();
328
+ const sequential = numbers.every((number, index) => Number.parseInt(number, 10) === index + 1);
329
+ if (numbers.length > 0 && !sequential) {
330
+ report(
331
+ 'docs-decision-sequence',
332
+ 'docs/decisions/',
333
+ `docs/decisions/ numbers run ${numbers.join(', ')}: they run from 001 with no gap — a decision that moved folders takes the next number where it lands`,
334
+ );
335
+ }
336
+
316
337
  const claimed = new Map();
317
338
  for (const record of records) {
318
339
  const number = record.name.slice(0, 3);
package/src/docs.test.ts CHANGED
@@ -154,7 +154,7 @@ test('names chapter numbers that skip, and numbers claimed twice', () => {
154
154
 
155
155
  // Then - one sentence about the whole run
156
156
  expect(sentence(tree, 'docs-chapter-numbering')).toBe(
157
- 'chapter numbers run 01, 02, 03, 05, 05: they are contiguous from 01, one file per number',
157
+ 'chapter numbers run 01, 02, 03, 05, 05: they are contiguous from 01, one file per number, except that 04 may be absent',
158
158
  );
159
159
  });
160
160
 
@@ -211,6 +211,40 @@ test('never asks for 04-operating.md from a repository that ships nothing', () =
211
211
  expect(rules(manual())).toEqual([]);
212
212
  });
213
213
 
214
+ test('lets 05 follow 03 directly when the repository ships nothing', () => {
215
+ // Given - 01-03 then 05, no 04 chapter, nothing that would ask for one
216
+ const tree = manual({
217
+ files: [...manual().files, 'docs/05-building.md'],
218
+ links: { 'docs/README.md': [...manual().links['docs/README.md'], '05-building.md'] },
219
+ });
220
+
221
+ // Then - 04 is the one gap the numbering excuses
222
+ expect(rules(tree)).toEqual([]);
223
+ });
224
+
225
+ test('still asks for 04-operating.md from that same shape once it ships', () => {
226
+ // Given - the same 01-03 + 05 shape, but an image this time
227
+ const tree = manual({
228
+ files: [...manual().files, 'docs/05-building.md'],
229
+ links: { 'docs/README.md': [...manual().links['docs/README.md'], '05-building.md'] },
230
+ ships: { dockerfile: true, infrastructure: false, publishable: false },
231
+ });
232
+
233
+ // Then - the numbering stays clean; only the presence test speaks
234
+ expect(rules(tree)).toEqual(['docs-operating-missing']);
235
+ });
236
+
237
+ test('refuses a gap at 05 even though 04 may be absent', () => {
238
+ // Given - 01-03 then 06, skipping past the one number the spine excuses
239
+ const tree = manual({
240
+ files: [...manual().files, 'docs/06-quality-checks.md'],
241
+ links: { 'docs/README.md': [...manual().links['docs/README.md'], '06-quality-checks.md'] },
242
+ });
243
+
244
+ // Then - only 04 is excused; 05 still has to be there
245
+ expect(rules(tree)).toEqual(['docs-chapter-numbering']);
246
+ });
247
+
214
248
  test('names a chapter that is a journal, not a subject', () => {
215
249
  // Given - a fourth chapter recording a design exploration
216
250
  const tree = manual({
@@ -332,12 +366,28 @@ test('names a decision outside the status vocabulary', () => {
332
366
 
333
367
  // Then - the four the vocabulary holds
334
368
  expect(sentence(tree, 'docs-decision-status')).toBe(
335
- 'docs/decisions/001-the-first-call.md: **Status:** is none of Proposed, Accepted, Superseded by ADR-NNN, Deprecated',
369
+ 'docs/decisions/001-the-first-call.md: **Status:** is none of Proposed, Accepted, Superseded by [ADR-NNN](file.md), Deprecated',
336
370
  );
337
371
  });
338
372
 
339
- test('accepts a superseded status that names the record replacing it', () => {
340
- // Given - the one status carrying an argument
373
+ test('accepts a superseded status that links the record replacing it', () => {
374
+ // Given - the one status carrying an argument, written as a citation
375
+ const tree = withDecisions({
376
+ heads: {
377
+ 'docs/decisions/001-the-first-call.md': [
378
+ '# ADR-001: The first call',
379
+ '',
380
+ '**Status:** Superseded by [ADR-014](014-the-later-call.md)',
381
+ ],
382
+ },
383
+ });
384
+
385
+ // Then - nothing to say
386
+ expect(rules(tree)).toEqual([]);
387
+ });
388
+
389
+ test('refuses a superseded status with no link naming the successor', () => {
390
+ // Given - the bare form: a successor is named, but not as a place to look
341
391
  const tree = withDecisions({
342
392
  heads: {
343
393
  'docs/decisions/001-the-first-call.md': [
@@ -348,10 +398,108 @@ test('accepts a superseded status that names the record replacing it', () => {
348
398
  },
349
399
  });
350
400
 
401
+ // Then - the missing link is the whole complaint
402
+ expect(sentence(tree, 'docs-decision-status')).toBe(
403
+ 'docs/decisions/001-the-first-call.md: **Status:** names a successor but no link — write Superseded by [ADR-NNN](file.md)',
404
+ );
405
+ });
406
+
407
+ test('passes an empty decisions folder', () => {
408
+ // Given - the folder exists but holds no record yet
409
+ const tree = manual({ files: [...manual().files, 'docs/decisions/'] });
410
+
411
+ // Then - a folder with nothing to number has no sequence to break
412
+ expect(rules(tree)).toEqual(['docs-template-missing']);
413
+ });
414
+
415
+ test('passes a decisions folder holding only the template', () => {
416
+ // Given - the mold, and not a single record beside it
417
+ const tree = manual({
418
+ files: [...manual().files, 'docs/decisions/', 'docs/decisions/_template.md'],
419
+ });
420
+
351
421
  // Then - nothing to say
352
422
  expect(rules(tree)).toEqual([]);
353
423
  });
354
424
 
425
+ test('refuses a decision sequence that skips a number', () => {
426
+ // Given - 001 and 003, with no record ever numbered 002
427
+ const tree = withDecisions({
428
+ files: [
429
+ ...manual().files,
430
+ 'docs/decisions/',
431
+ 'docs/decisions/001-the-first-call.md',
432
+ 'docs/decisions/003-the-third-call.md',
433
+ 'docs/decisions/_template.md',
434
+ ],
435
+ heads: {
436
+ 'docs/decisions/001-the-first-call.md': [
437
+ '# ADR-001: The first call',
438
+ '',
439
+ '**Status:** Accepted',
440
+ ],
441
+ 'docs/decisions/003-the-third-call.md': [
442
+ '# ADR-003: The third call',
443
+ '',
444
+ '**Status:** Accepted',
445
+ ],
446
+ },
447
+ });
448
+
449
+ // Then - the gap is named, from 001
450
+ expect(sentence(tree, 'docs-decision-sequence')).toBe(
451
+ 'docs/decisions/ numbers run 001, 003: they run from 001 with no gap — a decision that moved folders takes the next number where it lands',
452
+ );
453
+ });
454
+
455
+ test('refuses a decision sequence that starts above 001', () => {
456
+ // Given - 006 and 008, with nothing numbered before them
457
+ const tree = withDecisions({
458
+ files: [
459
+ ...manual().files,
460
+ 'docs/decisions/',
461
+ 'docs/decisions/006-the-sixth-call.md',
462
+ 'docs/decisions/008-the-eighth-call.md',
463
+ 'docs/decisions/_template.md',
464
+ ],
465
+ heads: {
466
+ 'docs/decisions/006-the-sixth-call.md': [
467
+ '# ADR-006: The sixth call',
468
+ '',
469
+ '**Status:** Accepted',
470
+ ],
471
+ 'docs/decisions/008-the-eighth-call.md': [
472
+ '# ADR-008: The eighth call',
473
+ '',
474
+ '**Status:** Accepted',
475
+ ],
476
+ },
477
+ });
478
+
479
+ // Then - the same rule, from an offset start
480
+ expect(sentence(tree, 'docs-decision-sequence')).toBe(
481
+ 'docs/decisions/ numbers run 006, 008: they run from 001 with no gap — a decision that moved folders takes the next number where it lands',
482
+ );
483
+ });
484
+
485
+ test('lets a duplicated number pass the sequence check on its own', () => {
486
+ // Given - two records claiming 001, and none other — the set is just {001}
487
+ const tree = withDecisions({
488
+ files: [...withDecisions().files, 'docs/decisions/001-the-same-call.md'],
489
+ heads: {
490
+ ...withDecisions().heads,
491
+ 'docs/decisions/001-the-same-call.md': [
492
+ '# ADR-001: The same call',
493
+ '',
494
+ '**Status:** Proposed',
495
+ ],
496
+ },
497
+ });
498
+
499
+ // Then - docs-decision-number still refuses it; the sequence itself is not the gap
500
+ expect(rules(tree)).toEqual(['docs-decision-number']);
501
+ });
502
+
355
503
  test('names a number two records claim', () => {
356
504
  // Given - two records numbered 001
357
505
  const tree = withDecisions({