@esneiderbravo/speclaw 0.4.0 → 1.0.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/README.md +88 -72
- package/dist/cli/commands/index-build.js +12 -3
- package/dist/cli/commands/lawbook.js +1 -0
- package/dist/cli/commands/laws.js +149 -8
- package/dist/cli/commands/owners.js +44 -0
- package/dist/cli/commands/query.js +32 -10
- package/dist/cli/commands/update.js +28 -0
- package/dist/cli/commands/verify.js +8 -0
- package/dist/cli/index.js +13 -4
- package/dist/modules/compass/budget.js +128 -0
- package/dist/modules/compass/db.js +290 -30
- package/dist/modules/compass/embed-input.js +28 -0
- package/dist/modules/compass/embedder.js +3 -1
- package/dist/modules/compass/explore-rich.js +10 -5
- package/dist/modules/compass/extract.js +86 -0
- package/dist/modules/compass/hybrid.js +318 -0
- package/dist/modules/compass/indexer.js +204 -33
- package/dist/modules/compass/merkle.js +76 -0
- package/dist/modules/compass/pagerank.js +122 -0
- package/dist/modules/compass/rank.js +95 -0
- package/dist/modules/compass/register.js +8 -4
- package/dist/modules/foundation/check.js +4 -2
- package/dist/modules/foundation/compile-laws.js +212 -0
- package/dist/modules/foundation/dialects/agentsmd.js +95 -0
- package/dist/modules/foundation/dialects/claude-cursor.js +45 -0
- package/dist/modules/foundation/dialects/coderabbit.js +27 -0
- package/dist/modules/foundation/dialects/copilot.js +35 -0
- package/dist/modules/foundation/dialects/index.js +5 -0
- package/dist/modules/foundation/dialects/types.js +58 -0
- package/dist/modules/foundation/doctor.js +220 -14
- package/dist/modules/foundation/import-rules.js +67 -0
- package/dist/modules/foundation/integrity.js +307 -0
- package/dist/modules/foundation/laws-parse.js +131 -0
- package/dist/modules/foundation/laws.js +5 -0
- package/dist/modules/foundation/lock.js +283 -0
- package/dist/modules/foundation/ownership.js +4 -0
- package/dist/modules/foundation/scaffold.js +25 -0
- package/dist/modules/foundation/scan.js +227 -0
- package/dist/modules/foundation/verify.js +9 -1
- package/dist/modules/lawbook/coverage.js +45 -6
- package/dist/modules/lawbook/ears.js +417 -0
- package/dist/modules/lawbook/engine.js +29 -0
- package/dist/modules/lawbook/spec-items.js +4 -1
- package/dist/modules/team/owners.js +464 -0
- package/package.json +4 -3
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt-injection scanners for rule / skill files.
|
|
3
|
+
* Complements digests: hashes catch any edit; scanners catch known payloads.
|
|
4
|
+
*/
|
|
5
|
+
// Covers: req~injection-scan~1
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
const OVERRIDE = /\b(ignore\s+previous\s+instructions|disregard\s+the\s+above|you\s+are\s+now|new\s+system\s+prompt|ignora\s+las\s+instrucciones\s+anteriores)\b/i;
|
|
9
|
+
const SHELL = /\b(curl\s+[^\n|]*\|\s*(ba)?sh|bash\s+-c|Invoke-Expression|iex\b|eval\s*\(|npm\s+run\s+[^\s]+.*\|\s*sh)\b/i;
|
|
10
|
+
const EXFIL = /\b(send\s+(this|the)\s+(repo|code|contents?)\s+to|exfiltrat|upload\s+to\s+https?:\/\/|read\s+(\.env|~\/\.ssh|~\/\.aws|\.npmrc)|exfiltra)\b/i;
|
|
11
|
+
const URL_RE = /https?:\/\/[^\s)>\]]+/gi;
|
|
12
|
+
const ZERO_WIDTH = /[\u200B-\u200D\uFEFF\u202A-\u202E]/;
|
|
13
|
+
const IMPERATIVE_HTML = /<!--[\s\S]{0,200}\b(run|execute|ignore|disregard|curl|bash|send)\b[\s\S]{0,200}-->/i;
|
|
14
|
+
/**
|
|
15
|
+
* Normalize text before detection (NFKC, strip zero-width/bidi, fold common
|
|
16
|
+
* Cyrillic lookalikes, collapse whitespace).
|
|
17
|
+
*/
|
|
18
|
+
export function normalizeForScan(text) {
|
|
19
|
+
let t = text.normalize("NFKC");
|
|
20
|
+
t = t.replace(ZERO_WIDTH, "");
|
|
21
|
+
t = t.replace(/[\u0400-\u04FF]/g, (ch) => CYRILLIC_FOLD[ch] ?? ch);
|
|
22
|
+
t = t.replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&");
|
|
23
|
+
t = t.replace(/\\\*|\\_|\\`/g, (m) => m.slice(1));
|
|
24
|
+
t = t.replace(/\s+/g, " ").trim();
|
|
25
|
+
return t;
|
|
26
|
+
}
|
|
27
|
+
const CYRILLIC_FOLD = {
|
|
28
|
+
а: "a",
|
|
29
|
+
е: "e",
|
|
30
|
+
о: "o",
|
|
31
|
+
р: "p",
|
|
32
|
+
с: "c",
|
|
33
|
+
у: "y",
|
|
34
|
+
х: "x",
|
|
35
|
+
А: "A",
|
|
36
|
+
Е: "E",
|
|
37
|
+
О: "O",
|
|
38
|
+
Р: "P",
|
|
39
|
+
С: "C",
|
|
40
|
+
У: "Y",
|
|
41
|
+
Х: "X",
|
|
42
|
+
};
|
|
43
|
+
/** Scan one file's raw contents; returns findings with original line numbers. */
|
|
44
|
+
export function scanText(relPath, raw, opts = {}) {
|
|
45
|
+
const suppressions = opts.suppressions ?? [];
|
|
46
|
+
const allow = new Set((opts.allowHosts ?? []).map((h) => h.toLowerCase()));
|
|
47
|
+
const out = [];
|
|
48
|
+
const lines = raw.split(/\r?\n/);
|
|
49
|
+
const push = (f) => {
|
|
50
|
+
if (suppressions.some((s) => s.detector === f.detector && matchPath(s.path, f.path) && s.note.trim().length > 0)) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
out.push(f);
|
|
54
|
+
};
|
|
55
|
+
if (ZERO_WIDTH.test(raw)) {
|
|
56
|
+
push({
|
|
57
|
+
detector: "injection/hidden-text",
|
|
58
|
+
severity: "error",
|
|
59
|
+
path: relPath,
|
|
60
|
+
line: 1,
|
|
61
|
+
excerpt: "zero-width or bidi control characters",
|
|
62
|
+
message: "Hidden / bidi control characters in a rule file.",
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
for (let i = 0; i < lines.length; i++) {
|
|
66
|
+
const line = lines[i];
|
|
67
|
+
const norm = normalizeForScan(line);
|
|
68
|
+
const ln = i + 1;
|
|
69
|
+
if (OVERRIDE.test(norm) || OVERRIDE.test(line)) {
|
|
70
|
+
push({
|
|
71
|
+
detector: "injection/instruction-override",
|
|
72
|
+
severity: "error",
|
|
73
|
+
path: relPath,
|
|
74
|
+
line: ln,
|
|
75
|
+
excerpt: clip(line),
|
|
76
|
+
message: "Instruction-override phrasing in a rule file.",
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
if (SHELL.test(norm) || SHELL.test(line)) {
|
|
80
|
+
push({
|
|
81
|
+
detector: "injection/shell-execution",
|
|
82
|
+
severity: "error",
|
|
83
|
+
path: relPath,
|
|
84
|
+
line: ln,
|
|
85
|
+
excerpt: clip(line),
|
|
86
|
+
message: "Shell-execution instruction in a rule file.",
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
if (EXFIL.test(norm) || EXFIL.test(line)) {
|
|
90
|
+
push({
|
|
91
|
+
detector: "injection/exfiltration",
|
|
92
|
+
severity: "error",
|
|
93
|
+
path: relPath,
|
|
94
|
+
line: ln,
|
|
95
|
+
excerpt: clip(line),
|
|
96
|
+
message: "Possible exfiltration instruction in a rule file.",
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
for (const m of line.matchAll(URL_RE)) {
|
|
100
|
+
try {
|
|
101
|
+
const host = new URL(m[0]).hostname.toLowerCase();
|
|
102
|
+
if (allow.size > 0 && !allow.has(host) && !host.endsWith(".github.com")) {
|
|
103
|
+
push({
|
|
104
|
+
detector: "injection/unallowlisted-url",
|
|
105
|
+
severity: "warn",
|
|
106
|
+
path: relPath,
|
|
107
|
+
line: ln,
|
|
108
|
+
excerpt: clip(m[0]),
|
|
109
|
+
message: `URL host not on allowlist: ${host}`,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
/* ignore */
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (IMPERATIVE_HTML.test(raw)) {
|
|
119
|
+
const idx = raw.search(IMPERATIVE_HTML);
|
|
120
|
+
const snippet = raw.slice(Math.max(0, idx), idx + 220);
|
|
121
|
+
// Data-only speclaw markers (map/laws/provenance) are not agent instructions.
|
|
122
|
+
if (!/<!--\s*speclaw:/.test(snippet)) {
|
|
123
|
+
const line = lineOf(raw, idx);
|
|
124
|
+
push({
|
|
125
|
+
detector: "injection/imperative-html-comment",
|
|
126
|
+
severity: "warn",
|
|
127
|
+
path: relPath,
|
|
128
|
+
line,
|
|
129
|
+
excerpt: clip(raw.slice(idx, idx + 120)),
|
|
130
|
+
message: "HTML comment contains imperative language (visible to some agents).",
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// External @import / @~/ / @C:\…
|
|
135
|
+
// External @import / @~/ / @C:\…
|
|
136
|
+
for (let i = 0; i < lines.length; i++) {
|
|
137
|
+
const line = lines[i];
|
|
138
|
+
const m = /@([~/][^\s)\]>"']+)/.exec(line) ??
|
|
139
|
+
/@([A-Za-z]:[^\s)\]>"']+)/.exec(line) ??
|
|
140
|
+
/@import\s+["']([^"']+)["']/.exec(line);
|
|
141
|
+
if (!m)
|
|
142
|
+
continue;
|
|
143
|
+
const target = m[1];
|
|
144
|
+
push({
|
|
145
|
+
detector: "injection/external-import",
|
|
146
|
+
severity: "warn",
|
|
147
|
+
path: relPath,
|
|
148
|
+
line: i + 1,
|
|
149
|
+
excerpt: clip(line),
|
|
150
|
+
message: `Import may resolve outside the working directory: ${target}`,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
// Frontmatter ↔ body mismatch (skills)
|
|
154
|
+
const fm = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/.exec(raw);
|
|
155
|
+
if (fm) {
|
|
156
|
+
const front = fm[1];
|
|
157
|
+
const body = fm[2];
|
|
158
|
+
const frontDesc = /(?:^|\n)description:\s*["']?([^\n"']+)/i.exec(front)?.[1] ?? "";
|
|
159
|
+
if (frontDesc && SHELL.test(body) && !SHELL.test(frontDesc)) {
|
|
160
|
+
push({
|
|
161
|
+
detector: "injection/manifest-prose-mismatch",
|
|
162
|
+
severity: "warn",
|
|
163
|
+
path: relPath,
|
|
164
|
+
line: 1,
|
|
165
|
+
excerpt: clip(frontDesc),
|
|
166
|
+
message: "Skill frontmatter description and body disagree on shell risk.",
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return out;
|
|
171
|
+
}
|
|
172
|
+
function matchPath(pattern, file) {
|
|
173
|
+
if (pattern === file)
|
|
174
|
+
return true;
|
|
175
|
+
if (pattern.endsWith("/**")) {
|
|
176
|
+
const prefix = pattern.slice(0, -3);
|
|
177
|
+
return file === prefix || file.startsWith(prefix + "/");
|
|
178
|
+
}
|
|
179
|
+
if (pattern.includes("*")) {
|
|
180
|
+
const re = new RegExp("^" + pattern.split("*").map(escapeRegExp).join(".*") + "$");
|
|
181
|
+
return re.test(file);
|
|
182
|
+
}
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
function escapeRegExp(s) {
|
|
186
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
187
|
+
}
|
|
188
|
+
function clip(s, n = 120) {
|
|
189
|
+
const t = s.trim();
|
|
190
|
+
return t.length <= n ? t : t.slice(0, n - 1) + "…";
|
|
191
|
+
}
|
|
192
|
+
function lineOf(text, index) {
|
|
193
|
+
if (index <= 0)
|
|
194
|
+
return 1;
|
|
195
|
+
return text.slice(0, index).split(/\r?\n/).length;
|
|
196
|
+
}
|
|
197
|
+
/** Load optional scan suppressions from lawbook/config.yaml (line-oriented). */
|
|
198
|
+
export function loadScanSuppressions(projectPath) {
|
|
199
|
+
const cfgPath = path.join(projectPath, "lawbook", "config.yaml");
|
|
200
|
+
if (!fs.existsSync(cfgPath))
|
|
201
|
+
return [];
|
|
202
|
+
const text = fs.readFileSync(cfgPath, "utf8");
|
|
203
|
+
// Very small subset: repeated blocks under scanSuppressions are not fully
|
|
204
|
+
// parsed; support a flat JSON-ish list in a comment-free line for v1:
|
|
205
|
+
// scanSuppressions: [{"detector":"…","path":"…","note":"…"}]
|
|
206
|
+
const m = /^\s*scanSuppressions\s*:\s*(\[[\s\S]*?\])\s*$/m.exec(text);
|
|
207
|
+
if (!m)
|
|
208
|
+
return [];
|
|
209
|
+
try {
|
|
210
|
+
const arr = JSON.parse(m[1]);
|
|
211
|
+
return arr.filter((s) => s.detector && s.path && s.note);
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
return [];
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/** Scan a list of project-relative files that exist. */
|
|
218
|
+
export function scanPaths(projectPath, relPaths, opts = {}) {
|
|
219
|
+
const out = [];
|
|
220
|
+
for (const rel of relPaths) {
|
|
221
|
+
const abs = path.join(projectPath, rel);
|
|
222
|
+
if (!fs.existsSync(abs) || !fs.statSync(abs).isFile())
|
|
223
|
+
continue;
|
|
224
|
+
out.push(...scanText(rel, fs.readFileSync(abs, "utf8"), opts));
|
|
225
|
+
}
|
|
226
|
+
return out;
|
|
227
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { performance } from "node:perf_hooks";
|
|
2
2
|
import { openDb, indexExists } from "../compass/db.js";
|
|
3
|
-
import { hasBatchBackend, loadManifestForVerify } from "./laws.js";
|
|
3
|
+
import { hasBatchBackend, isActiveLaw, loadManifestForVerify } from "./laws.js";
|
|
4
4
|
import { runDepsLaw } from "./deps.js";
|
|
5
5
|
import { runGraphLaw } from "./graph.js";
|
|
6
6
|
export { underPaths } from "./verify-model.js";
|
|
@@ -50,6 +50,14 @@ export function verifyLaws(args) {
|
|
|
50
50
|
const manifest = loadManifestForVerify(args.projectPath);
|
|
51
51
|
const engines = args.engines;
|
|
52
52
|
const selected = manifest.laws.filter((law) => {
|
|
53
|
+
if (!isActiveLaw(law)) {
|
|
54
|
+
skipped.push({
|
|
55
|
+
lawId: law.id,
|
|
56
|
+
reason: "draft",
|
|
57
|
+
detail: "status=draft — pending human activation; does not gate",
|
|
58
|
+
});
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
53
61
|
if (!hasBatchBackend(law))
|
|
54
62
|
return false;
|
|
55
63
|
if (args.lawIds && !args.lawIds.includes(law.id))
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { openDb, indexExists } from "../compass/db.js";
|
|
4
|
+
import { detectPropertyRunnerInWindow, loadPropertyRunners } from "./ears.js";
|
|
4
5
|
import { formatItemId, loadSpecItems, parseItemId, parseSpecItems, } from "./spec-items.js";
|
|
5
6
|
export const DEFAULT_COVERAGE_CONFIG = {
|
|
6
7
|
defaultNeeds: ["impl", "utest"],
|
|
@@ -10,8 +11,10 @@ export const DEFAULT_COVERAGE_CONFIG = {
|
|
|
10
11
|
impl: ["src/**"],
|
|
11
12
|
utest: ["test/unit/**", "test/**/*.test.ts", "test/**/*.test.js"],
|
|
12
13
|
itest: ["test/integration/**"],
|
|
14
|
+
ptest: ["test/property/**"],
|
|
13
15
|
},
|
|
14
16
|
exclude: ["**/node_modules/**", "**/dist/**", "**/.speclaw/**"],
|
|
17
|
+
propertyRunners: [],
|
|
15
18
|
};
|
|
16
19
|
/**
|
|
17
20
|
* Load coverage config from lawbook/config.yaml when present; otherwise defaults.
|
|
@@ -20,8 +23,10 @@ export const DEFAULT_COVERAGE_CONFIG = {
|
|
|
20
23
|
export function loadCoverageConfig(projectPath) {
|
|
21
24
|
const cfg = structuredClone(DEFAULT_COVERAGE_CONFIG);
|
|
22
25
|
const cfgPath = path.join(projectPath, "lawbook", "config.yaml");
|
|
23
|
-
if (!fs.existsSync(cfgPath))
|
|
26
|
+
if (!fs.existsSync(cfgPath)) {
|
|
27
|
+
cfg.propertyRunners = loadPropertyRunners(projectPath);
|
|
24
28
|
return cfg;
|
|
29
|
+
}
|
|
25
30
|
const text = fs.readFileSync(cfgPath, "utf8");
|
|
26
31
|
const gate = /^\s*gateArchive\s*:\s*(true|false)\s*$/im.exec(text);
|
|
27
32
|
if (gate)
|
|
@@ -43,8 +48,21 @@ export function loadCoverageConfig(projectPath) {
|
|
|
43
48
|
.toLowerCase())
|
|
44
49
|
.filter(Boolean);
|
|
45
50
|
}
|
|
51
|
+
cfg.propertyRunners = loadPropertyRunners(projectPath);
|
|
46
52
|
return cfg;
|
|
47
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Effective coverage needs for an item: explicit `Needs:` or defaults, plus
|
|
56
|
+
* `ptest` when `Verification: property` is declared.
|
|
57
|
+
*/
|
|
58
|
+
// Covers: req~ptest-need~1, req~ptest-archive-gate~1
|
|
59
|
+
export function effectiveNeeds(item, cfg) {
|
|
60
|
+
const needs = item.needs.length > 0 ? [...item.needs] : [...cfg.defaultNeeds];
|
|
61
|
+
if (item.verification === "property" && !needs.includes("ptest")) {
|
|
62
|
+
needs.push("ptest");
|
|
63
|
+
}
|
|
64
|
+
return needs;
|
|
65
|
+
}
|
|
48
66
|
/** Glob match supporting `**`, `*`, and path separators. */
|
|
49
67
|
export function matchGlob(relPath, pattern) {
|
|
50
68
|
const norm = relPath.split("\\").join("/");
|
|
@@ -78,13 +96,33 @@ export function inferArtifactType(relPath, cfg) {
|
|
|
78
96
|
const norm = relPath.split("\\").join("/");
|
|
79
97
|
if (cfg.exclude.some((g) => matchGlob(norm, g)))
|
|
80
98
|
return null;
|
|
81
|
-
for (const type of ["itest", "utest", "impl"]) {
|
|
99
|
+
for (const type of ["ptest", "itest", "utest", "impl"]) {
|
|
82
100
|
const globs = cfg.sources[type] ?? [];
|
|
83
101
|
if (globs.some((g) => matchGlob(norm, g)))
|
|
84
102
|
return type;
|
|
85
103
|
}
|
|
86
104
|
return null;
|
|
87
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Prefer `ptest` when a property-runner invocation sits near the link line.
|
|
108
|
+
*/
|
|
109
|
+
// Covers: req~ptest-need~1
|
|
110
|
+
export function refineSourceType(projectPath, filePath, line, current, runners) {
|
|
111
|
+
if (runners.length === 0)
|
|
112
|
+
return current;
|
|
113
|
+
const abs = path.isAbsolute(filePath) ? filePath : path.join(projectPath, filePath);
|
|
114
|
+
if (!fs.existsSync(abs))
|
|
115
|
+
return current;
|
|
116
|
+
let source;
|
|
117
|
+
try {
|
|
118
|
+
source = fs.readFileSync(abs, "utf8");
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return current;
|
|
122
|
+
}
|
|
123
|
+
const hit = detectPropertyRunnerInWindow(source, line, runners);
|
|
124
|
+
return hit ? "ptest" : current;
|
|
125
|
+
}
|
|
88
126
|
function readIndexLinks(projectPath) {
|
|
89
127
|
if (!indexExists(projectPath))
|
|
90
128
|
return [];
|
|
@@ -135,7 +173,7 @@ function inlineLinksAsRaw(projectPath, items, cfg) {
|
|
|
135
173
|
}
|
|
136
174
|
return out;
|
|
137
175
|
}
|
|
138
|
-
function classifyLink(link, item, idCounts, cfg) {
|
|
176
|
+
function classifyLink(link, item, idCounts, cfg, projectPath) {
|
|
139
177
|
const base = {
|
|
140
178
|
artifactType: link.artifactType,
|
|
141
179
|
name: link.name,
|
|
@@ -172,6 +210,7 @@ function classifyLink(link, item, idCounts, cfg) {
|
|
|
172
210
|
const inferred = inferArtifactType(link.filePath, cfg);
|
|
173
211
|
if (inferred)
|
|
174
212
|
base.sourceType = inferred;
|
|
213
|
+
base.sourceType = refineSourceType(projectPath, link.filePath, link.line, base.sourceType, cfg.propertyRunners);
|
|
175
214
|
return base;
|
|
176
215
|
}
|
|
177
216
|
/**
|
|
@@ -198,12 +237,12 @@ export function buildCoverageReport(projectPath, opts = {}) {
|
|
|
198
237
|
const matchedKeys = new Set();
|
|
199
238
|
for (const item of identified) {
|
|
200
239
|
const idText = item.idText;
|
|
201
|
-
const needs = item
|
|
240
|
+
const needs = effectiveNeeds(item, cfg);
|
|
202
241
|
const itemLinks = rawLinks
|
|
203
242
|
.filter((l) => l.artifactType === item.id.artifactType && l.name === item.id.name)
|
|
204
243
|
.map((l) => {
|
|
205
244
|
matchedKeys.add(`${l.filePath}:${l.line}:${l.revision}:${l.kind}`);
|
|
206
|
-
return classifyLink(l, item, idCounts, cfg);
|
|
245
|
+
return classifyLink(l, item, idCounts, cfg, projectPath);
|
|
207
246
|
});
|
|
208
247
|
const covering = itemLinks.filter((l) => l.status === "Covers");
|
|
209
248
|
const coveredTypes = [...new Set(covering.map((l) => l.sourceType))];
|
|
@@ -282,7 +321,7 @@ export function buildCoverageReport(projectPath, opts = {}) {
|
|
|
282
321
|
const key = `${l.filePath}:${l.line}:${l.revision}:${l.kind}`;
|
|
283
322
|
if (matchedKeys.has(key))
|
|
284
323
|
continue;
|
|
285
|
-
orphans.push(classifyLink(l, undefined, idCounts, cfg));
|
|
324
|
+
orphans.push(classifyLink(l, undefined, idCounts, cfg, projectPath));
|
|
286
325
|
}
|
|
287
326
|
const gated = results.filter((r) => cfg.gateStatuses.includes(r.status));
|
|
288
327
|
const directDefects = gated.reduce((n, r) => n + r.directDefects.length, 0);
|