@jterrazz/typescript 9.2.0 → 9.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jterrazz/typescript",
3
- "version": "9.2.0",
3
+ "version": "9.2.1",
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
 
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,18 +366,18 @@ 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
341
375
  const tree = withDecisions({
342
376
  heads: {
343
377
  'docs/decisions/001-the-first-call.md': [
344
378
  '# ADR-001: The first call',
345
379
  '',
346
- '**Status:** Superseded by ADR-014',
380
+ '**Status:** Superseded by [ADR-014](014-the-later-call.md)',
347
381
  ],
348
382
  },
349
383
  });
@@ -352,6 +386,24 @@ test('accepts a superseded status that names the record replacing it', () => {
352
386
  expect(rules(tree)).toEqual([]);
353
387
  });
354
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
391
+ const tree = withDecisions({
392
+ heads: {
393
+ 'docs/decisions/001-the-first-call.md': [
394
+ '# ADR-001: The first call',
395
+ '',
396
+ '**Status:** Superseded by ADR-014',
397
+ ],
398
+ },
399
+ });
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
+
355
407
  test('names a number two records claim', () => {
356
408
  // Given - two records numbered 001
357
409
  const tree = withDecisions({