octwin-cli 0.8.5 → 0.8.6
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 +25 -0
- package/dist/index.js +153 -149
- package/dist/lib/outcome.js +79 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,31 @@ Format: [Keep a Changelog](https://keepachangelog.com/) — newest first, bucket
|
|
|
5
5
|
**Added · Changed · Deprecated · Removed · Fixed · Security**. The platform-wide view lives in the
|
|
6
6
|
repo root [`CHANGELOG.md`](../../CHANGELOG.md); this file is the CLI-only cut that ships with the package.
|
|
7
7
|
|
|
8
|
+
## [0.8.6] - 2026-09-04
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- **`validate` can no longer print a `✓` for a check that did not run.** Each of the eight
|
|
12
|
+
checks now RETURNS an `Outcome` — `passed`, `findings`, or `not-run` — and a single printer
|
|
13
|
+
renders all of them, groups the skips by reason, and computes the exit code. Previously each
|
|
14
|
+
check printed its own `✓` inside the `if` that held its data, with a `skipped[]` array and a
|
|
15
|
+
`skipReasons` Map maintained alongside; it was correct because someone was careful, and the
|
|
16
|
+
next check added was one `if` away from lying again.
|
|
17
|
+
|
|
18
|
+
It had lied twice, in the same shape: the KB-dependent block once printed ONE `✓` above six
|
|
19
|
+
checks whose own `✓`s lived inside their `if`s (so a run with no pulled KB read as "one check,
|
|
20
|
+
passed", and an entire backlog batch reached production that way), and `yamlDocs()` once
|
|
21
|
+
dropped an unparseable file so `validate` printed all-green over a pack the platform's importer
|
|
22
|
+
then refused. **An unparseable file is not "no findings", it is "no idea", and those must never
|
|
23
|
+
print the same** — a check that returns nothing now cannot reach the `passed` branch at all.
|
|
24
|
+
|
|
25
|
+
Output is unchanged for a normal run. `passed` carries its own line, so the reserved-entity
|
|
26
|
+
check keeps the narrower wording it uses when the catalog rules are absent from an older pull.
|
|
27
|
+
`--require-kb` is now one line over the returned skip list.
|
|
28
|
+
|
|
29
|
+
Verified 2026-09-04: all 23 marketplace packs exit 0 with no findings; a pack with no pulled KB
|
|
30
|
+
prints zero `✓` for the six KB-dependent checks plus one grouped ⚠ naming all six, and
|
|
31
|
+
`--require-kb` exits 1; a duplicate YAML key still exits 1 naming the file.
|
|
32
|
+
|
|
8
33
|
## [0.8.5] - 2026-09-02
|
|
9
34
|
|
|
10
35
|
### Added
|
package/dist/index.js
CHANGED
|
@@ -73,7 +73,8 @@ import { loadBuiltinNames, findBuiltinViolations, describeBuiltinFinding } from
|
|
|
73
73
|
import { loadTemplateSpecs, findTemplateViolations, describeTemplateFinding } from './lib/template-check.js';
|
|
74
74
|
import { loadSystemEntities, findEntityViolations, describeEntityFinding } from './lib/entity-check.js';
|
|
75
75
|
import { loadDeclarationSpecs, findDeclarationViolations, describeDeclarationFinding } from './lib/declaration-check.js';
|
|
76
|
-
import {
|
|
76
|
+
import { findPlatformKbDir } from './lib/kb-path.js';
|
|
77
|
+
import { reportChecks } from './lib/outcome.js';
|
|
77
78
|
import { classifyPackPath, isSkippedDir } from './lib/pack-source.js';
|
|
78
79
|
import { readPage, morePageHint } from './lib/page.js';
|
|
79
80
|
import { kbOneLiner, buildKbIndexMarkdown, buildKbOutlineMarkdown, } from './lib/kb-index.js';
|
|
@@ -976,158 +977,161 @@ async function cmdValidate(flags) {
|
|
|
976
977
|
const yamlDocs = () => parsed;
|
|
977
978
|
// Checks that need the pulled KB. All of them DEGRADE when it is absent — the KB
|
|
978
979
|
// is a gitignored cache wiped by every pull, so failing hard would break a fresh
|
|
979
|
-
// clone before the author could act. But a skip is ANNOUNCED, and remembered
|
|
980
|
-
// the ✓ used to print above these blocks unconditionally while the per-check ✓s
|
|
981
|
-
// lived inside the `if`s, so a KB-less run read as "one check, passed". An entire
|
|
982
|
-
// backlog batch reached production that way. The defect is the silence, not the skip.
|
|
980
|
+
// clone before the author could act. But a skip is ANNOUNCED, and remembered.
|
|
983
981
|
//
|
|
984
|
-
//
|
|
985
|
-
//
|
|
986
|
-
//
|
|
987
|
-
//
|
|
988
|
-
|
|
989
|
-
const skipReasons = new Map();
|
|
990
|
-
const noteSkip = (label, lookup) => {
|
|
991
|
-
skipped.push(label);
|
|
992
|
-
// Group by the lookup's IDENTITY, then let `describeKbLookup` phrase the one
|
|
993
|
-
// line at print time — so the wording stays in the module that owns it and
|
|
994
|
-
// cannot drift into a doubled "SKIPPED — SKIPPED —".
|
|
995
|
-
const key = lookup.state === 'ok' ? 'ok'
|
|
996
|
-
: `${lookup.state}|${'dir' in lookup ? lookup.dir : ''}|${'reason' in lookup ? lookup.reason : ''}`;
|
|
997
|
-
const bucket = skipReasons.get(key) ?? { lookup, labels: [] };
|
|
998
|
-
bucket.labels.push(label);
|
|
999
|
-
skipReasons.set(key, bucket);
|
|
1000
|
-
};
|
|
982
|
+
// Each check RETURNS an `Outcome` and prints nothing; `reportChecks` renders all of
|
|
983
|
+
// them and groups the skips by reason. That is the whole point: a check that returns
|
|
984
|
+
// nothing cannot reach the `passed` branch, so the false-`✓` this file printed twice
|
|
985
|
+
// before — once for six checks at a time, once over an unparseable file — is no longer
|
|
986
|
+
// a mistake anyone can make. See [`lib/outcome.ts`](./lib/outcome.ts).
|
|
1001
987
|
/** Stamp each finding with its source line — the walkers carry the node
|
|
1002
988
|
* path; `files` holds the raw text the locator needs (E-08). */
|
|
1003
989
|
const withLines = (fs) => fs.map(f => ({ ...f, line: files[f.file] ? yamlLineOf(files[f.file], f.path) : null }));
|
|
1004
|
-
const
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
990
|
+
const checks = [
|
|
991
|
+
{
|
|
992
|
+
label: 'render-intent fields',
|
|
993
|
+
run: () => {
|
|
994
|
+
const render = loadAllowedRenderKeys(packDir);
|
|
995
|
+
if (!render.keys)
|
|
996
|
+
return { kind: 'not-run', lookup: render.lookup };
|
|
997
|
+
const findings = withLines(yamlDocs().flatMap(([p, doc]) => findRenderKeyViolations(doc, p, render.keys, render.nested)));
|
|
998
|
+
return findings.length
|
|
999
|
+
? {
|
|
1000
|
+
kind: 'findings', noun: 'render-intent field error',
|
|
1001
|
+
lines: findings.map(describeRenderFinding),
|
|
1002
|
+
hint: 'fix these before deploying — the platform rejects them at load, and before that they rendered as nothing',
|
|
1003
|
+
}
|
|
1004
|
+
: { kind: 'passed', line: 'render intents use only fields the platform renders' };
|
|
1005
|
+
},
|
|
1006
|
+
},
|
|
1007
|
+
{
|
|
1008
|
+
// Primitive `args:` keys, same source and same contract. Cannot see inside a
|
|
1009
|
+
// `use:` template body (expansion is the platform's job); `--remote` covers that.
|
|
1010
|
+
label: 'primitive arguments',
|
|
1011
|
+
run: () => {
|
|
1012
|
+
const args = loadPrimitiveArgSpecs(packDir);
|
|
1013
|
+
if (!args.specs)
|
|
1014
|
+
return { kind: 'not-run', lookup: args.lookup };
|
|
1015
|
+
const findings = withLines(yamlDocs().flatMap(([p, doc]) => findArgViolations(doc, p, args.specs)));
|
|
1016
|
+
return findings.length
|
|
1017
|
+
? {
|
|
1018
|
+
kind: 'findings', noun: 'primitive-argument error',
|
|
1019
|
+
lines: findings.map(describeArgFinding),
|
|
1020
|
+
hint: 'fix these before deploying — an undeclared argument is dropped with no error at runtime',
|
|
1021
|
+
}
|
|
1022
|
+
: { kind: 'passed', line: 'primitive arguments match their declared inputs' };
|
|
1023
|
+
},
|
|
1024
|
+
},
|
|
1025
|
+
{
|
|
1026
|
+
// Expression builtins — the function set is CLOSED and generated from the
|
|
1027
|
+
// runtime, so an invented `$fn(` is checkable here and nowhere else offline.
|
|
1028
|
+
label: 'expression functions',
|
|
1029
|
+
run: () => {
|
|
1030
|
+
const builtins = loadBuiltinNames(packDir);
|
|
1031
|
+
if (!builtins.names)
|
|
1032
|
+
return { kind: 'not-run', lookup: builtins.lookup };
|
|
1033
|
+
const findings = withLines(yamlDocs().flatMap(([p, doc]) => findBuiltinViolations(doc, p, builtins.names)));
|
|
1034
|
+
return findings.length
|
|
1035
|
+
? {
|
|
1036
|
+
kind: 'findings', noun: 'unknown expression function',
|
|
1037
|
+
lines: findings.map(describeBuiltinFinding),
|
|
1038
|
+
hint: 'fix these before deploying — the evaluator cannot resolve them, and it fails mid-conversation',
|
|
1039
|
+
}
|
|
1040
|
+
: { kind: 'passed', line: 'every $function() in an expression exists' };
|
|
1041
|
+
},
|
|
1042
|
+
},
|
|
1043
|
+
{
|
|
1044
|
+
// `use:` templates. A pack's OWN templates shadow the platform's, so they are
|
|
1045
|
+
// named here and skipped — this check has no schema for them.
|
|
1046
|
+
label: '`use:` templates',
|
|
1047
|
+
run: () => {
|
|
1048
|
+
const templates = loadTemplateSpecs(packDir);
|
|
1049
|
+
if (!templates.specs)
|
|
1050
|
+
return { kind: 'not-run', lookup: templates.lookup };
|
|
1051
|
+
// A pack template is `templates/<name>.template.yaml` — the `.template`
|
|
1052
|
+
// segment is part of the convention the expander scans for, NOT part of the
|
|
1053
|
+
// name a `use:` writes. Capturing it would leave every pack that shadows a
|
|
1054
|
+
// platform template (kaiian shadows `field_prompt_render`) reported as using
|
|
1055
|
+
// one that does not exist.
|
|
1056
|
+
const packTemplates = new Set(Object.keys(files)
|
|
1057
|
+
.map(p => /^templates\/(.+)\.template\.ya?ml$/i.exec(p.replace(/\\/g, '/'))?.[1])
|
|
1058
|
+
.filter((n) => !!n));
|
|
1059
|
+
const findings = yamlDocs().flatMap(([p, doc]) => findTemplateViolations(doc, p, templates.specs, packTemplates));
|
|
1060
|
+
return findings.length
|
|
1061
|
+
? {
|
|
1062
|
+
kind: 'findings', noun: 'template error',
|
|
1063
|
+
lines: findings.map(describeTemplateFinding),
|
|
1064
|
+
hint: 'fix these before deploying — a template param that does not exist arrives as undefined, and renders as a blank',
|
|
1065
|
+
}
|
|
1066
|
+
: { kind: 'passed', line: '`use:` templates and their params exist' };
|
|
1067
|
+
},
|
|
1068
|
+
},
|
|
1069
|
+
{
|
|
1070
|
+
// Reserved XRM entity keys — a boot error, which means the pack deploys clean
|
|
1071
|
+
// and then fails to load on the first inbound message.
|
|
1072
|
+
label: 'reserved entity keys',
|
|
1073
|
+
run: () => {
|
|
1074
|
+
const system = loadSystemEntities(packDir);
|
|
1075
|
+
if (!system.entities)
|
|
1076
|
+
return { kind: 'not-run', lookup: system.lookup };
|
|
1077
|
+
const findings = yamlDocs()
|
|
1078
|
+
.filter(([p]) => /(^|[/\\])xrm\.ya?ml$/i.test(p))
|
|
1079
|
+
.flatMap(([p, doc]) => findEntityViolations(doc, p, system.entities, system.rules));
|
|
1080
|
+
return findings.length
|
|
1081
|
+
? {
|
|
1082
|
+
kind: 'findings', noun: 'reserved-entity error',
|
|
1083
|
+
lines: findings.map(describeEntityFinding),
|
|
1084
|
+
hint: 'fix these before deploying — these fail at BOOT, after a deploy that reported success',
|
|
1085
|
+
}
|
|
1086
|
+
// Name the narrower promise when the catalog-level rules are absent (a KB
|
|
1087
|
+
// pulled before they were published) — a ✓ that reads wider than what ran is
|
|
1088
|
+
// the failure this file's skip contract exists to prevent, and it is why
|
|
1089
|
+
// `passed` carries its own line instead of the printer inventing one.
|
|
1090
|
+
: {
|
|
1091
|
+
kind: 'passed',
|
|
1092
|
+
line: system.rules
|
|
1093
|
+
? 'no entity collides with a reserved platform key, and no extension overrides a platform-owned one'
|
|
1094
|
+
: 'no entity collides with a reserved platform key (re-pull for the `contact`/extension-override rules)',
|
|
1095
|
+
};
|
|
1096
|
+
},
|
|
1097
|
+
},
|
|
1098
|
+
{
|
|
1099
|
+
// The declaration files themselves, against the published JSON Schemas.
|
|
1100
|
+
// Deliberately narrow (see declaration-check.ts) — it walks away from anything
|
|
1101
|
+
// it cannot read rather than guessing.
|
|
1102
|
+
label: 'declaration schemas',
|
|
1103
|
+
run: () => {
|
|
1104
|
+
const decls = loadDeclarationSpecs(packDir);
|
|
1105
|
+
if (!decls.specs)
|
|
1106
|
+
return { kind: 'not-run', lookup: decls.lookup };
|
|
1107
|
+
const findings = yamlDocs().flatMap(([p, doc]) => {
|
|
1108
|
+
const base = p.replace(/\\/g, '/').split('/').pop() ?? p;
|
|
1109
|
+
const spec = decls.specs.get(base);
|
|
1110
|
+
// Only a file at the PACK ROOT is a declaration — `flows/tools/xrm.yaml`
|
|
1111
|
+
// would be a flow that happens to share a name.
|
|
1112
|
+
if (!spec || p.replace(/\\/g, '/').includes('/'))
|
|
1113
|
+
return [];
|
|
1114
|
+
return findDeclarationViolations(doc, spec).map(f => ({ ...f, file: p }));
|
|
1115
|
+
});
|
|
1116
|
+
return findings.length
|
|
1117
|
+
? {
|
|
1118
|
+
kind: 'findings', noun: 'declaration error',
|
|
1119
|
+
lines: findings.map(describeDeclarationFinding),
|
|
1120
|
+
hint: 'fix these before deploying — a declaration file is parsed strictly, and an unknown key is rejected',
|
|
1121
|
+
}
|
|
1122
|
+
// Deliberately narrow wording. Most of `xrm.yaml`'s field shapes are a Zod
|
|
1123
|
+
// union, which renders as `anyOf` and which this check walks away from by
|
|
1124
|
+
// design — so "matches its schema" would be a promise it does not keep, and
|
|
1125
|
+
// an over-claimed ✓ is how an author stops reading `--remote` output.
|
|
1126
|
+
: { kind: 'passed', line: 'declaration files carry no unknown or missing keys (unions are left to --remote)' };
|
|
1127
|
+
},
|
|
1128
|
+
},
|
|
1129
|
+
];
|
|
1130
|
+
const skipped = reportChecks(checks, {
|
|
1131
|
+
log: (l) => console.log(l),
|
|
1132
|
+
err: (l) => console.error(l),
|
|
1133
|
+
die,
|
|
1134
|
+
});
|
|
1131
1135
|
// `--require-kb` is for CI, where a skip nobody reads is worse than a red build.
|
|
1132
1136
|
if (skipped.length && flags['require-kb'] === true) {
|
|
1133
1137
|
die(`--require-kb: ${skipped.length} check${skipped.length === 1 ? '' : 's'} could not run (${skipped.join(', ')})`);
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Outcome` — what one `octwin validate` check ACTUALLY did, as a value.
|
|
3
|
+
*
|
|
4
|
+
* ## Why this exists
|
|
5
|
+
*
|
|
6
|
+
* `cmdValidate` used to hand-write its own `✓` per check, inside the `if` that had the
|
|
7
|
+
* check's data, with a `skipped[]` array and a `skipReasons` Map maintained alongside. It
|
|
8
|
+
* was correct because someone was careful, and the next check added was one `if` away from
|
|
9
|
+
* lying again. It had lied before, twice, in the same shape:
|
|
10
|
+
*
|
|
11
|
+
* - the KB-dependent block printed ONE `✓` above six checks whose own `✓`s lived inside
|
|
12
|
+
* their `if`s, so a run with no pulled KB read as "one check, passed". An entire backlog
|
|
13
|
+
* batch reached production that way;
|
|
14
|
+
* - `yamlDocs()` parsed inside `try { … } catch { return [] }`, so a file that failed to
|
|
15
|
+
* parse was silently DROPPED and every later check skipped it — `validate` printed
|
|
16
|
+
* all-green over a pack the platform's importer then refused with the same message, from
|
|
17
|
+
* the same `yaml` version this CLI already had.
|
|
18
|
+
*
|
|
19
|
+
* **An unparseable file is not "no findings", it is "no idea", and those must never print the
|
|
20
|
+
* same.** So a check no longer prints; it RETURNS one of three things, and a single printer
|
|
21
|
+
* renders all of them. A check that returns nothing cannot reach the `passed` branch, which
|
|
22
|
+
* makes the false-`✓` unrepresentable rather than merely discouraged.
|
|
23
|
+
*
|
|
24
|
+
* ## Why `not-run` carries the lookup rather than a message
|
|
25
|
+
*
|
|
26
|
+
* `describeKbLookup` phrases it at print time, so the wording lives in the module that owns
|
|
27
|
+
* the three KB states and cannot drift into a doubled "SKIPPED — SKIPPED —". It also lets the
|
|
28
|
+
* printer GROUP by reason: when the KB is absent every check skips for the identical reason,
|
|
29
|
+
* and six copies of one sentence is how a reader learns to scroll past the ⚠ block — the same
|
|
30
|
+
* failure as not printing it at all.
|
|
31
|
+
*/
|
|
32
|
+
import { describeKbLookup } from './kb-path.js';
|
|
33
|
+
/**
|
|
34
|
+
* Run every check in order and print exactly what each returned.
|
|
35
|
+
*
|
|
36
|
+
* Returns the labels that could not run, so the caller can decide what a partial pass means
|
|
37
|
+
* (`--require-kb` fails on any; an interactive run says which, last, where it is read).
|
|
38
|
+
*
|
|
39
|
+
* The first `findings` outcome terminates via `die`, matching the previous behaviour: an
|
|
40
|
+
* author fixes one class at a time, and printing six screens of unrelated findings buries the
|
|
41
|
+
* first. Order therefore matters and is the caller's to choose.
|
|
42
|
+
*/
|
|
43
|
+
export function reportChecks(checks, io) {
|
|
44
|
+
const notRun = [];
|
|
45
|
+
for (const check of checks) {
|
|
46
|
+
const outcome = check.run();
|
|
47
|
+
switch (outcome.kind) {
|
|
48
|
+
case 'passed':
|
|
49
|
+
io.log(`✓ ${outcome.line}`);
|
|
50
|
+
break;
|
|
51
|
+
case 'findings': {
|
|
52
|
+
const n = outcome.lines.length;
|
|
53
|
+
io.err(`✗ ${n} ${outcome.noun}${n === 1 ? '' : 's'}:`);
|
|
54
|
+
for (const line of outcome.lines)
|
|
55
|
+
io.err(` ✗ ${line}`);
|
|
56
|
+
io.die(outcome.hint);
|
|
57
|
+
}
|
|
58
|
+
// eslint-disable-next-line no-fallthrough -- `die` returns never; the case above cannot exit.
|
|
59
|
+
case 'not-run':
|
|
60
|
+
notRun.push({ label: check.label, lookup: outcome.lookup });
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// One ⚠ per distinct reason, naming every check it cost. Grouped by the lookup's IDENTITY.
|
|
65
|
+
const byReason = new Map();
|
|
66
|
+
for (const { label, lookup } of notRun) {
|
|
67
|
+
const key = lookup.state === 'ok'
|
|
68
|
+
? 'ok'
|
|
69
|
+
: `${lookup.state}|${'dir' in lookup ? lookup.dir : ''}|${'reason' in lookup ? lookup.reason : ''}`;
|
|
70
|
+
const bucket = byReason.get(key) ?? { lookup, labels: [] };
|
|
71
|
+
bucket.labels.push(label);
|
|
72
|
+
byReason.set(key, bucket);
|
|
73
|
+
}
|
|
74
|
+
for (const { lookup, labels } of byReason.values()) {
|
|
75
|
+
const what = labels.length === 1 ? labels[0] : `${labels.length} checks (${labels.join(', ')})`;
|
|
76
|
+
io.log(`⚠ ${describeKbLookup(lookup, what)}`);
|
|
77
|
+
}
|
|
78
|
+
return notRun.map(n => n.label);
|
|
79
|
+
}
|
package/package.json
CHANGED