@assuredloop/cli 3.0.0 → 3.1.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": "@assuredloop/cli",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
4
4
  "description": "AssuredLoop keeps an AI focused on one change: the al command gathers and checks a change's requirement, spec, decisions, code and tests.",
5
5
  "license": "MIT",
6
6
  "repository": {
package/src/v4/blocks.js CHANGED
@@ -175,7 +175,7 @@ export const citedMessage = (c) => (c.next.length
175
175
 
176
176
  // The ADR that a link value or word names: `ADR-n` and `ADR-n-k` name ADR-n.
177
177
  const ADR_REF = /^ADR-(\d+)(?:-\d+)?$/;
178
- const adrOf = (ref) => (ADR_REF.test(ref) ? `ADR-${Number(ADR_REF.exec(ref)[1])}` : null);
178
+ export const adrOf = (ref) => (ADR_REF.test(ref) ? `ADR-${Number(ADR_REF.exec(ref)[1])}` : null);
179
179
  const MARKER = /^\s*<!--(.*)-->\s*$/;
180
180
 
181
181
  // The words that name ADRs in a text (adr-lists R1): `ADR-n`, or the number
package/src/v4/checks.js CHANGED
@@ -18,7 +18,7 @@ import {
18
18
  import { NEAR, closeness, diffWords, marks, requirementWords, sentencesOf, showHunk } from './words.js';
19
19
  import { groupOf, markerLines, onlyLinksAdded, sameBlocks } from './markers.js';
20
20
  import { canAlignTo, declaredNow, isCentral } from './indexer.js';
21
- import { recordBlocks, supersededBlocks } from './blocks.js';
21
+ import { adrOf, recordBlocks, supersededBlocks } from './blocks.js';
22
22
  import { phraseFindings } from './phrases.js';
23
23
  import { issueHints } from './issues.js';
24
24
 
@@ -388,11 +388,22 @@ export function checks(top, { base, phrases = [], issues = null }) {
388
388
  // changed (#4) and the rest unchanged; that gives no finding.
389
389
  const linksAdded = (a, h) => a.status === 'accepted' && a.head && h.head && a.rest === h.rest
390
390
  && onlyLinksAdded(a.marker, h.marker, now.kinds);
391
+ // A status goes only forward: accepted to superseded, and a superseded-by
392
+ // list only gains ADRs (#31). Any other change of a known status line is a
393
+ // change of the ADR; an unknown one has its own hint, adr-status.
394
+ const statusBack = (a, h) => h.known
395
+ && !(h.status === 'superseded' ? a.by.every((b) => h.by.includes(b)) : h.status === a.status);
391
396
  for (const [id, a] of was?.adrs ?? []) {
392
397
  if (!['accepted', 'superseded'].includes(a.status)) continue;
393
398
  const h = now.adrs.get(id);
394
- if (!h) add('hint', 'adr-changed', a.file, id, `${id} was ${a.status} and its file is gone: check that its decision is not lost; an ADR that no longer holds is superseded by a new ADR, not deleted`);
395
- else if (h.body !== a.body && !linksAdded(a, h)) add('hint', 'adr-changed', h.head ?? h.file, id, `${id}'s text changed: check that its decision is unchanged; if the decision changed, write a new ADR that supersedes it`);
399
+ if (!h) {
400
+ add('hint', 'adr-changed', a.file, id, `${id} was ${a.status} and its file is gone: check that its decision is not lost; an ADR that no longer holds is superseded by a new ADR, not deleted`);
401
+ continue;
402
+ }
403
+ const back = statusBack(a, h);
404
+ const changed = h.body !== a.body && !linksAdded(a, h);
405
+ if (back) add('hint', 'adr-changed', h.head ?? h.file, id, `${id}'s status changed from "${a.line}" to "${h.line}"${changed ? ', and its text changed' : ''}, but a status goes only forward: proposed, accepted, superseded by ADR-n; check that its decision is unchanged; if the decision changed, write a new ADR that supersedes it`);
406
+ else if (changed) add('hint', 'adr-changed', h.head ?? h.file, id, `${id}'s text changed: check that its decision is unchanged; if the decision changed, write a new ADR that supersedes it`);
396
407
  }
397
408
  // Every ADR, whatever its status (#4): a first line that is not a known
398
409
  // status, and a marker line that holds more than its ID, a kind and links.
@@ -410,9 +421,13 @@ export function checks(top, { base, phrases = [], issues = null }) {
410
421
  if (missing.length) add('hint', 'adr-links', a.head ?? a.file, a.id, `${a.id} has ${missing.map((w) => `no ${w}:`).join(' and ')}; add the links to its marker`);
411
422
  }
412
423
  for (const a of now.adrs.values()) {
413
- for (const old of a.head?.links.supersedes ?? []) {
424
+ // ADR-n-k, a paragraph of ADR-n, names ADR-n (schema.md 5), once each.
425
+ for (const old of new Set((a.head?.links.supersedes ?? []).map(adrOf).filter(Boolean))) {
414
426
  const o = now.adrs.get(old);
415
427
  if (o && o.status !== 'superseded') add('hint', 'adr-supersede', a.head, a.id, `${a.id} supersedes ${old}, whose status is ${o.status ?? 'missing'}; set it to superseded`);
428
+ // The other direction of superseded-status (#31): the status line of
429
+ // the superseded ADR names each ADR that supersedes it.
430
+ else if (o && !o.by.includes(a.id)) add('hint', 'adr-supersede', a.head, a.id, `${a.id} supersedes ${old}, but the status line of ${old} leaves out ${a.id}: write "Status: superseded by ${[...o.by, a.id].join(', ')}"`);
416
431
  }
417
432
  }
418
433
  for (const c of changes.filter((x) => !x.request)) {
package/src/v4/state.js CHANGED
@@ -175,7 +175,9 @@ export function loadState(top, rev = null) {
175
175
  const by = [...new Set([...(s?.[2] ?? '').matchAll(/ADR-(\d+)/gi)].map((m) => `ADR-${Number(m[1])}`))];
176
176
  const known = Boolean(s) && !/[\r\n]/.test(s[1]);
177
177
  const marker = head && lf(text).split('\n')[head.line - 1];
178
- adrs.set(id, { id, file, text, status, by, known, body: adrBody(text), rest: head && adrRest(text, head), paragraphs, head, marker });
178
+ // The words after `Status:`, for a hint that names a changed status (#31).
179
+ const line = /^\s*Status:[ \t]*([^\r\n]*)/i.exec(text)?.[1].trim() ?? null;
180
+ adrs.set(id, { id, file, text, status, by, known, line, body: adrBody(text), rest: head && adrRest(text, head), paragraphs, head, marker });
179
181
  }
180
182
  return { top, rev, config, kinds, root, get, docs, scopes, lints, spec, requests, adrs, unreadableAdrs };
181
183
  }