@esneiderbravo/speclaw 0.4.0 → 1.0.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/README.md +91 -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 +39 -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/assets/laws/laws-manifest.json +16 -7
- package/dist/modules/foundation/check.js +4 -2
- package/dist/modules/foundation/compile-laws.js +210 -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/graph.js +7 -3
- 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 +35 -32
- package/dist/modules/foundation/lock.js +283 -0
- package/dist/modules/foundation/ownership.js +4 -0
- package/dist/modules/foundation/scaffold.js +34 -6
- package/dist/modules/foundation/scan.js +227 -0
- package/dist/modules/foundation/seed-laws.js +263 -0
- package/dist/modules/foundation/verify.js +11 -3
- 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,263 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { assetsDir } from "../../shared/paths.js";
|
|
4
|
+
// Catalog adapter: the shipped JSON is speclaw's own architecture plus a few
|
|
5
|
+
// portable laws. Consumer repos get only what their tree can actually host —
|
|
6
|
+
// never compass/foundation/ATTRIBUTION rules copied from this package.
|
|
7
|
+
//
|
|
8
|
+
// Types are declared here (not imported from laws.ts) so the adapter and the
|
|
9
|
+
// schema do not form a file-level import cycle.
|
|
10
|
+
const ASSETS = assetsDir(import.meta.url);
|
|
11
|
+
const CATALOG_PATH = path.join(ASSETS, "laws", "laws-manifest.json");
|
|
12
|
+
/** Globs that drop test doubles from the cycle-law graph. */
|
|
13
|
+
export const TEST_SCOPE_EXCLUSIONS = [
|
|
14
|
+
"!**/*.{spec,test}.{ts,tsx,js,jsx}",
|
|
15
|
+
"!**/test/**",
|
|
16
|
+
"!**/__tests__/**",
|
|
17
|
+
];
|
|
18
|
+
let cachedCatalog = null;
|
|
19
|
+
/**
|
|
20
|
+
* Read the shipped law catalog (including adapter metadata). Cached for the
|
|
21
|
+
* process; tests that mutate the asset are not a supported path.
|
|
22
|
+
*
|
|
23
|
+
* @returns The parsed catalog.
|
|
24
|
+
* @throws If the asset is missing or not an object with a `laws` array.
|
|
25
|
+
*/
|
|
26
|
+
export function readLawCatalog() {
|
|
27
|
+
if (cachedCatalog)
|
|
28
|
+
return cachedCatalog;
|
|
29
|
+
const raw = JSON.parse(fs.readFileSync(CATALOG_PATH, "utf8"));
|
|
30
|
+
if (!raw || !Array.isArray(raw.laws)) {
|
|
31
|
+
throw new Error(`invalid law catalog at ${CATALOG_PATH}`);
|
|
32
|
+
}
|
|
33
|
+
cachedCatalog = raw;
|
|
34
|
+
return raw;
|
|
35
|
+
}
|
|
36
|
+
/** Drop adapter-only fields so the result is a persistable law. */
|
|
37
|
+
export function asLaw(entry) {
|
|
38
|
+
const law = {
|
|
39
|
+
id: entry.id,
|
|
40
|
+
title: entry.title,
|
|
41
|
+
severity: entry.severity,
|
|
42
|
+
scope: [...entry.scope],
|
|
43
|
+
prose: entry.prose,
|
|
44
|
+
verification: structuredClone(entry.verification),
|
|
45
|
+
enforcement: entry.enforcement,
|
|
46
|
+
source: { ...entry.source },
|
|
47
|
+
};
|
|
48
|
+
if (entry.rationale !== undefined)
|
|
49
|
+
law.rationale = entry.rationale;
|
|
50
|
+
if (entry.status !== undefined)
|
|
51
|
+
law.status = entry.status;
|
|
52
|
+
return law;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Whether `req` names an existing file, directory, or glob under `projectPath`.
|
|
56
|
+
*
|
|
57
|
+
* @param projectPath - Project root.
|
|
58
|
+
* @param req - Relative path or glob (`*` / `**` in a segment).
|
|
59
|
+
*/
|
|
60
|
+
export function pathRequirementExists(projectPath, req) {
|
|
61
|
+
const trimmed = req.replace(/^!/, "").replace(/\/\*\*$/, "");
|
|
62
|
+
return globExists(projectPath, trimmed.split("/").filter(Boolean));
|
|
63
|
+
}
|
|
64
|
+
function globExists(dir, parts) {
|
|
65
|
+
if (parts.length === 0)
|
|
66
|
+
return fs.existsSync(dir);
|
|
67
|
+
if (!fs.existsSync(dir))
|
|
68
|
+
return false;
|
|
69
|
+
const head = parts[0];
|
|
70
|
+
const rest = parts.slice(1);
|
|
71
|
+
if (head === "**") {
|
|
72
|
+
if (globExists(dir, rest))
|
|
73
|
+
return true;
|
|
74
|
+
let st;
|
|
75
|
+
try {
|
|
76
|
+
st = fs.statSync(dir);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
if (!st.isDirectory())
|
|
82
|
+
return false;
|
|
83
|
+
for (const name of fs.readdirSync(dir)) {
|
|
84
|
+
if (name.startsWith("."))
|
|
85
|
+
continue;
|
|
86
|
+
if (globExists(path.join(dir, name), parts))
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
if (head === "*") {
|
|
92
|
+
let st;
|
|
93
|
+
try {
|
|
94
|
+
st = fs.statSync(dir);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
if (!st.isDirectory())
|
|
100
|
+
return false;
|
|
101
|
+
for (const name of fs.readdirSync(dir)) {
|
|
102
|
+
if (name.startsWith("."))
|
|
103
|
+
continue;
|
|
104
|
+
if (globExists(path.join(dir, name), rest))
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
return globExists(path.join(dir, head), rest);
|
|
110
|
+
}
|
|
111
|
+
function childSrcExists(projectPath, parent) {
|
|
112
|
+
const dir = path.join(projectPath, parent);
|
|
113
|
+
try {
|
|
114
|
+
if (!fs.statSync(dir).isDirectory())
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
for (const name of fs.readdirSync(dir)) {
|
|
121
|
+
if (name.startsWith("."))
|
|
122
|
+
continue;
|
|
123
|
+
try {
|
|
124
|
+
if (fs.statSync(path.join(dir, name, "src")).isDirectory())
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
/* skip */
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Source-root globs inferred from a repository layout.
|
|
135
|
+
*
|
|
136
|
+
* @param projectPath - Project root.
|
|
137
|
+
* @returns Globs covering app/package `src` trees, plus top-level `src/` or `lib/`, when they exist.
|
|
138
|
+
*/
|
|
139
|
+
export function detectSourceRootGlobs(projectPath) {
|
|
140
|
+
const out = [];
|
|
141
|
+
if (childSrcExists(projectPath, "apps"))
|
|
142
|
+
out.push("apps/*/src/**");
|
|
143
|
+
if (childSrcExists(projectPath, "packages"))
|
|
144
|
+
out.push("packages/*/src/**");
|
|
145
|
+
if (pathRequirementExists(projectPath, "src"))
|
|
146
|
+
out.push("src/**");
|
|
147
|
+
if (pathRequirementExists(projectPath, "lib"))
|
|
148
|
+
out.push("lib/**");
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
151
|
+
function positivesSatisfied(projectPath, scope) {
|
|
152
|
+
const positives = scope.filter((g) => !g.startsWith("!"));
|
|
153
|
+
if (positives.length === 0)
|
|
154
|
+
return true;
|
|
155
|
+
return positives.some((g) => pathRequirementExists(projectPath, g));
|
|
156
|
+
}
|
|
157
|
+
function withTestExclusions(scope) {
|
|
158
|
+
const have = new Set(scope);
|
|
159
|
+
const out = [...scope];
|
|
160
|
+
for (const g of TEST_SCOPE_EXCLUSIONS) {
|
|
161
|
+
if (!have.has(g))
|
|
162
|
+
out.push(g);
|
|
163
|
+
}
|
|
164
|
+
return out;
|
|
165
|
+
}
|
|
166
|
+
function adaptCycleLaw(projectPath, entry) {
|
|
167
|
+
const law = asLaw(entry);
|
|
168
|
+
const catalogHits = positivesSatisfied(projectPath, entry.scope);
|
|
169
|
+
const roots = catalogHits
|
|
170
|
+
? entry.scope.filter((g) => !g.startsWith("!"))
|
|
171
|
+
: detectSourceRootGlobs(projectPath);
|
|
172
|
+
if (roots.length === 0)
|
|
173
|
+
return null;
|
|
174
|
+
law.scope = withTestExclusions(roots);
|
|
175
|
+
if (law.verification.kind === "graph") {
|
|
176
|
+
law.verification = {
|
|
177
|
+
kind: "graph",
|
|
178
|
+
rule: { ...(law.verification.rule ?? {}), circular: true, edgeKinds: ["import"] },
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
return law;
|
|
182
|
+
}
|
|
183
|
+
function isApplicable(projectPath, entry) {
|
|
184
|
+
const reqs = entry.requires ?? [];
|
|
185
|
+
return reqs.every((r) => pathRequirementExists(projectPath, r));
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Catalog laws stripped of adapter metadata — the full shipped set, unfiltered.
|
|
189
|
+
*
|
|
190
|
+
* @returns Persistable laws in catalog order.
|
|
191
|
+
*/
|
|
192
|
+
export function catalogLaws() {
|
|
193
|
+
return readLawCatalog().laws.map(asLaw);
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Laws from the catalog that apply to `projectPath`, with cycle-law scope
|
|
197
|
+
* rewritten to the repo's real source roots when needed.
|
|
198
|
+
*
|
|
199
|
+
* @param projectPath - Project root whose tree is inspected.
|
|
200
|
+
* @returns Persistable laws; adapter fields omitted.
|
|
201
|
+
*/
|
|
202
|
+
export function adaptedLaws(projectPath) {
|
|
203
|
+
// Covers: req~adapt-seed-to-repo~1
|
|
204
|
+
const out = [];
|
|
205
|
+
for (const entry of readLawCatalog().laws) {
|
|
206
|
+
if (!isApplicable(projectPath, entry))
|
|
207
|
+
continue;
|
|
208
|
+
if (entry.adaptScope === "source-roots") {
|
|
209
|
+
const adapted = adaptCycleLaw(projectPath, entry);
|
|
210
|
+
if (adapted)
|
|
211
|
+
out.push(adapted);
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
out.push(asLaw(entry));
|
|
215
|
+
}
|
|
216
|
+
return out;
|
|
217
|
+
}
|
|
218
|
+
/** True when `existing` is still the shipped catalog text (title + prose). */
|
|
219
|
+
export function isUnmodifiedCatalogLaw(existing, catalog) {
|
|
220
|
+
return existing.title === catalog.title && existing.prose === catalog.prose;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Merge an on-disk manifest with the adapted catalog: append applicable ids,
|
|
224
|
+
* rewrite unmodified cycle-law scope, drop unmodified inapplicable dogfood.
|
|
225
|
+
*
|
|
226
|
+
* @param existing - The project's current manifest.
|
|
227
|
+
* @param projectPath - Project root used to adapt the catalog.
|
|
228
|
+
* @returns The merged manifest and the ids that were added or removed.
|
|
229
|
+
*/
|
|
230
|
+
export function mergeAdaptedSeed(existing, projectPath) {
|
|
231
|
+
const catalog = readLawCatalog();
|
|
232
|
+
const byId = new Map(catalog.laws.map((l) => [l.id, l]));
|
|
233
|
+
const adapted = adaptedLaws(projectPath);
|
|
234
|
+
const adaptedById = new Map(adapted.map((l) => [l.id, l]));
|
|
235
|
+
const added = [];
|
|
236
|
+
const removed = [];
|
|
237
|
+
const result = [];
|
|
238
|
+
const seen = new Set();
|
|
239
|
+
for (const law of existing.laws) {
|
|
240
|
+
const cat = byId.get(law.id);
|
|
241
|
+
if (!cat || !isUnmodifiedCatalogLaw(law, cat)) {
|
|
242
|
+
result.push(law);
|
|
243
|
+
seen.add(law.id);
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
const next = adaptedById.get(law.id);
|
|
247
|
+
if (next) {
|
|
248
|
+
result.push(next);
|
|
249
|
+
seen.add(law.id);
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
removed.push(law.id);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
for (const law of adapted) {
|
|
256
|
+
if (seen.has(law.id))
|
|
257
|
+
continue;
|
|
258
|
+
result.push(law);
|
|
259
|
+
added.push(law.id);
|
|
260
|
+
seen.add(law.id);
|
|
261
|
+
}
|
|
262
|
+
return { manifest: { ...existing, laws: result }, added, removed };
|
|
263
|
+
}
|
|
@@ -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";
|
|
@@ -17,8 +17,8 @@ export { underPaths } from "./verify-model.js";
|
|
|
17
17
|
*
|
|
18
18
|
* When the project has no index, every selected batch law is reported as
|
|
19
19
|
* `skipped` with reason `no-index` (never silently passed). When the gitignored
|
|
20
|
-
* manifest file is missing, the
|
|
21
|
-
* report an empty pass. Each evaluated law lands in exactly one of `passed` /
|
|
20
|
+
* manifest file is missing, the adapted seed is used so a clean clone does not
|
|
21
|
+
* report an empty pass or inherit inapplicable dogfood laws. Each evaluated law lands in exactly one of `passed` /
|
|
22
22
|
* `failed` / `unknown`: it fails when the engine produced a finding, is
|
|
23
23
|
* `unknown` when it produced none but rests on unresolved edges (which could
|
|
24
24
|
* hide a violation), and passes otherwise.
|
|
@@ -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);
|