@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 +1 -1
- package/src/docs.js +30 -9
- package/src/docs.test.ts +152 -4
package/package.json
CHANGED
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
|
-
/**
|
|
52
|
-
|
|
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
|
|
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
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
|
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({
|