@assuredloop/cli 0.0.1 → 0.2.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/LICENSE +21 -0
- package/README.md +311 -4
- package/bin/al.js +129 -0
- package/package.json +31 -4
- package/skills/assuredloop/SKILL.md +207 -0
- package/src/v4/base.js +80 -0
- package/src/v4/check.js +81 -0
- package/src/v4/checks.js +383 -0
- package/src/v4/commands.js +168 -0
- package/src/v4/conclude.js +119 -0
- package/src/v4/config.js +75 -0
- package/src/v4/dispositions.js +191 -0
- package/src/v4/export.js +469 -0
- package/src/v4/git.js +153 -0
- package/src/v4/ids.js +87 -0
- package/src/v4/indexer.js +361 -0
- package/src/v4/markers.js +290 -0
- package/src/v4/records.js +66 -0
- package/src/v4/repos.js +329 -0
- package/src/v4/request-md.js +108 -0
- package/src/v4/results.js +42 -0
- package/src/v4/scope.js +167 -0
- package/src/v4/search.js +443 -0
- package/src/v4/snapshot.js +20 -0
- package/src/v4/spec.js +148 -0
- package/src/v4/state.js +208 -0
- package/src/v4/views.js +480 -0
- package/src/v4/words.js +98 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: assuredloop
|
|
3
|
+
description: >-
|
|
4
|
+
Keeping the records of a change with AssuredLoop's `al` (v4): the owner's
|
|
5
|
+
words, the signed requirement, the change spec, the hidden paragraph
|
|
6
|
+
markers in the spec, the decisions and the dispositions. Use in a project
|
|
7
|
+
whose AGENTS.md names AssuredLoop, before work that changes the spec, and
|
|
8
|
+
whenever you need to know where a request stands.
|
|
9
|
+
---
|
|
10
|
+
# AssuredLoop v4
|
|
11
|
+
|
|
12
|
+
`al` keeps the records of what was asked, decided, changed and delivered,
|
|
13
|
+
and gives hints. It refuses only in `al conclude`. How and when you design,
|
|
14
|
+
test and review is your project's way of working; this skill does not set it.
|
|
15
|
+
|
|
16
|
+
## Start here, on every path
|
|
17
|
+
|
|
18
|
+
1. Run `al context`. For each spec paragraph that your work touches, run
|
|
19
|
+
`al context <ID>`. For an open request, run `al context <name>`. Each one
|
|
20
|
+
shows the requirement, the open changes, the ADRs and the checks of what
|
|
21
|
+
it names. Read those paragraphs and records, not the whole spec.
|
|
22
|
+
2. To find a paragraph by its words, run `al search <words>`. Add
|
|
23
|
+
`--change <name>` for one request's open text, and `--history` for removed
|
|
24
|
+
and replaced text. In a central repo, a hit from an output repo shows
|
|
25
|
+
`<repo>:<path>` and the lines that name a central ID.
|
|
26
|
+
3. Choose the path from the table. Then read "Markers" and the section of
|
|
27
|
+
your path only.
|
|
28
|
+
|
|
29
|
+
| Path | When | Records | Owner's sign-off |
|
|
30
|
+
|---|---|---|---|
|
|
31
|
+
| 0 | No promise changes: a fix that restores what the spec says, or a typo in any paragraph. | none | no |
|
|
32
|
+
| 1 | One or a few promise paragraphs change. | a short request; no change spec | yes |
|
|
33
|
+
| 1d | Only names or wording of design paragraphs change. | a short request; no change spec | no |
|
|
34
|
+
| 2 | A real design, in one or a few PRs. | a request and a change spec | if a promise changes |
|
|
35
|
+
| 3 | A big change, delivered in parts over time. | as path 2, part by part | as path 2 |
|
|
36
|
+
| S | It is not clear yet what to build. | a request with a question | yes, on the question |
|
|
37
|
+
|
|
38
|
+
The promise kinds are purpose, scope, rule, limit and definition. Work that
|
|
39
|
+
grows changes its path: a small change that needs a design becomes path 2.
|
|
40
|
+
|
|
41
|
+
4. Before you ask for review, run `al index`, then `al check`. Fix each
|
|
42
|
+
`not ok`, or say in the PR why it stays. A hint is an observation, not a
|
|
43
|
+
verdict.
|
|
44
|
+
5. Put the claim in a commit message and in the PR, for example
|
|
45
|
+
`Tier: 1 — amends INV-10`. `al check` reads the newest `Tier:` line of the
|
|
46
|
+
branch.
|
|
47
|
+
|
|
48
|
+
## Markers
|
|
49
|
+
|
|
50
|
+
Each paragraph in `specs/` and in a `requests/<name>/spec.md` has a hidden
|
|
51
|
+
marker on its own line before it, with a blank line before and after it:
|
|
52
|
+
|
|
53
|
+
```markdown
|
|
54
|
+
<!-- INV-41 rule serves:invoice-exports/R2 builds-on:INV-12 -->
|
|
55
|
+
|
|
56
|
+
The export link MUST expire 30 minutes after the email is sent.
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
- When you edit a marked paragraph, keep its marker line and the blank lines
|
|
60
|
+
around it. Keep the ID when you move the paragraph. Never renumber or
|
|
61
|
+
delete an ID: a lost ID is `not ok`.
|
|
62
|
+
- For a new paragraph, write the text with no marker. Then run
|
|
63
|
+
`al spec --add-ids <file> --yes`: it gives each unmarked paragraph the next
|
|
64
|
+
free ID. Then write the kind and the links into each new marker.
|
|
65
|
+
- The kinds: promise (purpose, scope, rule, limit, definition); design
|
|
66
|
+
(component, interface, data, flow, choice); approach (how one change is
|
|
67
|
+
built; it stays with the change); informative (rationale, example, open);
|
|
68
|
+
note (headings and connecting text); change-only (plan, step, migration).
|
|
69
|
+
- The links: `serves:R<n>` in a change spec, `serves:<request>/R<n>` in
|
|
70
|
+
`specs/`; `builds-on:<ID>`, `changes:<ID>`, `removes:<ID>`;
|
|
71
|
+
`explains:<ID>`, `illustrates:<ID>`, `resolved-by:<ID>`,
|
|
72
|
+
`governed-by:<ADR>`; `for:<task>`. A promise, design or approach paragraph
|
|
73
|
+
needs at least one link. In `specs/`, the ID of a change spec paragraph
|
|
74
|
+
names its request: `invoice-exports/SP-12`.
|
|
75
|
+
- `al` writes every hash and every derived field. Do not write them by hand.
|
|
76
|
+
|
|
77
|
+
## Path 0: a fix
|
|
78
|
+
|
|
79
|
+
1. Change the code, or the spec text that has the error.
|
|
80
|
+
2. Write no request. Claim one of:
|
|
81
|
+
- `Tier: 0 — restores INV-41` when the work makes the code do what the
|
|
82
|
+
spec says;
|
|
83
|
+
- `Tier: 0 — typo in INV-3` when you fix a typo in a spec paragraph. Name
|
|
84
|
+
each paragraph: `Tier: 0 — typo in INV-3, EXP-4`.
|
|
85
|
+
3. `al check` shows each changed word. It marks a change to a normative
|
|
86
|
+
word, a negation, a number or a quantifier as "meaning-sensitive". The
|
|
87
|
+
review decides whether the meaning stays the same.
|
|
88
|
+
|
|
89
|
+
## Path 1: a small promise change
|
|
90
|
+
|
|
91
|
+
1. `al new <name> --tier 1 --from -`, with the owner's words on standard
|
|
92
|
+
input. The name is permanent.
|
|
93
|
+
2. In `requests/<name>/request.md`, add the organized requirement, in plain
|
|
94
|
+
words that the owner reads:
|
|
95
|
+
|
|
96
|
+
```markdown
|
|
97
|
+
## Organized requirement
|
|
98
|
+
|
|
99
|
+
<!-- R1 from:<the snapshot file in origin/> -->
|
|
100
|
+
|
|
101
|
+
### R1 <a short title>
|
|
102
|
+
<one requirement, with MUST, SHOULD or MAY>
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
3. Run `al index`. Show the requirement to the owner with
|
|
106
|
+
`al record <name> signoff --source <where> --words "<their words>"`. Only
|
|
107
|
+
on the owner's own yes, run the same command with `--yes`. Do not sign for
|
|
108
|
+
the owner.
|
|
109
|
+
4. Edit the promise paragraphs in `specs/` directly. Each changed or new
|
|
110
|
+
promise paragraph says `serves:<name>/R<n>` in its marker.
|
|
111
|
+
5. Run `al index`, then `al check`. Claim `Tier: 1 — amends <ID>`. When the
|
|
112
|
+
PR is ready to merge, run `al conclude <name> --yes`: it writes the
|
|
113
|
+
Outcome and moves the request to `requests/archive/<name>/`.
|
|
114
|
+
|
|
115
|
+
## Path 1d: a small design correction
|
|
116
|
+
|
|
117
|
+
1. `al new <name> --tier 1d --from -`, with a one-line note of where the
|
|
118
|
+
correction comes from.
|
|
119
|
+
2. Change only names or wording of design paragraphs in `specs/`. Add or
|
|
120
|
+
remove no paragraph. Do not change an accepted ADR's text: a new ADR
|
|
121
|
+
supersedes it.
|
|
122
|
+
3. Claim `Tier: 1d — <what changes>`. `al check` gives `not ok` when the PR
|
|
123
|
+
touches a promise paragraph, or adds or removes a design paragraph. No
|
|
124
|
+
sign-off is needed.
|
|
125
|
+
4. Run `al conclude <name> --yes` with the PR.
|
|
126
|
+
|
|
127
|
+
## Path 2: a big change
|
|
128
|
+
|
|
129
|
+
1. `al new <name> --tier 2 --from -`. Write the organized requirement as in
|
|
130
|
+
path 1, with `Out:` and `Assumed:` lines where they help. Draft freely;
|
|
131
|
+
deliver with the owner's sign-off or after it. A change that touches no
|
|
132
|
+
promise kind needs no sign-off.
|
|
133
|
+
2. Write the design in `requests/<name>/spec.md`, the change spec: the
|
|
134
|
+
promises this change adds or changes, its design, its plan. Run
|
|
135
|
+
`al spec --add-ids requests/<name>/spec.md --yes` for the IDs (SP-1,
|
|
136
|
+
SP-2…), then write the kinds and links. A paragraph that relies on a spec
|
|
137
|
+
paragraph says `builds-on:<ID>`. One that replaces it says `changes:<ID>`.
|
|
138
|
+
One that removes it says `removes:<ID>`. Never copy the old text.
|
|
139
|
+
3. Snapshot each later word of the owner that you rely on:
|
|
140
|
+
`al record <name> origin --url <where> --from - --yes`. Record each later
|
|
141
|
+
decision: `al record <name> decision --source <who> --text "<it>" --yes`.
|
|
142
|
+
4. Consolidate by hand, in the PR with the code or later: copy each paragraph
|
|
143
|
+
of a kind that goes into `specs/` (promise, the lasting design kinds,
|
|
144
|
+
rationale, example, open) into `specs/`, word for word. Give it a spec ID
|
|
145
|
+
(`al spec --add-ids`), its kind, and its links. In `specs/`, a link names
|
|
146
|
+
the request: `serves:<name>/R<n>`. Plan, step, migration, approach and note
|
|
147
|
+
paragraphs stay with the change.
|
|
148
|
+
5. For each change paragraph that goes into `specs/`, and each `removes:`,
|
|
149
|
+
add a disposition to `.assuredloop/records/requests/<name>.yaml`, then run
|
|
150
|
+
`al index` to fill its hashes:
|
|
151
|
+
|
|
152
|
+
```yaml
|
|
153
|
+
dispositions:
|
|
154
|
+
- {source: <name>/SP-4, disposition: incorporated, spec: EXP-9}
|
|
155
|
+
- {source: <name>/SP-6, disposition: removed, spec: INV-4}
|
|
156
|
+
- {source: <name>/SP-7, disposition: abandoned, decision: D2}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
- `incorporated`: the spec text equals the change paragraph exactly. If
|
|
160
|
+
you word it differently, record a decision and add
|
|
161
|
+
`wording: {source: <name>/SP-n, spec: <ID>}` to that decision in the
|
|
162
|
+
record; the disposition then names `decision: Dn`.
|
|
163
|
+
- `removed`: the paragraph declares `removes:<ID>`, and the ID is gone
|
|
164
|
+
from `specs/`. Removing a promise needs a signed requirement.
|
|
165
|
+
- `superseded`: `by: <later request>/SP-n`, when a later change replaced
|
|
166
|
+
the incorporated text.
|
|
167
|
+
- `abandoned`: the paragraph is not delivered. Name the owner's decision
|
|
168
|
+
when an applied effect is kept.
|
|
169
|
+
6. When `al check` says a paragraph you build on changed, check your change
|
|
170
|
+
against the new text, then run `al index --align <name>/SP-n`.
|
|
171
|
+
7. Close with `al conclude <name> --yes`. It refuses while a promise change
|
|
172
|
+
is not signed, or a paragraph with a baseline effect has no valid
|
|
173
|
+
disposition for its current version. `al context <name>` lists them.
|
|
174
|
+
|
|
175
|
+
## Path 3: an epic
|
|
176
|
+
|
|
177
|
+
As path 2, in parts. Each part consolidates the paragraphs it delivers,
|
|
178
|
+
with their dispositions, in its own PR. Tasks are optional: where the
|
|
179
|
+
project uses them, list them in the record as
|
|
180
|
+
`tasks: [{id: T2, ref: "issue #34", delivers: [R3]}]`, and name the task in
|
|
181
|
+
the commit messages of its PRs. Run `al conclude <name> --yes` with the last
|
|
182
|
+
part.
|
|
183
|
+
|
|
184
|
+
## Path S: a spike
|
|
185
|
+
|
|
186
|
+
1. `al new <name> --tier S --from -`. In `request.md`, write
|
|
187
|
+
`## Organized question` with `<!-- Q1 -->` and `### Q1 <title>`.
|
|
188
|
+
2. The owner signs the question, as in path 1, step 3.
|
|
189
|
+
3. The change spec holds the candidates and the experiments. It is never
|
|
190
|
+
consolidated and needs no dispositions.
|
|
191
|
+
4. Write the findings in the request folder. Open them with the answer, and
|
|
192
|
+
name the question version that it answers, for example "Answer to Q1
|
|
193
|
+
version 1: …". Change no spec text.
|
|
194
|
+
5. Run `al conclude <name> --yes`. A spike may end with no follow-up. An
|
|
195
|
+
answer may also inform work that is already open. Only separately
|
|
196
|
+
authorized work needs a new request; say in its `request.md` that it
|
|
197
|
+
follows the spike: `Follows: <name>`.
|
|
198
|
+
|
|
199
|
+
## The views
|
|
200
|
+
|
|
201
|
+
- `al context --diff main...HEAD --for review`: each changed paragraph
|
|
202
|
+
beside its kind, its requirement and the baseline it touches.
|
|
203
|
+
- `al context <name> --audit`: the whole trace of a request. Add
|
|
204
|
+
`--at <commit>` to any view for a past state.
|
|
205
|
+
- `al spec`: every doc's IDs, display numbers and kinds.
|
|
206
|
+
- Each output ends with `Read`, `Next` and `Not known`. Take `Not known` as
|
|
207
|
+
it is: the tool does not know it, and you do not either.
|
package/src/v4/base.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// What every v4 command shares: the failure that refuses, the clock, hashes,
|
|
2
|
+
// request names and the record paths.
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, lstatSync } from 'node:fs';
|
|
5
|
+
import { dirname, isAbsolute, join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
// A refusal: exit 2, `al: <message>`, and nothing written.
|
|
8
|
+
export class Fail extends Error {
|
|
9
|
+
constructor(message, next) { super(message); this.next = next; }
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const sha256 = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
|
13
|
+
|
|
14
|
+
// The clock, read only to stamp what the tool writes. SOURCE_DATE_EPOCH
|
|
15
|
+
// (seconds) stands in for it when set.
|
|
16
|
+
export const now = () => (process.env.SOURCE_DATE_EPOCH ? new Date(Number(process.env.SOURCE_DATE_EPOCH) * 1000) : new Date());
|
|
17
|
+
export const STAMP = /^\d{4}-\d\d-\d\dT\d\d:\d\dZ$/;
|
|
18
|
+
export const stamp = (d) => d.toISOString().slice(0, 16) + 'Z';
|
|
19
|
+
export const day = (d) => d.toISOString().slice(0, 10);
|
|
20
|
+
|
|
21
|
+
// A request name: lowercase letters, digits and hyphens; never a path.
|
|
22
|
+
export const isName = (s) => typeof s === 'string' && /^[a-z0-9][a-z0-9-]*$/.test(s);
|
|
23
|
+
|
|
24
|
+
export const SCHEMA = 'assuredloop/1';
|
|
25
|
+
export const recordPath = (name) => `.assuredloop/records/requests/${name}.yaml`;
|
|
26
|
+
export const docRecordPath = (file) => `.assuredloop/records/${file}.yaml`;
|
|
27
|
+
|
|
28
|
+
// A path that stays inside the repo: relative, with no `..` part.
|
|
29
|
+
export const inside = (path) => !isAbsolute(path) && !path.split(/[\\/]/).includes('..');
|
|
30
|
+
|
|
31
|
+
// The first part of `path` (a path under `top`) that is a symlink, or null.
|
|
32
|
+
function linkOn(top, path) {
|
|
33
|
+
let at = top;
|
|
34
|
+
for (const part of path.split('/')) {
|
|
35
|
+
at = join(at, part);
|
|
36
|
+
let st;
|
|
37
|
+
try { st = lstatSync(at); } catch { return null; }
|
|
38
|
+
if (st.isSymbolicLink()) return at.slice(top.length + 1);
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// The text of `path` under `top` (its bytes with `encoding` null), or null
|
|
44
|
+
// when it is absent. Nothing is read outside the repo or through a symlink,
|
|
45
|
+
// which can lead out of it: either is a refusal.
|
|
46
|
+
export function read(top, path, encoding = 'utf8') {
|
|
47
|
+
if (!inside(path)) throw new Fail(`${path} is outside the repository; nothing was read or written`);
|
|
48
|
+
const link = linkOn(top, path);
|
|
49
|
+
if (link) throw new Fail(`${path} is reached through the symlink ${link}; nothing was read or written`, `make ${link} a real file or folder in this repo`);
|
|
50
|
+
try { return readFileSync(join(top, path), encoding); } catch { return null; }
|
|
51
|
+
}
|
|
52
|
+
export const exists = (top, path) => existsSync(join(top, path));
|
|
53
|
+
|
|
54
|
+
// Refuses, before anything is written, when any of `paths` (paths under
|
|
55
|
+
// `top`) leaves the repo, or a part of one that exists is a symlink: records
|
|
56
|
+
// are written only through real folders, so nothing lands outside the repo.
|
|
57
|
+
export function guard(top, paths) {
|
|
58
|
+
for (const path of paths) {
|
|
59
|
+
if (!inside(path)) throw new Fail(`${path} is outside the repository; nothing was written`);
|
|
60
|
+
const link = linkOn(top, path);
|
|
61
|
+
if (link) throw new Fail(`${path} is reached through the symlink ${link}; records are written only through real folders, and nothing was written`, `make ${link} a real folder in this repo`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Writes `text` to `path` under `top`, making its folders; a file whose text
|
|
66
|
+
// is already that is left alone, so indexing twice changes no byte.
|
|
67
|
+
export function write(top, path, text) {
|
|
68
|
+
guard(top, [path]);
|
|
69
|
+
if (read(top, path) === text) return false;
|
|
70
|
+
mkdirSync(dirname(join(top, path)), { recursive: true });
|
|
71
|
+
writeFileSync(join(top, path), text);
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// The first file name free in `dir`: `<base>.md`, else `<base>-2.md`...
|
|
76
|
+
export function freeName(top, dir, base) {
|
|
77
|
+
let file = `${base}.md`;
|
|
78
|
+
for (let n = 2; existsSync(join(top, dir, file)); n++) file = `${base}-${n}.md`;
|
|
79
|
+
return file;
|
|
80
|
+
}
|
package/src/v4/check.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// al check (design.md 3, What the script checks): the lints of every marker in
|
|
2
|
+
// scope, and per scope the ID lints of the branch against its base (lost, used
|
|
3
|
+
// again, one ID in two files), then each paragraph's change (T8); then the
|
|
4
|
+
// checks that read records (T10, checks.js). The base is the merge-base of
|
|
5
|
+
// HEAD with main; the head is the working tree. It exits 0; with --strict, 1
|
|
6
|
+
// when it prints a `not ok`. --strict raises every marker lint and an invalid
|
|
7
|
+
// disposition to `not ok`; the hints that need judgment stay hints.
|
|
8
|
+
import { loadConfig, loadSchema } from './config.js';
|
|
9
|
+
import { mergeBase } from './git.js';
|
|
10
|
+
import { diffParagraphs, idLints } from './ids.js';
|
|
11
|
+
import { docsInScope, idsEverUsed, isShallow, notRead, parseScopes, rootProblem, SPEC } from './scope.js';
|
|
12
|
+
import { Fail } from './spec.js';
|
|
13
|
+
import { centralLookup } from './repos.js';
|
|
14
|
+
import { checks } from './checks.js';
|
|
15
|
+
|
|
16
|
+
export function check({ top, opts }) {
|
|
17
|
+
const config = loadConfig(top);
|
|
18
|
+
const root = rootProblem(config);
|
|
19
|
+
if (root) throw new Fail(root, 'fix root: in .assuredloop/config.yaml');
|
|
20
|
+
const { kinds } = loadSchema(top);
|
|
21
|
+
const base = mergeBase(top);
|
|
22
|
+
const now = parseScopes(docsInScope(top, config), kinds);
|
|
23
|
+
const was = base ? parseScopes(docsInScope(top, config, base), kinds) : null;
|
|
24
|
+
const used = base ? idsEverUsed(top, config, base) : null;
|
|
25
|
+
// A change spec's removes: names spec paragraphs.
|
|
26
|
+
const removes = [...now.scopes].filter(([s]) => s !== SPEC).flatMap(([, ps]) => ps.flatMap((p) => p.links.removes));
|
|
27
|
+
|
|
28
|
+
const lints = [...now.lints];
|
|
29
|
+
const changes = [];
|
|
30
|
+
for (const scope of new Set([...now.scopes.keys(), ...(was?.scopes.keys() ?? [])])) {
|
|
31
|
+
const head = now.scopes.get(scope) ?? [];
|
|
32
|
+
const before = was?.scopes.get(scope) ?? [];
|
|
33
|
+
if (!base) {
|
|
34
|
+
lints.push(...idLints(head, head));
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
lints.push(...idLints(before, head, { everUsed: used.get(scope) ?? [], removes: scope === SPEC ? removes : [] }));
|
|
38
|
+
changes.push(...diffParagraphs(before, head).filter((c) => c.changes.length));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const t10 = checks(top, { base, strict: opts.strict });
|
|
42
|
+
// An adopted paragraph needs no link while its text is the captured text (D16).
|
|
43
|
+
const shown = lints.filter((l) => !(l.code === 'no-link' && t10.adopted.has(l.id) && !l.file.startsWith('requests/')));
|
|
44
|
+
lints.length = 0;
|
|
45
|
+
lints.push(...shown);
|
|
46
|
+
const severity = (l) => (opts.strict ? 'not ok' : l.severity);
|
|
47
|
+
const body = notRead(top, config);
|
|
48
|
+
body.push(...lints.map((l) => `${severity(l)} ${l.code} ${l.file}:${l.line} ${l.id ?? '-'} ${l.message}`));
|
|
49
|
+
if (base) body.push(...changes.map((c) => `${c.file} ${c.id} ${c.changes.join('+')}`));
|
|
50
|
+
if (!lints.length) body.push('ok: no marker lint');
|
|
51
|
+
body.push(...t10.findings.map((f) => `${f.severity} ${f.code} ${f.where} ${f.id} ${f.message}`));
|
|
52
|
+
// Under --strict every marker lint is a not ok.
|
|
53
|
+
const notOk = lints.length > 0 || t10.findings.some((f) => f.severity === 'not ok');
|
|
54
|
+
// In an output repo: the central IDs that the branch's changed files cite (T12).
|
|
55
|
+
const central = centralLookup(top);
|
|
56
|
+
body.push(...central.body);
|
|
57
|
+
const notKnown = ['IDs used on branches that were never fetched here', ...t10.notKnown, ...central.notKnown,
|
|
58
|
+
'whether each kind is right, whether a requirement is fully covered, and whether a change is authorized in substance (the review judges these)'];
|
|
59
|
+
if (!base) notKnown.unshift('no base (no main branch, or no commit): lost and used-again IDs are not checked');
|
|
60
|
+
else if (isShallow(top)) notKnown.unshift('IDs used before the shallow history begins');
|
|
61
|
+
return {
|
|
62
|
+
body,
|
|
63
|
+
read: base ? `working tree · base ${base.slice(0, 7)} (merge-base with main)` : 'working tree · no base',
|
|
64
|
+
next: nextStep([...lints.map(severity), ...t10.findings.map((f) => f.severity),
|
|
65
|
+
...central.body.map((l) => (/^not ok /.test(l) ? 'not ok' : /^hint /.test(l) ? 'hint' : 'info'))], lints.some((l) => l.code === 'no-id')),
|
|
66
|
+
notKnown,
|
|
67
|
+
exit: opts.strict && notOk ? 1 : 0,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// The Next line names what to do for what printed: a not ok, a hint, both, or
|
|
72
|
+
// neither (#198).
|
|
73
|
+
function nextStep(severities, noId) {
|
|
74
|
+
const notOk = severities.includes('not ok');
|
|
75
|
+
const hint = severities.includes('hint');
|
|
76
|
+
const addIds = noId ? '; al spec --add-ids <file> marks the paragraphs with no ID' : '';
|
|
77
|
+
if (notOk && hint) return `fix each not ok, and deal with each hint or say in the PR why it stays${addIds}`;
|
|
78
|
+
if (notOk) return `fix each not ok${addIds}`;
|
|
79
|
+
if (hint) return 'deal with each hint, or say in the PR why it stays';
|
|
80
|
+
return 'al context';
|
|
81
|
+
}
|