@am_shork/attest 0.8.0 → 0.9.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/CHANGELOG.md +886 -53
- package/README.md +3 -2
- package/bin/attest.js +42 -1
- package/dist/cli/report.js +19 -5
- package/dist/core/apply.d.ts +18 -1
- package/dist/core/apply.js +19 -2
- package/dist/core/locate.d.ts +19 -4
- package/dist/core/locate.js +116 -43
- package/dist/core/merge.js +218 -72
- package/dist/core/pipeline.js +45 -23
- package/dist/core/skill.js +88 -27
- package/dist/core/splice.d.ts +62 -3
- package/dist/core/splice.js +255 -13
- package/dist/core/static-registry.d.ts +55 -0
- package/dist/core/static-registry.js +141 -17
- package/dist/core/validator.d.ts +1 -0
- package/dist/core/validator.js +22 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -125,7 +125,8 @@ attest verify # run tests + coverage + drift, graded report
|
|
|
125
125
|
attest cover # which requirements lack a scenario
|
|
126
126
|
attest render # the requirements as Markdown, for people who don't read TS
|
|
127
127
|
attest archive <change> # gate a proposed change: green + covered + no drift
|
|
128
|
-
attest status <change> #
|
|
128
|
+
attest status <change> # per added id: scenario written? seen red? (part of that
|
|
129
|
+
# gate, without running anything — never a verdict)
|
|
129
130
|
```
|
|
130
131
|
|
|
131
132
|
Every command takes the project root as an optional last argument, and `--json`
|
|
@@ -166,7 +167,7 @@ Every diagnostic carries a `code`, and every code has a section in
|
|
|
166
167
|
```
|
|
167
168
|
ERROR registry-not-static (requirements/upload.reqs.ts:5)
|
|
168
169
|
Value is not a literal.
|
|
169
|
-
→ https://gitlab.com/Pseudorca/attest/-/blob/v0.
|
|
170
|
+
→ https://gitlab.com/Pseudorca/attest/-/blob/v0.9.1/docs/en/troubleshooting.md#registry-not-static
|
|
170
171
|
```
|
|
171
172
|
|
|
172
173
|
The anchor **is** the code, so the link cannot point somewhere the section
|
package/bin/attest.js
CHANGED
|
@@ -1,3 +1,44 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Thin launcher: delegates to the built CLI shell (src/cli -> dist/cli).
|
|
3
|
-
|
|
3
|
+
//
|
|
4
|
+
// **The compile cache is enabled here because here is the only place it can
|
|
5
|
+
// be.** `typescript` is the fixed toll on every invocation: measured on this
|
|
6
|
+
// machine, the CLI's whole dependency graph costs ~790 ms to load and the
|
|
7
|
+
// compiler is ~672 ms of it, against ~28 ms for commander, ~17 ms for chalk and
|
|
8
|
+
// ~70 ms for zod. Nothing can move that by loading it later, either — every
|
|
9
|
+
// command except `init` reads a registry or a spec through the AST, so the
|
|
10
|
+
// import is not merely eager, it is *needed*. What a code cache changes is the
|
|
11
|
+
// price of the same load: V8 keeps its compilation output between runs, and the
|
|
12
|
+
// saving lands on the commands a pipeline actually repeats. Measured over two
|
|
13
|
+
// independent A/B rounds, nine interleaved runs each, median: **11% to 28% off
|
|
14
|
+
// every command** — `check self` 1153 -> 970 and 1106 -> 971 ms, `cover self`
|
|
15
|
+
// 1122 -> 951 and 1346 -> 1195 ms, `status` 1110 -> 923 ms, `init` 904 -> 711
|
|
16
|
+
// and 1365 -> 984 ms. Quoted as a range because the absolute figures move with
|
|
17
|
+
// whatever else the machine is doing; the direction did not, across either
|
|
18
|
+
// round or any command in them.
|
|
19
|
+
//
|
|
20
|
+
// **The import below has to stay dynamic, and this is the whole reason.** A
|
|
21
|
+
// static `import` is hoisted and its module graph is evaluated *before* any
|
|
22
|
+
// statement in this file runs — so with one, the call above happens after
|
|
23
|
+
// `typescript` has already been compiled and the cache does nothing at all. It
|
|
24
|
+
// still returns success, and every command still works, so there is nothing to
|
|
25
|
+
// notice: measured, the static spelling came back at 873 ms against an
|
|
26
|
+
// unpatched 897 ms, i.e. inside the noise, while the dynamic one is 669 ms.
|
|
27
|
+
// That is this repository's dominant failure shape — a defence that reads as
|
|
28
|
+
// present and is not — arriving in a three-line file.
|
|
29
|
+
//
|
|
30
|
+
// Optional-chained because `engines` allows Node 20 and this API arrived in
|
|
31
|
+
// 22.1: on an older runtime the speed-up is absent, which is the right
|
|
32
|
+
// behaviour for an optimisation and not a reason to refuse to start. It never
|
|
33
|
+
// throws either — a cache directory that cannot be written comes back as a
|
|
34
|
+
// status this deliberately ignores, since the only thing a failure costs is the
|
|
35
|
+
// saving above.
|
|
36
|
+
//
|
|
37
|
+
// The cache lands under the OS temp directory, never in the project: every
|
|
38
|
+
// other file Attest writes is one the user commits (see `core/write.ts`), and
|
|
39
|
+
// this is the one that must not be.
|
|
40
|
+
import module from 'node:module';
|
|
41
|
+
|
|
42
|
+
module.enableCompileCache?.();
|
|
43
|
+
|
|
44
|
+
await import('../dist/cli/index.js');
|
package/dist/cli/report.js
CHANGED
|
@@ -190,7 +190,15 @@ export function formatStatus(result) {
|
|
|
190
190
|
}
|
|
191
191
|
const { proven } = result.counts;
|
|
192
192
|
lines.push(chalk.dim(`— ${plural(result.rows.length, 'added requirement')}, ${proven} ready to archive`));
|
|
193
|
-
|
|
193
|
+
// Two pointers, because this command answers less than a reader assumes on
|
|
194
|
+
// both sides. `archive` is the verdict it deliberately cannot give. `check` is
|
|
195
|
+
// the half it deliberately does not duplicate: a proposed spec no delta
|
|
196
|
+
// claims, or one whose merged name is taken, is a static fact about this
|
|
197
|
+
// change that `check` already reports — and reporting it here too would put a
|
|
198
|
+
// second answer to one question in the tree, which is what this project takes
|
|
199
|
+
// apart everywhere else. Naming it is the whole cost of not absorbing it.
|
|
200
|
+
lines.push(chalk.dim(`Not a verdict: run \`attest archive ${inline(result.change)}\` to run the suite, ` +
|
|
201
|
+
`\`attest check\` for the proposed specs themselves.`));
|
|
194
202
|
return lines.join('\n');
|
|
195
203
|
}
|
|
196
204
|
/**
|
|
@@ -242,10 +250,16 @@ export function formatCoverage(rows) {
|
|
|
242
250
|
const detail = r.covered
|
|
243
251
|
? chalk.dim(`${r.scenarioCount} scenario${r.scenarioCount === 1 ? '' : 's'}`)
|
|
244
252
|
: chalk.red('no scenario');
|
|
245
|
-
// Sanitised
|
|
246
|
-
// `^[A-Z]+-\d
|
|
247
|
-
//
|
|
248
|
-
//
|
|
253
|
+
// Sanitised even though a registry id cannot carry a control character:
|
|
254
|
+
// `^[A-Z]+-\d+$` holds on both reader paths — the static one by
|
|
255
|
+
// construction, the evaluating one since ATX-38 — so this can never fire.
|
|
256
|
+
// It stays because the grammar is held somewhere else, and a defence that
|
|
257
|
+
// lapses when the other one does is not a defence; `CoverageRow.reqId` is
|
|
258
|
+
// a `string`, and this function is exported. Same standard as
|
|
259
|
+
// `splice.ts`'s `keySource`, and the reason it is not an inconsistency
|
|
260
|
+
// with `render.ts` deciding the opposite: that one states it need *not*
|
|
261
|
+
// sanitise ids, which is true, and builds its container prototype-free
|
|
262
|
+
// anyway for exactly this reason.
|
|
249
263
|
return ` ${mark} ${chalk.bold(inline(r.reqId))} ${detail}`;
|
|
250
264
|
})
|
|
251
265
|
.join('\n');
|
package/dist/core/apply.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { RegistryDelta } from './registry.js';
|
|
2
|
-
import type { Issue, Registry } from './types.js';
|
|
2
|
+
import type { Issue, Registry, Requirement } from './types.js';
|
|
3
3
|
export interface ApplyResult {
|
|
4
4
|
registry: Registry;
|
|
5
5
|
issues: Issue[];
|
|
@@ -12,6 +12,14 @@ export interface ApplyResult {
|
|
|
12
12
|
* than off `delta.added`, so the two can never be scoped differently.
|
|
13
13
|
*/
|
|
14
14
|
export declare function addedIds(d: RegistryDelta): string[];
|
|
15
|
+
/**
|
|
16
|
+
* The ids a delta MODIFIEs — the scope of `--apply`'s field-level write-back.
|
|
17
|
+
*
|
|
18
|
+
* Here for the reason `addedIds` is: two places ask, the merge that writes them
|
|
19
|
+
* and the refusal that checks their prefix is owned, and a delta's operations
|
|
20
|
+
* must not be enumerated twice.
|
|
21
|
+
*/
|
|
22
|
+
export declare function modifiedIds(d: RegistryDelta): string[];
|
|
15
23
|
/**
|
|
16
24
|
* The ids a delta **claims**: what it adds, renames to, or modifies.
|
|
17
25
|
*
|
|
@@ -33,4 +41,13 @@ export declare function claimedIds(d: RegistryDelta): string[];
|
|
|
33
41
|
* base registry is never mutated. Applying the same delta twice is a no-op.
|
|
34
42
|
*/
|
|
35
43
|
export declare function applyDelta(base: Registry, d: RegistryDelta): ApplyResult;
|
|
44
|
+
/**
|
|
45
|
+
* Content equality via canonical JSON (params key order does not matter).
|
|
46
|
+
*
|
|
47
|
+
* Exported because `--apply` asks the same question twice — whether a modified
|
|
48
|
+
* entry is already what the end state says, and whether the bytes it wrote read
|
|
49
|
+
* back as it — and a second spelling of "the same requirement" is a second
|
|
50
|
+
* answer the moment `Requirement` gains a field.
|
|
51
|
+
*/
|
|
52
|
+
export declare function sameRequirement(a: Requirement, b: Requirement): boolean;
|
|
36
53
|
//# sourceMappingURL=apply.d.ts.map
|
package/dist/core/apply.js
CHANGED
|
@@ -14,6 +14,16 @@ import { byCodeUnit, sortDeep } from './order.js';
|
|
|
14
14
|
export function addedIds(d) {
|
|
15
15
|
return Object.keys(d.added ?? {});
|
|
16
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* The ids a delta MODIFIEs — the scope of `--apply`'s field-level write-back.
|
|
19
|
+
*
|
|
20
|
+
* Here for the reason `addedIds` is: two places ask, the merge that writes them
|
|
21
|
+
* and the refusal that checks their prefix is owned, and a delta's operations
|
|
22
|
+
* must not be enumerated twice.
|
|
23
|
+
*/
|
|
24
|
+
export function modifiedIds(d) {
|
|
25
|
+
return Object.keys(d.modified ?? {}).sort(byCodeUnit);
|
|
26
|
+
}
|
|
17
27
|
/**
|
|
18
28
|
* The ids a delta **claims**: what it adds, renames to, or modifies.
|
|
19
29
|
*
|
|
@@ -177,8 +187,15 @@ function firstMessage(error) {
|
|
|
177
187
|
const path = first.path.map(String).join('.');
|
|
178
188
|
return path ? `${path}: ${first.message}` : first.message;
|
|
179
189
|
}
|
|
180
|
-
/**
|
|
181
|
-
|
|
190
|
+
/**
|
|
191
|
+
* Content equality via canonical JSON (params key order does not matter).
|
|
192
|
+
*
|
|
193
|
+
* Exported because `--apply` asks the same question twice — whether a modified
|
|
194
|
+
* entry is already what the end state says, and whether the bytes it wrote read
|
|
195
|
+
* back as it — and a second spelling of "the same requirement" is a second
|
|
196
|
+
* answer the moment `Requirement` gains a field.
|
|
197
|
+
*/
|
|
198
|
+
export function sameRequirement(a, b) {
|
|
182
199
|
return canonical(a) === canonical(b);
|
|
183
200
|
}
|
|
184
201
|
function canonical(req) {
|
package/dist/core/locate.d.ts
CHANGED
|
@@ -27,6 +27,20 @@ export declare const isSpecFile: (name: string) => boolean;
|
|
|
27
27
|
* serial `for await` spent one filesystem round-trip per directory, which is
|
|
28
28
|
* the dominant cost of `check` on a large repo. The result is sorted, so the
|
|
29
29
|
* order never depends on which `readdir` happened to resolve first.
|
|
30
|
+
*
|
|
31
|
+
* Concurrency is bounded, for the reason `parseSpecs` bounds its own: the input
|
|
32
|
+
* size is not ours to choose, `check` being what this project tells people to
|
|
33
|
+
* run first on an untrusted fork MR. What must not come back is a fan-out whose
|
|
34
|
+
* peak is the shape of the tree rather than a constant — which is what
|
|
35
|
+
* recursing through `Promise.all(subdirs)` gave, and what
|
|
36
|
+
* `tests/locate-fanout.spec.ts` pins.
|
|
37
|
+
*
|
|
38
|
+
* A level at a time, rather than one pool over a queue that grows as directories
|
|
39
|
+
* are discovered: the pool would have to keep workers alive while the queue is
|
|
40
|
+
* momentarily empty but another worker may still push to it, and that
|
|
41
|
+
* termination condition is the part worth not owning. The cost is a barrier per
|
|
42
|
+
* depth, which is paid in tree *depth* — small, and bounded by the filesystem —
|
|
43
|
+
* while the fan-out being bounded is paid in tree *width*, which is not.
|
|
30
44
|
*/
|
|
31
45
|
export declare function findFiles(root: string, match: (name: string) => boolean): Promise<string[]>;
|
|
32
46
|
/** The file sets every command needs, collected in one pass. */
|
|
@@ -167,10 +181,11 @@ export declare function idPrefix(id: string): string;
|
|
|
167
181
|
* avoid: input size is not ours to choose here, `check` being what this project
|
|
168
182
|
* tells people to run first on an untrusted fork MR. Measured in `[0.7.0]`.
|
|
169
183
|
*
|
|
170
|
-
* `findFiles` above
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
184
|
+
* `findFiles` above bounds its own fan-out the same way and through the same
|
|
185
|
+
* helper. It was left unbounded when this one was capped, on the grounds that
|
|
186
|
+
* the failure it invited — descriptor exhaustion — could not be produced on
|
|
187
|
+
* either development platform; what closed it is that the peak itself is
|
|
188
|
+
* portable arithmetic, which is the standard this half was accepted on.
|
|
174
189
|
*/
|
|
175
190
|
export declare function parseSpecs(files: string[], displayRoot: string): Promise<{
|
|
176
191
|
plan: AttestPlan;
|
package/dist/core/locate.js
CHANGED
|
@@ -40,6 +40,49 @@ export const isProposedSpecFile = (name) => name.endsWith('.proposed.spec.ts');
|
|
|
40
40
|
* of them until its gate passes.
|
|
41
41
|
*/
|
|
42
42
|
export const isSpecFile = (name) => name.endsWith('.spec.ts') && !isProposedSpecFile(name);
|
|
43
|
+
/**
|
|
44
|
+
* Run `fn` over every item with at most `limit` of them in flight.
|
|
45
|
+
*
|
|
46
|
+
* Extracted at the second call site rather than the first, because a shape
|
|
47
|
+
* written N times is one a later fix reaches only some copies of.
|
|
48
|
+
*
|
|
49
|
+
* **There are three, and the third is the one to look for.** `findFiles` and
|
|
50
|
+
* `parseSpecs` are named for what they fan out over; `loadRegistry` is named for
|
|
51
|
+
* merging, and its concurrency sits under a comment about *ordering* that
|
|
52
|
+
* answers a different question convincingly. Anything here that reads a list the
|
|
53
|
+
* project's tree decides the length of belongs in this pool, whatever the
|
|
54
|
+
* function around it is called.
|
|
55
|
+
*
|
|
56
|
+
* The order `fn` is *called* in is the input order; the order it *completes* in
|
|
57
|
+
* is not, so a caller that needs a stable result either indexes into a
|
|
58
|
+
* preallocated array by `index` or sorts afterwards. All three callers here do
|
|
59
|
+
* one of those, deliberately.
|
|
60
|
+
*
|
|
61
|
+
* No result is collected and none is needed — every caller writes into something
|
|
62
|
+
* it already owns, and a version returning `T[]` would have to choose an
|
|
63
|
+
* ordering on their behalf. A throw from `fn` propagates and abandons the rest,
|
|
64
|
+
* which is the existing behaviour at all three sites: `parseSpecs` and
|
|
65
|
+
* `loadRegistry` catch per file so that one hostile source scraps only itself
|
|
66
|
+
* (ATX-65), and a failed `readdir` really does end the walk.
|
|
67
|
+
*/
|
|
68
|
+
async function forEachBounded(items, limit, fn) {
|
|
69
|
+
let cursor = 0;
|
|
70
|
+
const worker = async () => {
|
|
71
|
+
for (let i = cursor++; i < items.length; i = cursor++) {
|
|
72
|
+
await fn(items[i], i);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* How many directories are open at once during the walk. The same figure as
|
|
79
|
+
* `PARSE_CONCURRENCY` and deliberately not the same constant: the two bound
|
|
80
|
+
* different resources, and sharing one would make either impossible to tune
|
|
81
|
+
* without moving the other. High enough that the walk stays I/O bound on any
|
|
82
|
+
* real project, low enough that what is in flight is a constant rather than the
|
|
83
|
+
* shape of the input.
|
|
84
|
+
*/
|
|
85
|
+
const WALK_CONCURRENCY = 32;
|
|
43
86
|
/**
|
|
44
87
|
* Recursively find files under root whose basename matches `match`.
|
|
45
88
|
*
|
|
@@ -47,25 +90,41 @@ export const isSpecFile = (name) => name.endsWith('.spec.ts') && !isProposedSpec
|
|
|
47
90
|
* serial `for await` spent one filesystem round-trip per directory, which is
|
|
48
91
|
* the dominant cost of `check` on a large repo. The result is sorted, so the
|
|
49
92
|
* order never depends on which `readdir` happened to resolve first.
|
|
93
|
+
*
|
|
94
|
+
* Concurrency is bounded, for the reason `parseSpecs` bounds its own: the input
|
|
95
|
+
* size is not ours to choose, `check` being what this project tells people to
|
|
96
|
+
* run first on an untrusted fork MR. What must not come back is a fan-out whose
|
|
97
|
+
* peak is the shape of the tree rather than a constant — which is what
|
|
98
|
+
* recursing through `Promise.all(subdirs)` gave, and what
|
|
99
|
+
* `tests/locate-fanout.spec.ts` pins.
|
|
100
|
+
*
|
|
101
|
+
* A level at a time, rather than one pool over a queue that grows as directories
|
|
102
|
+
* are discovered: the pool would have to keep workers alive while the queue is
|
|
103
|
+
* momentarily empty but another worker may still push to it, and that
|
|
104
|
+
* termination condition is the part worth not owning. The cost is a barrier per
|
|
105
|
+
* depth, which is paid in tree *depth* — small, and bounded by the filesystem —
|
|
106
|
+
* while the fan-out being bounded is paid in tree *width*, which is not.
|
|
50
107
|
*/
|
|
51
108
|
export async function findFiles(root, match) {
|
|
52
109
|
const out = [];
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
110
|
+
let level = [root];
|
|
111
|
+
while (level.length > 0) {
|
|
112
|
+
const next = [];
|
|
113
|
+
await forEachBounded(level, WALK_CONCURRENCY, async (dir) => {
|
|
114
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
115
|
+
for (const e of entries) {
|
|
116
|
+
const full = join(dir, e.name);
|
|
117
|
+
if (e.isDirectory()) {
|
|
118
|
+
if (!SKIP_DIRS.has(e.name))
|
|
119
|
+
next.push(full);
|
|
120
|
+
}
|
|
121
|
+
else if (e.isFile() && match(e.name)) {
|
|
122
|
+
out.push(full);
|
|
123
|
+
}
|
|
61
124
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
await Promise.all(subdirs.map(walk));
|
|
67
|
-
};
|
|
68
|
-
await walk(root);
|
|
125
|
+
});
|
|
126
|
+
level = next;
|
|
127
|
+
}
|
|
69
128
|
return out.sort(byCodeUnit);
|
|
70
129
|
}
|
|
71
130
|
/**
|
|
@@ -245,6 +304,14 @@ async function declaredIds(absPath) {
|
|
|
245
304
|
return [];
|
|
246
305
|
}
|
|
247
306
|
}
|
|
307
|
+
/**
|
|
308
|
+
* How many registry files are read at once. A third figure equal to the other
|
|
309
|
+
* two and, for the reason `WALK_CONCURRENCY` gives, a third constant: what this
|
|
310
|
+
* bounds is neither directories nor spec sources but whatever the *reader*
|
|
311
|
+
* holds — a source and a `SourceFile` under `staticReader`, a Vite SSR module
|
|
312
|
+
* evaluation under `evalReader` — and those are not tunable together.
|
|
313
|
+
*/
|
|
314
|
+
const REGISTRY_CONCURRENCY = 32;
|
|
248
315
|
/**
|
|
249
316
|
* Read and merge every `*.reqs.ts` registry under root, with the given reader.
|
|
250
317
|
* A registry the reader rejects becomes its own ERROR; a duplicate id across
|
|
@@ -263,7 +330,16 @@ export async function loadRegistry(root, reader, files) {
|
|
|
263
330
|
const paths = files ?? (await scanProject(root)).reqsFiles;
|
|
264
331
|
// Read the files concurrently, then fold the results in sorted file order:
|
|
265
332
|
// the issue list stays deterministic regardless of which one finished first.
|
|
266
|
-
|
|
333
|
+
//
|
|
334
|
+
// Indexed rather than appended, exactly as `parseSpecs` does it and for the
|
|
335
|
+
// same reason: `forEachBounded` *calls* in input order and *completes* in
|
|
336
|
+
// whatever order the reads finish, so the fold below is driven by position.
|
|
337
|
+
const outcomes = new Array(paths.length);
|
|
338
|
+
await forEachBounded(paths, REGISTRY_CONCURRENCY, async (file, i) => {
|
|
339
|
+
// `readGuarded` catches, so nothing here can throw and abandon the rest —
|
|
340
|
+
// which is the one thing the pool does not do for its caller.
|
|
341
|
+
outcomes[i] = await readGuarded(reader, file);
|
|
342
|
+
});
|
|
267
343
|
const registry = {};
|
|
268
344
|
const issues = [];
|
|
269
345
|
const unreadable = [];
|
|
@@ -277,9 +353,10 @@ export async function loadRegistry(root, reader, files) {
|
|
|
277
353
|
// have to turn a display path back into one; the messages below relativise at
|
|
278
354
|
// the point they are built, which is where the reader's path belongs anyway.
|
|
279
355
|
const prefixOwner = new Map();
|
|
280
|
-
for (
|
|
281
|
-
const
|
|
282
|
-
const
|
|
356
|
+
for (let i = 0; i < paths.length; i += 1) {
|
|
357
|
+
const file = paths[i];
|
|
358
|
+
const outcome = outcomes[i];
|
|
359
|
+
const display = relativePath(root, file);
|
|
283
360
|
if ('issue' in outcome) {
|
|
284
361
|
issues.push({ ...outcome.issue, file: display });
|
|
285
362
|
// Read the source again for the ids alone. Both readers land here, and the
|
|
@@ -288,7 +365,7 @@ export async function loadRegistry(root, reader, files) {
|
|
|
288
365
|
// is already an ERROR is not a cost worth arranging around, and sharing
|
|
289
366
|
// the reader's source instead would mean the two readers passing different
|
|
290
367
|
// things to one diagnostic.
|
|
291
|
-
unreadable.push({ file: display, ids: await declaredIds(
|
|
368
|
+
unreadable.push({ file: display, ids: await declaredIds(file) });
|
|
292
369
|
continue;
|
|
293
370
|
}
|
|
294
371
|
// One issue per colliding prefix rather than per requirement: the fact is
|
|
@@ -299,9 +376,9 @@ export async function loadRegistry(root, reader, files) {
|
|
|
299
376
|
const prefix = idPrefix(id);
|
|
300
377
|
const owner = prefixOwner.get(prefix);
|
|
301
378
|
if (owner === undefined) {
|
|
302
|
-
prefixOwner.set(prefix,
|
|
379
|
+
prefixOwner.set(prefix, file);
|
|
303
380
|
}
|
|
304
|
-
else if (owner !==
|
|
381
|
+
else if (owner !== file && !reported.has(prefix)) {
|
|
305
382
|
reported.add(prefix);
|
|
306
383
|
// No `reqId`: this is about two files, not about any one of the
|
|
307
384
|
// requirements that happen to reveal it.
|
|
@@ -362,10 +439,11 @@ const PARSE_CONCURRENCY = 32;
|
|
|
362
439
|
* avoid: input size is not ours to choose here, `check` being what this project
|
|
363
440
|
* tells people to run first on an untrusted fork MR. Measured in `[0.7.0]`.
|
|
364
441
|
*
|
|
365
|
-
* `findFiles` above
|
|
366
|
-
*
|
|
367
|
-
*
|
|
368
|
-
*
|
|
442
|
+
* `findFiles` above bounds its own fan-out the same way and through the same
|
|
443
|
+
* helper. It was left unbounded when this one was capped, on the grounds that
|
|
444
|
+
* the failure it invited — descriptor exhaustion — could not be produced on
|
|
445
|
+
* either development platform; what closed it is that the peak itself is
|
|
446
|
+
* portable arithmetic, which is the standard this half was accepted on.
|
|
369
447
|
*/
|
|
370
448
|
export async function parseSpecs(files, displayRoot) {
|
|
371
449
|
// Indexed rather than appended, so the merge below follows the input order
|
|
@@ -374,24 +452,19 @@ export async function parseSpecs(files, displayRoot) {
|
|
|
374
452
|
// Same index space, so a file contributes either a parse or an issue and the
|
|
375
453
|
// two lists cannot disagree about which file is which.
|
|
376
454
|
const failures = new Array(files.length);
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
// the command with it. A hostile spec scraps only itself (ATX-65).
|
|
385
|
-
try {
|
|
386
|
-
parsed[i] = parseSpecFile(display, await readFile(file, 'utf8'));
|
|
387
|
-
}
|
|
388
|
-
catch (err) {
|
|
389
|
-
parsed[i] = { scenarios: [], paramRefs: [] };
|
|
390
|
-
failures[i] = { ...unreadableIssue(err), file: display };
|
|
391
|
-
}
|
|
455
|
+
await forEachBounded(files, PARSE_CONCURRENCY, async (file, i) => {
|
|
456
|
+
const display = relativePath(displayRoot, file);
|
|
457
|
+
// Per file, for the reason `readGuarded` exists above: these run
|
|
458
|
+
// concurrently, so one throw would abandon the rest and take the command
|
|
459
|
+
// with it. A hostile spec scraps only itself (ATX-65).
|
|
460
|
+
try {
|
|
461
|
+
parsed[i] = parseSpecFile(display, await readFile(file, 'utf8'));
|
|
392
462
|
}
|
|
393
|
-
|
|
394
|
-
|
|
463
|
+
catch (err) {
|
|
464
|
+
parsed[i] = { scenarios: [], paramRefs: [] };
|
|
465
|
+
failures[i] = { ...unreadableIssue(err), file: display };
|
|
466
|
+
}
|
|
467
|
+
});
|
|
395
468
|
const plan = { scenarios: [], paramRefs: [] };
|
|
396
469
|
for (const one of parsed) {
|
|
397
470
|
plan.scenarios.push(...one.scenarios);
|