@descent-vtt/spec-harness 0.6.0 → 0.8.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +118 -0
- package/README.md +275 -24
- package/dist/audit.d.ts +71 -1
- package/dist/audit.js +140 -17
- package/dist/audit.js.map +1 -1
- package/dist/branch.d.ts +13 -0
- package/dist/branch.js +19 -0
- package/dist/branch.js.map +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +53 -13
- package/dist/cli.js.map +1 -1
- package/dist/commands.js +12 -4
- package/dist/commands.js.map +1 -1
- package/dist/config.js +3 -1
- package/dist/config.js.map +1 -1
- package/dist/configure.d.ts +62 -3
- package/dist/configure.js +121 -5
- package/dist/configure.js.map +1 -1
- package/dist/context.d.ts +23 -7
- package/dist/context.js +69 -21
- package/dist/context.js.map +1 -1
- package/dist/document.d.ts +15 -2
- package/dist/document.js.map +1 -1
- package/dist/formats.d.ts +31 -0
- package/dist/formats.js +127 -0
- package/dist/formats.js.map +1 -0
- package/dist/git.d.ts +6 -0
- package/dist/git.js +8 -0
- package/dist/git.js.map +1 -1
- package/dist/guard.d.ts +8 -0
- package/dist/guard.js +19 -1
- package/dist/guard.js.map +1 -1
- package/dist/host.d.ts +15 -0
- package/dist/host.js +111 -0
- package/dist/host.js.map +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/manifests.d.ts +8 -1
- package/dist/manifests.js +16 -6
- package/dist/manifests.js.map +1 -1
- package/dist/premises.d.ts +3 -0
- package/dist/premises.js +52 -14
- package/dist/premises.js.map +1 -1
- package/dist/reader.js +28 -3
- package/dist/reader.js.map +1 -1
- package/dist/round.d.ts +8 -0
- package/dist/round.js +70 -23
- package/dist/round.js.map +1 -1
- package/dist/server.js +10 -3
- package/dist/server.js.map +1 -1
- package/dist/setup.d.ts +10 -0
- package/dist/setup.js +73 -32
- package/dist/setup.js.map +1 -1
- package/dist/siblings.d.ts +2 -0
- package/dist/siblings.js +1 -1
- package/dist/siblings.js.map +1 -1
- package/dist/signers.d.ts +46 -0
- package/dist/signers.js +180 -0
- package/dist/signers.js.map +1 -0
- package/dist/types.d.ts +7 -0
- package/dist/types.js.map +1 -1
- package/dist/vendor/spec-core/markdown/frontmatter.d.ts +11 -1
- package/dist/vendor/spec-core/markdown/frontmatter.js +20 -3
- package/dist/vendor/spec-core/markdown/frontmatter.js.map +1 -1
- package/dist/vendor/spec-core/pattern/glob.d.ts +85 -0
- package/dist/vendor/spec-core/pattern/glob.js +207 -17
- package/dist/vendor/spec-core/pattern/glob.js.map +1 -1
- package/dist/vendor/spec-core/pattern/index.d.ts +2 -2
- package/dist/vendor/spec-core/pattern/index.js +1 -1
- package/dist/vendor/spec-core/pattern/index.js.map +1 -1
- package/dist/vendor/spec-core/text/index.d.ts +1 -0
- package/dist/vendor/spec-core/text/index.js +1 -0
- package/dist/vendor/spec-core/text/index.js.map +1 -1
- package/dist/vendor/spec-core/text/width.d.ts +30 -0
- package/dist/vendor/spec-core/text/width.js +113 -0
- package/dist/vendor/spec-core/text/width.js.map +1 -0
- package/dist/versions.d.ts +40 -1
- package/dist/versions.js +33 -1
- package/dist/versions.js.map +1 -1
- package/dist/workspace.d.ts +6 -1
- package/dist/workspace.js +17 -6
- package/dist/workspace.js.map +1 -1
- package/package.json +1 -1
- package/skills/close-round/SKILL.md +4 -1
package/dist/premises.js
CHANGED
|
@@ -15,13 +15,18 @@
|
|
|
15
15
|
* one the flag, SPEC_BRIEF or the branch names - is reported as the audit
|
|
16
16
|
* reports it, retired as the round intends, and fails nothing: on the round's
|
|
17
17
|
* branch, a premise that no longer holds is the work being done.
|
|
18
|
+
*
|
|
19
|
+
* A premise spec-guard cannot read is checked by nothing, and is a warning,
|
|
20
|
+
* as it is in the audit: `--strict` fails the run on it.
|
|
18
21
|
*/
|
|
19
|
-
import { isPremise, premiseFinding } from './audit.js';
|
|
22
|
+
import { isPremise, premiseFinding, unreadablePremiseFinding } from './audit.js';
|
|
20
23
|
import { briefIdFromBranch } from './branch.js';
|
|
21
24
|
import { findActive } from './briefs.js';
|
|
25
|
+
import { CONFIG_FILE } from './config.js';
|
|
26
|
+
import { formatFindings } from './formats.js';
|
|
22
27
|
import { createReader } from './reader.js';
|
|
23
28
|
import { briefText } from './round.js';
|
|
24
|
-
import { EXIT_ERROR, EXIT_FAILED, EXIT_OK, json, openWorkspace } from './workspace.js';
|
|
29
|
+
import { EXIT_ERROR, EXIT_FAILED, EXIT_OK, json, openWorkspace, version } from './workspace.js';
|
|
25
30
|
export async function premisesCommand(options, io) {
|
|
26
31
|
const workspace = await openWorkspace(options, io);
|
|
27
32
|
const reader = createReader();
|
|
@@ -31,7 +36,12 @@ export async function premisesCommand(options, io) {
|
|
|
31
36
|
const active = findActive(briefs, { flag: options.brief, environment: io.env['SPEC_BRIEF'], branch: fromBranch });
|
|
32
37
|
const round = active.kind === 'found' ? active.brief.file : null;
|
|
33
38
|
if (live.length === 0) {
|
|
34
|
-
|
|
39
|
+
if (options.format === 'json')
|
|
40
|
+
io.stdout.write(json('premises', { ok: true, checked: 0, findings: [] }));
|
|
41
|
+
else if (options.format === 'pretty')
|
|
42
|
+
io.stdout.write('no live brief\n');
|
|
43
|
+
else
|
|
44
|
+
io.stdout.write(formatFindings(options.format, [], { file: CONFIG_FILE, version: version(), summary: 'no live brief' }));
|
|
35
45
|
return EXIT_OK;
|
|
36
46
|
}
|
|
37
47
|
const answer = await workspace.siblings.json('spec-guard', [...live.map((brief) => brief.file), '--ignore-status', '--json']);
|
|
@@ -43,35 +53,63 @@ export async function premisesCommand(options, io) {
|
|
|
43
53
|
io.stderr.write('spec-harness: spec-guard could not run the briefs\' assertions\n');
|
|
44
54
|
return EXIT_ERROR;
|
|
45
55
|
}
|
|
46
|
-
const
|
|
56
|
+
const document = answer.document;
|
|
57
|
+
// A missing list is an empty one. A list that holds a string instead, the
|
|
58
|
+
// mutant, sorts nothing to a brief either, since a string has no spec, so
|
|
59
|
+
// it is equivalent; so is the one for errors.
|
|
60
|
+
const results = document.results ?? [];
|
|
61
|
+
// A directive spec-guard cannot read is listed apart from the results; one
|
|
62
|
+
// in a premise section is a premise nothing checks.
|
|
63
|
+
const errors = document.errors ?? [];
|
|
64
|
+
// A directive is a brief's when spec-guard places it in the brief's file;
|
|
65
|
+
// one it places nowhere is no brief's.
|
|
66
|
+
const inBrief = (item, file) => item.spec?.file.replace(/\\/g, '/') === file;
|
|
47
67
|
const findings = [];
|
|
48
68
|
let premises = 0;
|
|
49
69
|
for (const brief of live) {
|
|
50
|
-
const own = results.filter((result) => result
|
|
51
|
-
|
|
70
|
+
const own = results.filter((result) => inBrief(result, brief.file));
|
|
71
|
+
const unread = errors.filter((error) => inBrief(error, brief.file));
|
|
72
|
+
// Only saves reading a brief with nothing to judge: going on finds nothing, so that mutant is equivalent.
|
|
73
|
+
if (own.length === 0 && unread.length === 0)
|
|
52
74
|
continue;
|
|
53
75
|
const text = await briefText(workspace, brief);
|
|
76
|
+
const premise = (line) => isPremise(reader.sectionsAt(text, line), workspace.config.assertions.premises);
|
|
54
77
|
for (const result of own) {
|
|
55
|
-
const line = result.spec
|
|
56
|
-
if (!
|
|
78
|
+
const { line } = result.spec;
|
|
79
|
+
if (!premise(line))
|
|
57
80
|
continue;
|
|
58
81
|
premises += 1;
|
|
59
82
|
if (result.ok)
|
|
60
83
|
continue;
|
|
61
84
|
findings.push(premiseFinding(brief, { description: result.description, message: result.message, line }, brief.file === round));
|
|
62
85
|
}
|
|
86
|
+
for (const error of unread) {
|
|
87
|
+
const { line } = error.spec;
|
|
88
|
+
if (premise(line))
|
|
89
|
+
findings.push(unreadablePremiseFinding(brief, { message: error.message, line, raw: error.raw ?? '' }));
|
|
90
|
+
}
|
|
63
91
|
}
|
|
64
92
|
const stale = findings.filter((finding) => finding.severity === 'error').length;
|
|
65
|
-
const retired = findings.
|
|
93
|
+
const retired = findings.filter((finding) => finding.severity === 'note').length;
|
|
94
|
+
const unreadable = findings.filter((finding) => finding.severity === 'warning').length;
|
|
95
|
+
// Only the round on the active brief retires a premise, so with one retired
|
|
96
|
+
// there is an active brief, and dropping the second test is equivalent.
|
|
97
|
+
const intended = retired === 0 || active.kind !== 'found' ? '' : `, and ${retired} retired by the round on brief ${active.brief.id}, as it intends`;
|
|
98
|
+
const unchecked = unreadable === 0 ? '' : `; ${unreadable} premise(s) spec-guard cannot read, so not checked`;
|
|
99
|
+
const summary = `${premises} premise(s) in ${live.length} live brief(s), ${stale} no longer hold${intended}${unchecked}`;
|
|
100
|
+
// As the audit does: a warning fails the run under --strict.
|
|
101
|
+
const failed = stale > 0 || (options.strict && unreadable > 0);
|
|
66
102
|
if (options.format === 'json') {
|
|
67
|
-
io.stdout.write(json('premises', { ok:
|
|
103
|
+
io.stdout.write(json('premises', { ok: !failed, briefs: live.length, premises, unreadable, findings }));
|
|
68
104
|
}
|
|
69
|
-
else {
|
|
105
|
+
else if (options.format === 'pretty') {
|
|
70
106
|
for (const finding of findings)
|
|
71
107
|
io.stdout.write(`${finding.severity.padEnd(8)} ${finding.file}:${finding.line} ${finding.message}\n ${finding.hint}\n`);
|
|
72
|
-
|
|
73
|
-
|
|
108
|
+
io.stdout.write(`${summary}\n`);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
io.stdout.write(formatFindings(options.format, findings, { file: CONFIG_FILE, version: version(), summary }));
|
|
74
112
|
}
|
|
75
|
-
return
|
|
113
|
+
return failed ? EXIT_FAILED : EXIT_OK;
|
|
76
114
|
}
|
|
77
115
|
//# sourceMappingURL=premises.js.map
|
package/dist/premises.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"premises.js","sourceRoot":"","sources":["../src/premises.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `spec-harness premises`: is every live brief still about something true?\n *\n * A brief written against a defect states the defect as a premise - an\n * assertion under a section such as \"The Defect, Measured\" that holds while\n * the defect does. When the premise stops holding before the round has run,\n * the brief is stale: someone fixed the defect another way, or the code it\n * describes is gone. A repository that keeps its premises executable can say\n * so on every build, and ask for the brief to be archived or rewritten before\n * an agent is sent to fix what is already fixed.\n *\n * Goals are the audit's (`audit`), not this: a goal fails until its round is\n * done, and a build that failed for that would fail for every brief in flight.\n * For the same reason the premise of the brief a round is working on - the\n * one the flag, SPEC_BRIEF or the branch names - is reported as the audit\n * reports it, retired as the round intends, and fails nothing: on the round's\n * branch, a premise that no longer holds is the work being done.\n */\n\nimport { isPremise, premiseFinding } from './audit.js';\nimport { briefIdFromBranch } from './branch.js';\nimport { findActive } from './briefs.js';\nimport { createReader } from './reader.js';\nimport { briefText } from './round.js';\nimport type { Finding } from './types.js';\nimport { EXIT_ERROR, EXIT_FAILED, EXIT_OK, json, openWorkspace, type CliIO, type Options } from './workspace.js';\n\ninterface GuardResult {\n readonly ok: boolean;\n readonly description: string;\n readonly message: string;\n readonly spec?: { readonly file: string; readonly line: number };\n}\n\nexport async function premisesCommand(options: Options, io: CliIO): Promise<number> {\n const workspace = await openWorkspace(options, io);\n const reader = createReader();\n const briefs = await workspace.siblings.briefs();\n const live = briefs.filter((brief) => brief.phase === 'live');\n const fromBranch = workspace.branch === null ? null : briefIdFromBranch(workspace.config.branches, workspace.branch);\n const active = findActive(briefs, { flag: options.brief, environment: io.env['SPEC_BRIEF'], branch: fromBranch });\n const round = active.kind === 'found' ? active.brief.file : null;\n if (live.length === 0) {\n io.stdout.write(options.format === 'json' ? json('premises', { ok: true, checked: 0, findings: [] }) : 'no live brief\\n');\n return EXIT_OK;\n }\n const answer = await workspace.siblings.json('spec-guard', [...live.map((brief) => brief.file), '--ignore-status', '--json']);\n if ('absent' in answer) {\n io.stderr.write(`spec-harness: premises cannot be checked: ${answer.absent}\\n`);\n return EXIT_ERROR;\n }\n if (answer.code === 2) {\n io.stderr.write('spec-harness: spec-guard could not run the briefs\\' assertions\\n');\n return EXIT_ERROR;\n }\n const results = ((answer.document as { results?: GuardResult[] }).results ?? []).filter((result) => result.spec !== undefined);\n const findings: Finding[] = [];\n let premises = 0;\n for (const brief of live) {\n const own = results.filter((result) => result.spec?.file.replace(/\\\\/g, '/') === brief.file);\n if (own.length === 0) continue;\n const text = await briefText(workspace, brief);\n for (const result of own) {\n const line = result.spec?.line ?? 0;\n if (!isPremise(reader.sectionsAt(text, line), workspace.config.assertions.premises)) continue;\n premises += 1;\n if (result.ok) continue;\n findings.push(premiseFinding(brief, { description: result.description, message: result.message, line }, brief.file === round));\n }\n }\n const stale = findings.filter((finding) => finding.severity === 'error').length;\n const retired = findings.length - stale;\n if (options.format === 'json') {\n io.stdout.write(json('premises', { ok: stale === 0, briefs: live.length, premises, findings }));\n } else {\n for (const finding of findings) io.stdout.write(`${finding.severity.padEnd(8)} ${finding.file}:${finding.line} ${finding.message}\\n ${finding.hint}\\n`);\n const intended = retired === 0 || active.kind !== 'found' ? '' : `, and ${retired} retired by the round on brief ${active.brief.id}, as it intends`;\n io.stdout.write(`${premises} premise(s) in ${live.length} live brief(s), ${stale} no longer hold${intended}\\n`);\n }\n return stale > 0 ? EXIT_FAILED : EXIT_OK;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"premises.js","sourceRoot":"","sources":["../src/premises.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `spec-harness premises`: is every live brief still about something true?\n *\n * A brief written against a defect states the defect as a premise - an\n * assertion under a section such as \"The Defect, Measured\" that holds while\n * the defect does. When the premise stops holding before the round has run,\n * the brief is stale: someone fixed the defect another way, or the code it\n * describes is gone. A repository that keeps its premises executable can say\n * so on every build, and ask for the brief to be archived or rewritten before\n * an agent is sent to fix what is already fixed.\n *\n * Goals are the audit's (`audit`), not this: a goal fails until its round is\n * done, and a build that failed for that would fail for every brief in flight.\n * For the same reason the premise of the brief a round is working on - the\n * one the flag, SPEC_BRIEF or the branch names - is reported as the audit\n * reports it, retired as the round intends, and fails nothing: on the round's\n * branch, a premise that no longer holds is the work being done.\n *\n * A premise spec-guard cannot read is checked by nothing, and is a warning,\n * as it is in the audit: `--strict` fails the run on it.\n */\n\nimport { isPremise, premiseFinding, unreadablePremiseFinding } from './audit.js';\nimport { briefIdFromBranch } from './branch.js';\nimport { findActive } from './briefs.js';\nimport { CONFIG_FILE } from './config.js';\nimport { formatFindings } from './formats.js';\nimport { createReader } from './reader.js';\nimport { briefText } from './round.js';\nimport type { Finding } from './types.js';\nimport { EXIT_ERROR, EXIT_FAILED, EXIT_OK, json, openWorkspace, version, type CliIO, type Options } from './workspace.js';\n\n/** Where spec-guard places a directive. */\ninterface Spec {\n readonly file: string;\n readonly line: number;\n}\n\ninterface GuardResult {\n readonly ok: boolean;\n readonly description: string;\n readonly message: string;\n readonly spec?: Spec;\n}\n\ninterface GuardError {\n readonly message: string;\n readonly raw?: string;\n readonly spec?: Spec;\n}\n\nexport async function premisesCommand(options: Options, io: CliIO): Promise<number> {\n const workspace = await openWorkspace(options, io);\n const reader = createReader();\n const briefs = await workspace.siblings.briefs();\n const live = briefs.filter((brief) => brief.phase === 'live');\n const fromBranch = workspace.branch === null ? null : briefIdFromBranch(workspace.config.branches, workspace.branch);\n const active = findActive(briefs, { flag: options.brief, environment: io.env['SPEC_BRIEF'], branch: fromBranch });\n const round = active.kind === 'found' ? active.brief.file : null;\n if (live.length === 0) {\n if (options.format === 'json') io.stdout.write(json('premises', { ok: true, checked: 0, findings: [] }));\n else if (options.format === 'pretty') io.stdout.write('no live brief\\n');\n else io.stdout.write(formatFindings(options.format, [], { file: CONFIG_FILE, version: version(), summary: 'no live brief' }));\n return EXIT_OK;\n }\n const answer = await workspace.siblings.json('spec-guard', [...live.map((brief) => brief.file), '--ignore-status', '--json']);\n if ('absent' in answer) {\n io.stderr.write(`spec-harness: premises cannot be checked: ${answer.absent}\\n`);\n return EXIT_ERROR;\n }\n if (answer.code === 2) {\n io.stderr.write('spec-harness: spec-guard could not run the briefs\\' assertions\\n');\n return EXIT_ERROR;\n }\n const document = answer.document as { results?: GuardResult[]; errors?: GuardError[] };\n // A missing list is an empty one. A list that holds a string instead, the\n // mutant, sorts nothing to a brief either, since a string has no spec, so\n // it is equivalent; so is the one for errors.\n const results = document.results ?? [];\n // A directive spec-guard cannot read is listed apart from the results; one\n // in a premise section is a premise nothing checks.\n const errors = document.errors ?? [];\n // A directive is a brief's when spec-guard places it in the brief's file;\n // one it places nowhere is no brief's.\n const inBrief = <T extends { readonly spec?: Spec }>(item: T, file: string): item is T & { readonly spec: Spec } =>\n item.spec?.file.replace(/\\\\/g, '/') === file;\n const findings: Finding[] = [];\n let premises = 0;\n for (const brief of live) {\n const own = results.filter((result) => inBrief(result, brief.file));\n const unread = errors.filter((error) => inBrief(error, brief.file));\n // Only saves reading a brief with nothing to judge: going on finds nothing, so that mutant is equivalent.\n if (own.length === 0 && unread.length === 0) continue;\n const text = await briefText(workspace, brief);\n const premise = (line: number): boolean => isPremise(reader.sectionsAt(text, line), workspace.config.assertions.premises);\n for (const result of own) {\n const { line } = result.spec;\n if (!premise(line)) continue;\n premises += 1;\n if (result.ok) continue;\n findings.push(premiseFinding(brief, { description: result.description, message: result.message, line }, brief.file === round));\n }\n for (const error of unread) {\n const { line } = error.spec;\n if (premise(line)) findings.push(unreadablePremiseFinding(brief, { message: error.message, line, raw: error.raw ?? '' }));\n }\n }\n const stale = findings.filter((finding) => finding.severity === 'error').length;\n const retired = findings.filter((finding) => finding.severity === 'note').length;\n const unreadable = findings.filter((finding) => finding.severity === 'warning').length;\n // Only the round on the active brief retires a premise, so with one retired\n // there is an active brief, and dropping the second test is equivalent.\n const intended = retired === 0 || active.kind !== 'found' ? '' : `, and ${retired} retired by the round on brief ${active.brief.id}, as it intends`;\n const unchecked = unreadable === 0 ? '' : `; ${unreadable} premise(s) spec-guard cannot read, so not checked`;\n const summary = `${premises} premise(s) in ${live.length} live brief(s), ${stale} no longer hold${intended}${unchecked}`;\n // As the audit does: a warning fails the run under --strict.\n const failed = stale > 0 || (options.strict && unreadable > 0);\n if (options.format === 'json') {\n io.stdout.write(json('premises', { ok: !failed, briefs: live.length, premises, unreadable, findings }));\n } else if (options.format === 'pretty') {\n for (const finding of findings) io.stdout.write(`${finding.severity.padEnd(8)} ${finding.file}:${finding.line} ${finding.message}\\n ${finding.hint}\\n`);\n io.stdout.write(`${summary}\\n`);\n } else {\n io.stdout.write(formatFindings(options.format, findings, { file: CONFIG_FILE, version: version(), summary }));\n }\n return failed ? EXIT_FAILED : EXIT_OK;\n}\n"]}
|
package/dist/reader.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* The document reader, on spec-core's Markdown scanner: nothing inside code
|
|
3
3
|
* or a comment is ever taken for a section, a table, a link or a probe.
|
|
4
4
|
*/
|
|
5
|
-
import { readFrontMatter, scanMarkdown } from './vendor/spec-core/markdown/index.js';
|
|
5
|
+
import { findEntry, readFrontMatter, scanMarkdown } from './vendor/spec-core/markdown/index.js';
|
|
6
|
+
import { splitLines } from './vendor/spec-core/text/index.js';
|
|
6
7
|
/** Section names compare without case, emphasis, a leading number or a trailing colon. */
|
|
7
8
|
export function sameSection(heading, name) {
|
|
8
9
|
const normal = (text) => text
|
|
@@ -13,6 +14,23 @@ export function sameSection(heading, name) {
|
|
|
13
14
|
.toLowerCase();
|
|
14
15
|
return normal(heading) === normal(name);
|
|
15
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* The lines of closed YAML front matter spec-core's reader passes over, since
|
|
19
|
+
* none is `key: value`, each with its reason. The reader reports a problem on
|
|
20
|
+
* a line it read too - a key declared twice - and one on the opening line for
|
|
21
|
+
* TOML front matter; neither is a line passed over, and only these are. A
|
|
22
|
+
* byte-order mark, which the reader leaves out, sits on the opening line,
|
|
23
|
+
* never one of these, so the text's own lines give each as written.
|
|
24
|
+
*/
|
|
25
|
+
function unreadLines(text, front) {
|
|
26
|
+
if (front === null)
|
|
27
|
+
return [];
|
|
28
|
+
const read = new Set(front.entries.map((entry) => entry.line));
|
|
29
|
+
const lines = splitLines(text);
|
|
30
|
+
return front.problems
|
|
31
|
+
.filter((problem) => problem.line > 0 && !read.has(problem.line))
|
|
32
|
+
.map((problem) => ({ line: problem.line + 1, text: lines[problem.line], reason: problem.message }));
|
|
33
|
+
}
|
|
16
34
|
function scanOf(text, cache) {
|
|
17
35
|
return cache !== null && cache.text === text ? cache.scan : scanMarkdown(text);
|
|
18
36
|
}
|
|
@@ -95,12 +113,19 @@ export function createReader() {
|
|
|
95
113
|
const title = scanned.headings.find((heading) => heading.level === 1)?.text ?? null;
|
|
96
114
|
const front = readFrontMatter(text);
|
|
97
115
|
let status = null;
|
|
98
|
-
|
|
116
|
+
// The status is read under `status` alone: a key in another language,
|
|
117
|
+
// such as `狀態`, is another key (ADR-0001).
|
|
118
|
+
const entry = findEntry(front, 'status');
|
|
99
119
|
if (entry !== undefined && entry.value.kind === 'scalar')
|
|
100
120
|
status = entry.value.scalar.text || null;
|
|
101
121
|
// TOML front matter gives no status, closed or not, so closing it would
|
|
102
122
|
// change nothing the context packet shows; only YAML's is worth saying.
|
|
103
|
-
return {
|
|
123
|
+
return {
|
|
124
|
+
title,
|
|
125
|
+
status,
|
|
126
|
+
unclosedFrontMatter: scanned.unclosedFrontMatter?.kind === 'yaml',
|
|
127
|
+
unreadableFrontMatter: unreadLines(text, front),
|
|
128
|
+
};
|
|
104
129
|
},
|
|
105
130
|
};
|
|
106
131
|
}
|
package/dist/reader.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reader.js","sourceRoot":"","sources":["../src/reader.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The document reader, on spec-core's Markdown scanner: nothing inside code\n * or a comment is ever taken for a section, a table, a link or a probe.\n */\n\nimport type { Citation, DocumentReader, SectionTables } from './document.js';\nimport type { CodeBlock } from './probe.js';\nimport type { TableView } from './rulings.js';\nimport { readFrontMatter, scanMarkdown, type MarkdownScan } from './vendor/spec-core/markdown/index.js';\n\n/** Section names compare without case, emphasis, a leading number or a trailing colon. */\nexport function sameSection(heading: string, name: string): boolean {\n const normal = (text: string): string =>\n text\n .replace(/[*_`]/g, '')\n .replace(/^\\s*\\d+[.)]?\\s+/, '')\n .replace(/[:\\s]+$/, '')\n .trim()\n .toLowerCase();\n return normal(heading) === normal(name);\n}\n\nfunction scanOf(text: string, cache: { text: string; scan: MarkdownScan } | null): MarkdownScan {\n return cache !== null && cache.text === text ? cache.scan : scanMarkdown(text);\n}\n\nexport function createReader(): DocumentReader {\n // Commands read the same brief several times in a row; one scan serves them.\n let last: { text: string; scan: MarkdownScan } | null = null;\n const scan = (text: string): MarkdownScan => {\n const result = scanOf(text, last);\n last = { text, scan: result };\n return result;\n };\n\n return {\n codeBlocks(text: string): CodeBlock[] {\n const scanned = scan(text);\n const spaces = (line: string): number => (/^ */.exec(line) as RegExpExecArray)[0].length;\n return scanned.blocks\n .filter((block) => block.kind === 'fenced')\n .map((block) => {\n const first = block.line + 1;\n const last = block.closed ? block.endLine - 1 : block.endLine;\n // A fence in a block quote or a list item: its content is the code\n // without the quote markers and without the fence's own indent, as\n // CommonMark reads it, so a probe file is written as it was meant.\n const indent = spaces(scanned.lines[block.line - 1]?.content ?? '');\n const lines: string[] = [];\n for (let line = first; line <= last; line += 1) {\n const content = scanned.lines[line - 1]?.content ?? '';\n lines.push(content.slice(Math.min(indent, spaces(content))));\n }\n return { info: block.info, content: lines.join('\\n'), line: block.line };\n });\n },\n\n sectionAt(text: string, line: number): string | null {\n let found: string | null = null;\n for (const heading of scan(text).headings) {\n if (heading.line > line) break;\n if (heading.level >= 2) found = heading.text;\n }\n return found;\n },\n\n sectionsAt(text: string, line: number): string[] {\n const open: { level: number; text: string }[] = [];\n for (const heading of scan(text).headings) {\n if (heading.line > line) break;\n if (heading.level < 2) continue;\n while (open.length > 0 && (open[open.length - 1] as { level: number }).level >= heading.level) open.pop();\n open.push({ level: heading.level, text: heading.text });\n }\n return open.map((section) => section.text);\n },\n\n sectionTables(text: string, section: string): SectionTables {\n const scanned = scan(text);\n const headings = scanned.headings;\n const index = headings.findIndex((heading) => heading.level >= 2 && sameSection(heading.text, section));\n if (index < 0) return { tables: [], headingLine: null, tableEndLine: null };\n const heading = headings[index] as (typeof headings)[number];\n const next = headings.slice(index + 1).find((candidate) => candidate.level <= heading.level);\n const endLine = next === undefined ? scanned.index.lineCount + 1 : next.line;\n const tables = scanned.tables.filter((table) => table.line > heading.endLine && table.line < endLine);\n const views: TableView[] = tables.map((table) => ({\n headers: table.headers.map((cell) => cell.text),\n rows: table.rows.map((row) => ({ cells: row.cells.map((cell) => cell.text), line: row.line })),\n }));\n const lastTable = tables[tables.length - 1];\n return { tables: views, headingLine: heading.endLine, tableEndLine: lastTable === undefined ? null : lastTable.endLine };\n },\n\n citations(text: string): Citation[] {\n return scan(text)\n .links.filter((link) => !link.image && link.form !== 'autolink' && link.form !== 'definition')\n .map((link) => ({ target: link.target, line: link.line }));\n },\n\n titleAndStatus(text: string): { title: string | null; status: string | null; unclosedFrontMatter: boolean } {\n const scanned = scan(text);\n const title = scanned.headings.find((heading) => heading.level === 1)?.text ?? null;\n const front = readFrontMatter(text);\n let status: string | null = null;\n const entry = front?.entries.find((candidate) => candidate.name === 'status');\n if (entry !== undefined && entry.value.kind === 'scalar') status = entry.value.scalar.text || null;\n // TOML front matter gives no status, closed or not, so closing it would\n // change nothing the context packet shows; only YAML's is worth saying.\n return { title, status, unclosedFrontMatter: scanned.unclosedFrontMatter?.kind === 'yaml' };\n },\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"reader.js","sourceRoot":"","sources":["../src/reader.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The document reader, on spec-core's Markdown scanner: nothing inside code\n * or a comment is ever taken for a section, a table, a link or a probe.\n */\n\nimport type { Citation, DocumentReader, SectionTables, UnreadableFrontMatterLine } from './document.js';\nimport type { CodeBlock } from './probe.js';\nimport type { TableView } from './rulings.js';\nimport { findEntry, readFrontMatter, scanMarkdown, type FrontMatter, type MarkdownScan } from './vendor/spec-core/markdown/index.js';\nimport { splitLines } from './vendor/spec-core/text/index.js';\n\n/** Section names compare without case, emphasis, a leading number or a trailing colon. */\nexport function sameSection(heading: string, name: string): boolean {\n const normal = (text: string): string =>\n text\n .replace(/[*_`]/g, '')\n .replace(/^\\s*\\d+[.)]?\\s+/, '')\n .replace(/[:\\s]+$/, '')\n .trim()\n .toLowerCase();\n return normal(heading) === normal(name);\n}\n\n/**\n * The lines of closed YAML front matter spec-core's reader passes over, since\n * none is `key: value`, each with its reason. The reader reports a problem on\n * a line it read too - a key declared twice - and one on the opening line for\n * TOML front matter; neither is a line passed over, and only these are. A\n * byte-order mark, which the reader leaves out, sits on the opening line,\n * never one of these, so the text's own lines give each as written.\n */\nfunction unreadLines(text: string, front: FrontMatter | null): UnreadableFrontMatterLine[] {\n if (front === null) return [];\n const read = new Set(front.entries.map((entry) => entry.line));\n const lines = splitLines(text);\n return front.problems\n .filter((problem) => problem.line > 0 && !read.has(problem.line))\n .map((problem) => ({ line: problem.line + 1, text: lines[problem.line] as string, reason: problem.message }));\n}\n\nfunction scanOf(text: string, cache: { text: string; scan: MarkdownScan } | null): MarkdownScan {\n return cache !== null && cache.text === text ? cache.scan : scanMarkdown(text);\n}\n\nexport function createReader(): DocumentReader {\n // Commands read the same brief several times in a row; one scan serves them.\n let last: { text: string; scan: MarkdownScan } | null = null;\n const scan = (text: string): MarkdownScan => {\n const result = scanOf(text, last);\n last = { text, scan: result };\n return result;\n };\n\n return {\n codeBlocks(text: string): CodeBlock[] {\n const scanned = scan(text);\n const spaces = (line: string): number => (/^ */.exec(line) as RegExpExecArray)[0].length;\n return scanned.blocks\n .filter((block) => block.kind === 'fenced')\n .map((block) => {\n const first = block.line + 1;\n const last = block.closed ? block.endLine - 1 : block.endLine;\n // A fence in a block quote or a list item: its content is the code\n // without the quote markers and without the fence's own indent, as\n // CommonMark reads it, so a probe file is written as it was meant.\n const indent = spaces(scanned.lines[block.line - 1]?.content ?? '');\n const lines: string[] = [];\n for (let line = first; line <= last; line += 1) {\n const content = scanned.lines[line - 1]?.content ?? '';\n lines.push(content.slice(Math.min(indent, spaces(content))));\n }\n return { info: block.info, content: lines.join('\\n'), line: block.line };\n });\n },\n\n sectionAt(text: string, line: number): string | null {\n let found: string | null = null;\n for (const heading of scan(text).headings) {\n if (heading.line > line) break;\n if (heading.level >= 2) found = heading.text;\n }\n return found;\n },\n\n sectionsAt(text: string, line: number): string[] {\n const open: { level: number; text: string }[] = [];\n for (const heading of scan(text).headings) {\n if (heading.line > line) break;\n if (heading.level < 2) continue;\n while (open.length > 0 && (open[open.length - 1] as { level: number }).level >= heading.level) open.pop();\n open.push({ level: heading.level, text: heading.text });\n }\n return open.map((section) => section.text);\n },\n\n sectionTables(text: string, section: string): SectionTables {\n const scanned = scan(text);\n const headings = scanned.headings;\n const index = headings.findIndex((heading) => heading.level >= 2 && sameSection(heading.text, section));\n if (index < 0) return { tables: [], headingLine: null, tableEndLine: null };\n const heading = headings[index] as (typeof headings)[number];\n const next = headings.slice(index + 1).find((candidate) => candidate.level <= heading.level);\n const endLine = next === undefined ? scanned.index.lineCount + 1 : next.line;\n const tables = scanned.tables.filter((table) => table.line > heading.endLine && table.line < endLine);\n const views: TableView[] = tables.map((table) => ({\n headers: table.headers.map((cell) => cell.text),\n rows: table.rows.map((row) => ({ cells: row.cells.map((cell) => cell.text), line: row.line })),\n }));\n const lastTable = tables[tables.length - 1];\n return { tables: views, headingLine: heading.endLine, tableEndLine: lastTable === undefined ? null : lastTable.endLine };\n },\n\n citations(text: string): Citation[] {\n return scan(text)\n .links.filter((link) => !link.image && link.form !== 'autolink' && link.form !== 'definition')\n .map((link) => ({ target: link.target, line: link.line }));\n },\n\n titleAndStatus(text: string): {\n title: string | null;\n status: string | null;\n unclosedFrontMatter: boolean;\n unreadableFrontMatter: UnreadableFrontMatterLine[];\n } {\n const scanned = scan(text);\n const title = scanned.headings.find((heading) => heading.level === 1)?.text ?? null;\n const front = readFrontMatter(text);\n let status: string | null = null;\n // The status is read under `status` alone: a key in another language,\n // such as `狀態`, is another key (ADR-0001).\n const entry = findEntry(front, 'status');\n if (entry !== undefined && entry.value.kind === 'scalar') status = entry.value.scalar.text || null;\n // TOML front matter gives no status, closed or not, so closing it would\n // change nothing the context packet shows; only YAML's is worth saying.\n return {\n title,\n status,\n unclosedFrontMatter: scanned.unclosedFrontMatter?.kind === 'yaml',\n unreadableFrontMatter: unreadLines(text, front),\n };\n },\n };\n}\n"]}
|
package/dist/round.d.ts
CHANGED
|
@@ -45,6 +45,14 @@ export declare function briefText(workspace: Workspace, brief: BriefRow): Promis
|
|
|
45
45
|
/** Decides paths against the brief, with the rulings verified against the base `baseFlag` names, or the configured one. */
|
|
46
46
|
export declare function checkPaths(workspace: Workspace, brief: BriefRow | null, noBrief: string | undefined, given: readonly string[], cwd: string, reader: DocumentReader, baseFlag: string | undefined): Promise<Decision[]>;
|
|
47
47
|
export declare function buildContext(workspace: Workspace, brief: BriefRow, briefs: readonly BriefRow[], reader: DocumentReader, baseFlag: string | undefined): Promise<ContextPacket>;
|
|
48
|
+
/**
|
|
49
|
+
* Whether a path spec-brief's `plugins` names, read from the root, is this
|
|
50
|
+
* package's plugin: the file its `./spec-brief-plugin` export names, as the
|
|
51
|
+
* project installed the package, or as this copy of it ships. spec-brief
|
|
52
|
+
* loads a plugin by a path as well as by its name, and a path to the plugin
|
|
53
|
+
* read as another plugin would say the archive cannot accept a ruling it can.
|
|
54
|
+
*/
|
|
55
|
+
export declare function pluginFile(root: string): (path: string) => boolean;
|
|
48
56
|
/** Whether spec-brief loads this package's plugin, as its configuration at the root says. */
|
|
49
57
|
export declare function specBriefPlugin(root: string): Promise<PluginState>;
|
|
50
58
|
/**
|
package/dist/round.js
CHANGED
|
@@ -5,12 +5,13 @@
|
|
|
5
5
|
* `audit.ts`, `guard.ts` and `rulings.ts`.
|
|
6
6
|
*/
|
|
7
7
|
import { createHash } from 'node:crypto';
|
|
8
|
-
import { existsSync } from 'node:fs';
|
|
8
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
9
9
|
import { readdir, readFile, rm } from 'node:fs/promises';
|
|
10
|
-
import { join } from 'node:path';
|
|
11
|
-
import {
|
|
10
|
+
import { join, resolve } from 'node:path';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
import { audit, tally } from './audit.js';
|
|
12
13
|
import { sameId } from './branch.js';
|
|
13
|
-
import { enabledPlugin, holdsGuard, loadsPlugin, registersServer, SPEC_BRIEF_CONFIGS, } from './configure.js';
|
|
14
|
+
import { enabledPlugin, exportedFile, holdsGuard, loadsPlugin, registersServer, SPEC_BRIEF_CONFIGS, } from './configure.js';
|
|
14
15
|
import { readRules, renderContext } from './context.js';
|
|
15
16
|
import { readJsonObject, readText, repositoryPath, stateDirectory, writeAtomic } from './fs.js';
|
|
16
17
|
import { blameLine, changes, mergeBase, remoteDefault, revision, show, verifyCommit } from './git.js';
|
|
@@ -130,8 +131,17 @@ function scopeBases(root, patterns) {
|
|
|
130
131
|
const bases = new Set();
|
|
131
132
|
for (const pattern of patterns) {
|
|
132
133
|
try {
|
|
133
|
-
|
|
134
|
+
// Case is read only in matching, which this compile never does: its
|
|
135
|
+
// bases are the same either way, so `caseSensitive: false` is equivalent.
|
|
136
|
+
for (const base of compileGlob(pattern, { dialect: 'path', caseSensitive: true, literal }).bases) {
|
|
137
|
+
// A rooted alternative's base, `/docs` or `/`, is outside the
|
|
138
|
+
// repository: it puts no path in the scope, and spec-guard refuses
|
|
139
|
+
// the whole question for it, with the rules for every other base.
|
|
140
|
+
// The packet names the pattern.
|
|
141
|
+
if (base.startsWith('/'))
|
|
142
|
+
continue;
|
|
134
143
|
bases.add(base === '' ? '.' : base);
|
|
144
|
+
}
|
|
135
145
|
}
|
|
136
146
|
catch {
|
|
137
147
|
// The packet names a pattern the guard cannot read, with spec-core's
|
|
@@ -220,7 +230,7 @@ async function dependencyChanges(workspace, base) {
|
|
|
220
230
|
// leave its manifests out of every audit measured from one.
|
|
221
231
|
const names = readManifestNames(workspace.config.dependencies.manifests);
|
|
222
232
|
if (base.kind === 'unresolved')
|
|
223
|
-
return { changes: [], unread: [], unreadNames: names.unread };
|
|
233
|
+
return { changes: [], unread: [], unreadNames: names.unread, rootedNames: names.rooted };
|
|
224
234
|
const isManifest = names.match;
|
|
225
235
|
const out = [];
|
|
226
236
|
const unread = [];
|
|
@@ -240,7 +250,47 @@ async function dependencyChanges(workspace, base) {
|
|
|
240
250
|
else
|
|
241
251
|
out.push(...diff.changes);
|
|
242
252
|
}
|
|
243
|
-
return { changes: out, unread, unreadNames: names.unread };
|
|
253
|
+
return { changes: out, unread, unreadNames: names.unread, rootedNames: names.rooted };
|
|
254
|
+
}
|
|
255
|
+
/** A file's real path, links and the filesystem's case resolved, or `null` when there is no such file. */
|
|
256
|
+
function realFile(path) {
|
|
257
|
+
try {
|
|
258
|
+
return realpathSync.native(path);
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Whether a path spec-brief's `plugins` names, read from the root, is this
|
|
266
|
+
* package's plugin: the file its `./spec-brief-plugin` export names, as the
|
|
267
|
+
* project installed the package, or as this copy of it ships. spec-brief
|
|
268
|
+
* loads a plugin by a path as well as by its name, and a path to the plugin
|
|
269
|
+
* read as another plugin would say the archive cannot accept a ruling it can.
|
|
270
|
+
*/
|
|
271
|
+
export function pluginFile(root) {
|
|
272
|
+
const installed = join(root, 'node_modules', '@descent-vtt', 'spec-harness');
|
|
273
|
+
const candidates = [fileURLToPath(new URL('../dist/plugin.js', import.meta.url))];
|
|
274
|
+
try {
|
|
275
|
+
// A manifest with no exports, or none for the plugin, throws in `join`
|
|
276
|
+
// or on the missing key inside this block, which the catch reads as no
|
|
277
|
+
// installed copy: `exports?.` and `target !== null` are equivalent to
|
|
278
|
+
// their mutants here.
|
|
279
|
+
const manifest = JSON.parse(readFileSync(join(installed, 'package.json'), 'utf8'));
|
|
280
|
+
const target = exportedFile(manifest.exports?.['./spec-brief-plugin']);
|
|
281
|
+
if (target !== null)
|
|
282
|
+
candidates.push(join(installed, target));
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
// Not installed at the root, or its manifest cannot be read: this copy's file is the one to compare.
|
|
286
|
+
}
|
|
287
|
+
// A candidate that names no file is `null` here, and so is a path that
|
|
288
|
+
// names none, which is never the plugin.
|
|
289
|
+
const files = new Set(candidates.map(realFile));
|
|
290
|
+
return (path) => {
|
|
291
|
+
const file = realFile(resolve(root, path));
|
|
292
|
+
return file !== null && files.has(file);
|
|
293
|
+
};
|
|
244
294
|
}
|
|
245
295
|
/** Whether spec-brief loads this package's plugin, as its configuration at the root says. */
|
|
246
296
|
export async function specBriefPlugin(root) {
|
|
@@ -250,7 +300,7 @@ export async function specBriefPlugin(root) {
|
|
|
250
300
|
const config = await readJsonObject(join(root, file));
|
|
251
301
|
if (config === null || config === 'unreadable')
|
|
252
302
|
return { kind: 'unreadable', file };
|
|
253
|
-
return { kind: loadsPlugin(config) ? 'loaded' : 'not-loaded', file };
|
|
303
|
+
return { kind: loadsPlugin(config, pluginFile(root)) ? 'loaded' : 'not-loaded', file };
|
|
254
304
|
}
|
|
255
305
|
/**
|
|
256
306
|
* Claude Code's settings files that can turn a plugin on or hold a hook,
|
|
@@ -295,6 +345,7 @@ async function archiveReasons(workspace, brief, base) {
|
|
|
295
345
|
const plan = answer.document.plan;
|
|
296
346
|
return { blocking: plan?.blocking ?? [], warnings: plan?.warnings ?? [] };
|
|
297
347
|
}
|
|
348
|
+
/** The brief's assertions as spec-guard ran them, and those it could not read, which it lists apart as `errors`. */
|
|
298
349
|
async function briefAssertions(workspace, brief, text, reader) {
|
|
299
350
|
const answer = await workspace.siblings.json('spec-guard', [brief.file, '--ignore-status', '--json']);
|
|
300
351
|
if ('absent' in answer)
|
|
@@ -302,12 +353,15 @@ async function briefAssertions(workspace, brief, text, reader) {
|
|
|
302
353
|
if (answer.code === 2)
|
|
303
354
|
return { unavailable: 'spec-guard could not run the brief\'s assertions' };
|
|
304
355
|
const report = answer.document;
|
|
305
|
-
|
|
306
|
-
|
|
356
|
+
const own = (spec) => spec === undefined || spec.file.replace(/\\/g, '/') === brief.file;
|
|
357
|
+
const outcomes = (report.results ?? [])
|
|
358
|
+
.filter((result) => own(result.spec))
|
|
307
359
|
.map((result) => {
|
|
308
360
|
const line = result.spec?.line ?? 0;
|
|
309
361
|
return { ok: result.ok, description: result.description, message: result.message, line, section: reader.sectionAt(text, line), enclosing: reader.sectionsAt(text, line) };
|
|
310
362
|
});
|
|
363
|
+
const unreadable = (report.errors ?? []).filter((error) => own(error.spec)).map((error) => ({ message: error.message, line: error.spec?.line ?? 0, raw: error.raw ?? '' }));
|
|
364
|
+
return { outcomes, unreadable };
|
|
311
365
|
}
|
|
312
366
|
export async function runAudit(workspace, brief, reader, baseFlag) {
|
|
313
367
|
const base = await resolveBase(workspace, baseFlag);
|
|
@@ -325,24 +379,17 @@ export async function runAudit(workspace, brief, reader, baseFlag) {
|
|
|
325
379
|
unmeasured,
|
|
326
380
|
dependencies,
|
|
327
381
|
archive,
|
|
328
|
-
assertions,
|
|
382
|
+
assertions: 'unavailable' in assertions ? assertions : assertions.outcomes,
|
|
383
|
+
// The audit reads no list as an empty one, and a run spec-guard could not
|
|
384
|
+
// make has none, so a mutant of this test reads the same.
|
|
385
|
+
unreadableAssertions: 'unavailable' in assertions ? [] : assertions.unreadable,
|
|
329
386
|
premiseSections: workspace.config.assertions.premises,
|
|
330
387
|
unverifiedRulings: rulings.unverified,
|
|
331
388
|
verifiedRulings: rulings.verified,
|
|
332
389
|
pluginLoaded: plugin.kind === 'loaded',
|
|
333
390
|
});
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
base,
|
|
337
|
-
dependencies: dependencies.changes,
|
|
338
|
-
report: { findings: [...rulings.problems, ...report.findings], counts: tally([...rulings.problems, ...report.findings]) },
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
function tally(findings) {
|
|
342
|
-
const counts = { error: 0, warning: 0, note: 0 };
|
|
343
|
-
for (const finding of findings)
|
|
344
|
-
counts[finding.severity] += 1;
|
|
345
|
-
return counts;
|
|
391
|
+
const findings = [...rulings.problems, ...report.findings];
|
|
392
|
+
return { brief, base, dependencies: dependencies.changes, report: { findings, counts: tally(findings), measured: report.measured } };
|
|
346
393
|
}
|
|
347
394
|
/* -------------------------------------------------------------- escalation */
|
|
348
395
|
async function escalationDirectory(workspace) {
|