@shrkcrft/inspector 0.1.0-alpha.21 → 0.1.0-alpha.23
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/dist/command-recommender.d.ts.map +1 -1
- package/dist/command-recommender.js +110 -1
- package/dist/context-tuning.d.ts +14 -0
- package/dist/context-tuning.d.ts.map +1 -0
- package/dist/context-tuning.js +38 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/knowledge-stale.d.ts +23 -0
- package/dist/knowledge-stale.d.ts.map +1 -1
- package/dist/knowledge-stale.js +58 -9
- package/dist/monorepo-onboarding.d.ts.map +1 -1
- package/dist/monorepo-onboarding.js +122 -12
- package/dist/onboarding.d.ts.map +1 -1
- package/dist/onboarding.js +79 -18
- package/dist/pack-doctor.d.ts +8 -1
- package/dist/pack-doctor.d.ts.map +1 -1
- package/dist/pack-doctor.js +110 -3
- package/dist/pack-quality-score.d.ts.map +1 -1
- package/dist/pack-quality-score.js +7 -8
- package/dist/pack-release-check.d.ts.map +1 -1
- package/dist/pack-release-check.js +23 -20
- package/dist/pack-signature-status.d.ts +9 -3
- package/dist/pack-signature-status.d.ts.map +1 -1
- package/dist/pack-signature-status.js +13 -2
- package/dist/plan-simulation.d.ts.map +1 -1
- package/dist/plan-simulation.js +26 -12
- package/dist/registry-lifecycle.d.ts +8 -0
- package/dist/registry-lifecycle.d.ts.map +1 -1
- package/dist/registry-lifecycle.js +21 -3
- package/dist/resolve-project-config.d.ts +26 -0
- package/dist/resolve-project-config.d.ts.map +1 -0
- package/dist/resolve-project-config.js +128 -0
- package/dist/resolve-verification-commands.d.ts +7 -0
- package/dist/resolve-verification-commands.d.ts.map +1 -1
- package/dist/resolve-verification-commands.js +53 -4
- package/dist/review-packet.d.ts +8 -0
- package/dist/review-packet.d.ts.map +1 -1
- package/dist/review-packet.js +11 -12
- package/dist/sharkcraft-inspector.d.ts.map +1 -1
- package/dist/sharkcraft-inspector.js +28 -2
- package/dist/spec/__tests__/spec-evidence.test.d.ts +2 -0
- package/dist/spec/__tests__/spec-evidence.test.d.ts.map +1 -0
- package/dist/spec/__tests__/spec-evidence.test.js +81 -0
- package/dist/spec/spec-evidence.d.ts +62 -0
- package/dist/spec/spec-evidence.d.ts.map +1 -0
- package/dist/spec/spec-evidence.js +215 -0
- package/dist/spec/spec-review.d.ts +1 -0
- package/dist/spec/spec-review.d.ts.map +1 -1
- package/dist/spec/spec-review.js +5 -0
- package/dist/symbol-index.d.ts +9 -0
- package/dist/symbol-index.d.ts.map +1 -1
- package/dist/symbol-index.js +6 -0
- package/dist/task-packet.d.ts.map +1 -1
- package/dist/task-packet.js +6 -1
- package/dist/task-ranker.d.ts +2 -1
- package/dist/task-ranker.d.ts.map +1 -1
- package/dist/task-ranker.js +30 -5
- package/dist/test-runner.d.ts.map +1 -1
- package/dist/test-runner.js +3 -0
- package/dist/why-file.d.ts.map +1 -1
- package/dist/why-file.js +13 -0
- package/package.json +17 -17
|
@@ -61,7 +61,17 @@ const REGISTER_PATTERNS = Object.freeze([
|
|
|
61
61
|
/(?:export\s+)?(?:async\s+)?function\s+(register[A-Z]\w*)\s*\(/g,
|
|
62
62
|
// class method DECLARATION: requires a body `{` or return-type `:` after the
|
|
63
63
|
// params, which a bare call site (`registry.registerX(...)`) never has.
|
|
64
|
-
|
|
64
|
+
// NOTE: every modifier carries its OWN single-line (`[ \t]`) trailing space
|
|
65
|
+
// and the leading whitespace class is single-line too — no two newline-
|
|
66
|
+
// spanning (`\s`) quantifiers sit adjacent. The old form had `[\t ]*` then
|
|
67
|
+
// `\s*` then `(?:static\s+)?` then `(?:async\s+)?` all touching, so from each
|
|
68
|
+
// `\n` anchor the engine re-partitioned the whole whitespace block looking for
|
|
69
|
+
// `register`: O(block^2) catastrophic backtracking that dominated the scan
|
|
70
|
+
// (~90% of runtime on a stripped source full of blanked comment/string runs).
|
|
71
|
+
// The only newline-spanning class is the final one, after the required `)`,
|
|
72
|
+
// where it cannot backtrack against a neighbour. Match-equivalent to the old
|
|
73
|
+
// pattern on real TS (modifiers always sit on the same line as `registerX(`).
|
|
74
|
+
/(?:^|\n)[ \t]*(?:(?:public|private|protected)[ \t]+)?(?:static[ \t]+)?(?:async[ \t]+)?(register[A-Z]\w*)[ \t]*\([^;]*?\)[ \t\r\n]*[:{]/g,
|
|
65
75
|
// assigned arrow / function expression: `registerX = (…) =>` / `registerX = function`
|
|
66
76
|
/\b(register[A-Z]\w*)\s*=\s*(?:async\s+)?(?:function\b|\([^)]*\)\s*(?::[^=]*)?=>)/g,
|
|
67
77
|
]);
|
|
@@ -220,8 +230,10 @@ function findRemoverInContent(content, candidates) {
|
|
|
220
230
|
}
|
|
221
231
|
export function buildRegistryLifecycleReport(input) {
|
|
222
232
|
const { projectRoot } = input;
|
|
233
|
+
const scope = input.scope && input.scope.length > 0 ? input.scope : undefined;
|
|
234
|
+
const walkRoot = scope ? join(projectRoot, scope) : projectRoot;
|
|
223
235
|
const files = [];
|
|
224
|
-
walk(
|
|
236
|
+
walk(walkRoot, projectRoot, files);
|
|
225
237
|
const limit = input.limit ?? 2000;
|
|
226
238
|
const scanFiles = files.slice(0, limit);
|
|
227
239
|
const matchedPairs = [];
|
|
@@ -299,6 +311,9 @@ export function buildRegistryLifecycleReport(input) {
|
|
|
299
311
|
return {
|
|
300
312
|
schema: 'sharkcraft.registry-lifecycle/v1',
|
|
301
313
|
filesScanned: scanFiles.length,
|
|
314
|
+
totalFiles: files.length,
|
|
315
|
+
truncated: files.length > scanFiles.length,
|
|
316
|
+
...(scope ? { scope } : {}),
|
|
302
317
|
registersFound,
|
|
303
318
|
matchedPairs,
|
|
304
319
|
missingRemovers,
|
|
@@ -310,7 +325,10 @@ export function buildRegistryLifecycleReport(input) {
|
|
|
310
325
|
export function renderRegistryLifecycleReportText(report) {
|
|
311
326
|
const lines = [];
|
|
312
327
|
lines.push('=== Registry lifecycle ===');
|
|
313
|
-
|
|
328
|
+
if (report.scope)
|
|
329
|
+
lines.push(` scope ${report.scope}`);
|
|
330
|
+
lines.push(` files scanned ${report.filesScanned}` +
|
|
331
|
+
(report.truncated ? ` ! capped (${report.totalFiles} candidates — re-run with --scope <dir> for the rest)` : ''));
|
|
314
332
|
lines.push(` registers found ${report.registersFound}`);
|
|
315
333
|
lines.push(` matched pairs ${report.matchedPairs.length}`);
|
|
316
334
|
lines.push(` missing removers ${report.missingRemovers.length}`);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type AppError, type Result } from '@shrkcrft/core';
|
|
2
|
+
import { type LoadedConfig } from '@shrkcrft/config';
|
|
3
|
+
/**
|
|
4
|
+
* A {@link LoadedConfig} whose four data planes have had pack contributions
|
|
5
|
+
* merged in (local-wins), plus the human-readable notes from that merge.
|
|
6
|
+
*/
|
|
7
|
+
export interface IResolvedProjectConfig extends LoadedConfig {
|
|
8
|
+
/**
|
|
9
|
+
* Notes from the pack-plane merge — missing/invalid pack files, dropped
|
|
10
|
+
* collisions, pack-discovery failures. Empty when there are no packs (or no
|
|
11
|
+
* pack contributions to the four planes). Surfaced by the readers that
|
|
12
|
+
* consume the merged planes (`shrk check wiring`, `registry`, `policy-lint`,
|
|
13
|
+
* `reuse`, `gate`).
|
|
14
|
+
*/
|
|
15
|
+
readonly planeDiagnostics: readonly string[];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Load the project config, then merge pack-contributed `wiringRules` /
|
|
19
|
+
* `registries` / `policyRules` / `reusePrimitives` over the local config
|
|
20
|
+
* (local-wins). Returns the loader error untouched on failure, so callers keep
|
|
21
|
+
* the same "invalid config vs. valid-with-no-rules" distinction they had with
|
|
22
|
+
* {@link loadProjectConfig}. A pack-discovery failure degrades to a diagnostic
|
|
23
|
+
* — it never fails config resolution.
|
|
24
|
+
*/
|
|
25
|
+
export declare function resolveProjectConfig(cwd: string): Promise<Result<IResolvedProjectConfig, AppError>>;
|
|
26
|
+
//# sourceMappingURL=resolve-project-config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve-project-config.d.ts","sourceRoot":"","sources":["../src/resolve-project-config.ts"],"names":[],"mappings":"AAuBA,OAAO,EAGL,KAAK,QAAQ,EAKb,KAAK,MAAM,EACZ,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAML,KAAK,YAAY,EAClB,MAAM,kBAAkB,CAAC;AAG1B;;;GAGG;AACH,MAAM,WAAW,sBAAuB,SAAQ,YAAY;IAC1D;;;;;;OAMG;IACH,QAAQ,CAAC,gBAAgB,EAAE,SAAS,MAAM,EAAE,CAAC;CAC9C;AA+GD;;;;;;;GAOG;AACH,wBAAsB,oBAAoB,CACxC,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,MAAM,CAAC,sBAAsB,EAAE,QAAQ,CAAC,CAAC,CAqDnD"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pack-aware project-config resolution.
|
|
3
|
+
*
|
|
4
|
+
* The four "cross-file invariant as DATA" planes — `wiringRules`, `registries`,
|
|
5
|
+
* `policyRules`, `reusePrimitives` — can be authored inline in a repo's
|
|
6
|
+
* `sharkcraft.config.ts`. They can ALSO be SHIPPED by a framework pack (e.g. a
|
|
7
|
+
* NestJS pack contributing "every @Injectable must be registered in a module"
|
|
8
|
+
* as a wiring rule) via the new `wiringRuleFiles` / `registryFiles` /
|
|
9
|
+
* `policyRuleFiles` / `reusePrimitiveFiles` manifest slots.
|
|
10
|
+
*
|
|
11
|
+
* The merge CANNOT live in `loadProjectConfig`: config sits at layer 3 and packs
|
|
12
|
+
* at layer 6, so config cannot import the pack discovery. The inspector (layer
|
|
13
|
+
* 10, above packs) is the lowest layer that can see both, so the merge seam
|
|
14
|
+
* lives here.
|
|
15
|
+
*
|
|
16
|
+
* Precedence is LOCAL-WINS: a repo's own declaration always beats a pack's, and
|
|
17
|
+
* a pack element whose key collides with a local (or an earlier pack) one is
|
|
18
|
+
* dropped with a diagnostic. Pack elements are validated with the SAME exported
|
|
19
|
+
* zod schemas the config loader uses, so a malformed pack element is skipped
|
|
20
|
+
* (with a diagnostic) rather than crashing config resolution.
|
|
21
|
+
*/
|
|
22
|
+
import { existsSync } from 'node:fs';
|
|
23
|
+
import * as nodePath from 'node:path';
|
|
24
|
+
import { importModuleViaLoader, ok, } from '@shrkcrft/core';
|
|
25
|
+
import { loadProjectConfig, PolicyRuleSchema, RegistryDeclarationSchema, ReusePrimitiveSchema, WiringRuleSchema, } from '@shrkcrft/config';
|
|
26
|
+
import { discoverPacks } from '@shrkcrft/packs';
|
|
27
|
+
/**
|
|
28
|
+
* Generic per-plane load + validate + merge. Seeds the merged map from the
|
|
29
|
+
* LOCAL array (keyed by `keyOf`), then folds in pack elements only when their
|
|
30
|
+
* key is free. Missing files, non-array default exports, schema-invalid
|
|
31
|
+
* elements, and key collisions all become diagnostics and are skipped — never a
|
|
32
|
+
* throw.
|
|
33
|
+
*/
|
|
34
|
+
async function mergePlane(localArr, packContribs, schema, keyOf, planeLabel, diagnostics) {
|
|
35
|
+
const merged = new Map();
|
|
36
|
+
const localKeys = new Set();
|
|
37
|
+
for (const item of localArr) {
|
|
38
|
+
const key = keyOf(item);
|
|
39
|
+
merged.set(key, item);
|
|
40
|
+
localKeys.add(key);
|
|
41
|
+
}
|
|
42
|
+
for (const contrib of packContribs) {
|
|
43
|
+
const full = nodePath.resolve(contrib.packageRoot, contrib.rel);
|
|
44
|
+
if (!existsSync(full)) {
|
|
45
|
+
diagnostics.push(`pack ${contrib.packageName}: missing ${planeLabel} file ${contrib.rel}`);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
let mod;
|
|
49
|
+
try {
|
|
50
|
+
mod = await importModuleViaLoader(full);
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
diagnostics.push(`pack ${contrib.packageName}: failed to load ${planeLabel} file ${contrib.rel} — ${e.message}`);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const arr = mod.default;
|
|
57
|
+
if (!Array.isArray(arr)) {
|
|
58
|
+
diagnostics.push(`pack ${contrib.packageName}: ${planeLabel} file ${contrib.rel} default export is not an array — skipped`);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
for (const raw of arr) {
|
|
62
|
+
const parsed = schema.safeParse(raw);
|
|
63
|
+
if (!parsed.success) {
|
|
64
|
+
const summary = (parsed.error?.issues ?? [])
|
|
65
|
+
.map((iss) => `${iss.path.join('.') || '<root>'}: ${iss.message}`)
|
|
66
|
+
.join('; ') || 'invalid element';
|
|
67
|
+
diagnostics.push(`pack ${contrib.packageName}: invalid ${planeLabel} element in ${contrib.rel} — ${summary} — skipped`);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const item = parsed.data;
|
|
71
|
+
const key = keyOf(item);
|
|
72
|
+
if (merged.has(key)) {
|
|
73
|
+
diagnostics.push(localKeys.has(key)
|
|
74
|
+
? `pack ${contrib.packageName}: ${planeLabel} "${key}" already provided by local config — skipped`
|
|
75
|
+
: `pack ${contrib.packageName}: ${planeLabel} "${key}" already provided — skipped`);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
merged.set(key, item);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return [...merged.values()];
|
|
82
|
+
}
|
|
83
|
+
/** Collect every pack contribution file for one manifest slot across valid packs. */
|
|
84
|
+
function gatherPackContribs(validPacks, slot) {
|
|
85
|
+
const out = [];
|
|
86
|
+
for (const pack of validPacks) {
|
|
87
|
+
// Mirror sharkcraft-inspector's pack-merge: read the slot off the
|
|
88
|
+
// contributions bag with a narrow cast (the new slots are all string[]).
|
|
89
|
+
const contributions = pack.manifest?.contributions;
|
|
90
|
+
const files = contributions?.[slot];
|
|
91
|
+
for (const rel of files ?? []) {
|
|
92
|
+
out.push({ packageName: pack.packageName, packageRoot: pack.packageRoot, rel });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Load the project config, then merge pack-contributed `wiringRules` /
|
|
99
|
+
* `registries` / `policyRules` / `reusePrimitives` over the local config
|
|
100
|
+
* (local-wins). Returns the loader error untouched on failure, so callers keep
|
|
101
|
+
* the same "invalid config vs. valid-with-no-rules" distinction they had with
|
|
102
|
+
* {@link loadProjectConfig}. A pack-discovery failure degrades to a diagnostic
|
|
103
|
+
* — it never fails config resolution.
|
|
104
|
+
*/
|
|
105
|
+
export async function resolveProjectConfig(cwd) {
|
|
106
|
+
const loaded = await loadProjectConfig(cwd);
|
|
107
|
+
if (!loaded.ok)
|
|
108
|
+
return loaded;
|
|
109
|
+
const diagnostics = [];
|
|
110
|
+
const base = loaded.value;
|
|
111
|
+
let validPacks = [];
|
|
112
|
+
try {
|
|
113
|
+
const packs = await discoverPacks({ projectRoot: base.projectRoot });
|
|
114
|
+
validPacks = packs.validPacks;
|
|
115
|
+
}
|
|
116
|
+
catch (e) {
|
|
117
|
+
diagnostics.push(`pack discovery failed — pack-contributed planes skipped: ${e.message}`);
|
|
118
|
+
}
|
|
119
|
+
const wiringRules = await mergePlane(base.config.wiringRules ?? [], gatherPackContribs(validPacks, 'wiringRuleFiles'), WiringRuleSchema, (r) => r.id, 'wiringRule', diagnostics);
|
|
120
|
+
const registries = await mergePlane(base.config.registries ?? [], gatherPackContribs(validPacks, 'registryFiles'), RegistryDeclarationSchema, (r) => r.name, 'registry', diagnostics);
|
|
121
|
+
const policyRules = await mergePlane(base.config.policyRules ?? [], gatherPackContribs(validPacks, 'policyRuleFiles'), PolicyRuleSchema, (r) => r.id, 'policyRule', diagnostics);
|
|
122
|
+
const reusePrimitives = await mergePlane(base.config.reusePrimitives ?? [], gatherPackContribs(validPacks, 'reusePrimitiveFiles'), ReusePrimitiveSchema, (r) => r.symbol, 'reusePrimitive', diagnostics);
|
|
123
|
+
return ok({
|
|
124
|
+
...base,
|
|
125
|
+
config: { ...base.config, wiringRules, registries, policyRules, reusePrimitives },
|
|
126
|
+
planeDiagnostics: diagnostics,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
@@ -17,6 +17,13 @@ import type { ISharkcraftInspection } from './sharkcraft-inspector.js';
|
|
|
17
17
|
* keeping the real post-change gates. Rules carry no verification field, so
|
|
18
18
|
* they are intentionally not a source here.
|
|
19
19
|
*
|
|
20
|
+
* Package-manager templating: any command may use a `<pm-run>` or `<pm>`
|
|
21
|
+
* placeholder that is substituted with the project's detected package manager
|
|
22
|
+
* at consume time (e.g. a pack playbook ships `<pm-run> test` and it resolves
|
|
23
|
+
* to `bun run test` / `npm run test` / `pnpm test` per the target repo). The
|
|
24
|
+
* substitution runs before the placeholder-exclusion check, so a templated
|
|
25
|
+
* gate survives while a truly generative `<task>` step is still dropped.
|
|
26
|
+
*
|
|
20
27
|
* Deterministic, order-preserving, deduped. No commands are executed.
|
|
21
28
|
*/
|
|
22
29
|
export declare function resolveVerificationCommands(inspection: ISharkcraftInspection, options?: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolve-verification-commands.d.ts","sourceRoot":"","sources":["../src/resolve-verification-commands.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"resolve-verification-commands.d.ts","sourceRoot":"","sources":["../src/resolve-verification-commands.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AAEvE;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,2BAA2B,CACzC,UAAU,EAAE,qBAAqB,EACjC,OAAO,GAAE;IACP,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,QAAQ,CAAC,iBAAiB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC3C,GACL,MAAM,EAAE,CA8BV"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { PipelineStepType } from '@shrkcrft/pipelines';
|
|
2
|
+
import { PackageManager, WorkspaceProfile, } from '@shrkcrft/workspace';
|
|
2
3
|
/**
|
|
3
4
|
* Resolve the verification commands an agent should run after a task, grounded
|
|
4
5
|
* in the pack rather than a generic default. Precedence (first non-empty wins):
|
|
@@ -17,9 +18,17 @@ import { PipelineStepType } from '@shrkcrft/pipelines';
|
|
|
17
18
|
* keeping the real post-change gates. Rules carry no verification field, so
|
|
18
19
|
* they are intentionally not a source here.
|
|
19
20
|
*
|
|
21
|
+
* Package-manager templating: any command may use a `<pm-run>` or `<pm>`
|
|
22
|
+
* placeholder that is substituted with the project's detected package manager
|
|
23
|
+
* at consume time (e.g. a pack playbook ships `<pm-run> test` and it resolves
|
|
24
|
+
* to `bun run test` / `npm run test` / `pnpm test` per the target repo). The
|
|
25
|
+
* substitution runs before the placeholder-exclusion check, so a templated
|
|
26
|
+
* gate survives while a truly generative `<task>` step is still dropped.
|
|
27
|
+
*
|
|
20
28
|
* Deterministic, order-preserving, deduped. No commands are executed.
|
|
21
29
|
*/
|
|
22
30
|
export function resolveVerificationCommands(inspection, options = {}) {
|
|
31
|
+
const ws = inspection.workspace;
|
|
23
32
|
const fromPipelines = [];
|
|
24
33
|
for (const id of options.pipelineIds ?? []) {
|
|
25
34
|
const pipeline = inspection.pipelineRegistry.get(id);
|
|
@@ -31,7 +40,7 @@ export function resolveVerificationCommands(inspection, options = {}) {
|
|
|
31
40
|
if (step.required === false)
|
|
32
41
|
continue;
|
|
33
42
|
for (const raw of step.cliCommands ?? []) {
|
|
34
|
-
const cmd = raw.trim();
|
|
43
|
+
const cmd = substitutePmPlaceholders(raw.trim(), ws);
|
|
35
44
|
if (cmd.length > 0 && !cmd.includes('<'))
|
|
36
45
|
fromPipelines.push(cmd);
|
|
37
46
|
}
|
|
@@ -42,13 +51,53 @@ export function resolveVerificationCommands(inspection, options = {}) {
|
|
|
42
51
|
const cfg = inspection.config;
|
|
43
52
|
const fromConfig = [];
|
|
44
53
|
for (const vc of cfg?.verificationCommands ?? []) {
|
|
45
|
-
const cmd = vc?.command?.trim();
|
|
46
|
-
if (cmd
|
|
54
|
+
const cmd = substitutePmPlaceholders(vc?.command?.trim() ?? '', ws);
|
|
55
|
+
if (cmd.length > 0)
|
|
47
56
|
fromConfig.push(cmd);
|
|
48
57
|
}
|
|
49
58
|
if (fromConfig.length > 0)
|
|
50
59
|
return dedupe(fromConfig);
|
|
51
|
-
return dedupe(options.knowledgeDefaults ?? []);
|
|
60
|
+
return dedupe((options.knowledgeDefaults ?? []).map((c) => substitutePmPlaceholders(c, ws)));
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Replace package-manager placeholders in a single command with the project's
|
|
64
|
+
* detected toolchain. `<pm-run>` → the run-prefix (`bun run`, `npm run`,
|
|
65
|
+
* `pnpm`, `yarn`); `<pm>` → the bare manager name (`bun`, `npm`, `pnpm`,
|
|
66
|
+
* `yarn`). A no-op (and zero workspace access) when the command carries no
|
|
67
|
+
* `<pm` token, so non-templated callers and stubbed inspections are unaffected.
|
|
68
|
+
*/
|
|
69
|
+
function substitutePmPlaceholders(command, ws) {
|
|
70
|
+
if (!command.includes('<pm'))
|
|
71
|
+
return command;
|
|
72
|
+
const manager = effectivePackageManager(ws);
|
|
73
|
+
return command
|
|
74
|
+
.replaceAll('<pm-run>', packageManagerRunPrefix(manager))
|
|
75
|
+
.replaceAll('<pm>', bareManager(manager));
|
|
76
|
+
}
|
|
77
|
+
function effectivePackageManager(ws) {
|
|
78
|
+
const detected = ws?.packageManager?.manager;
|
|
79
|
+
if (detected && detected !== PackageManager.Unknown)
|
|
80
|
+
return detected;
|
|
81
|
+
if (ws?.profiles?.includes(WorkspaceProfile.HasBun))
|
|
82
|
+
return PackageManager.Bun;
|
|
83
|
+
return PackageManager.Npm;
|
|
84
|
+
}
|
|
85
|
+
function packageManagerRunPrefix(manager) {
|
|
86
|
+
switch (manager) {
|
|
87
|
+
case PackageManager.Bun:
|
|
88
|
+
return 'bun run';
|
|
89
|
+
case PackageManager.Pnpm:
|
|
90
|
+
return 'pnpm';
|
|
91
|
+
case PackageManager.Yarn:
|
|
92
|
+
return 'yarn';
|
|
93
|
+
case PackageManager.Npm:
|
|
94
|
+
return 'npm run';
|
|
95
|
+
default:
|
|
96
|
+
return 'npm run';
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function bareManager(manager) {
|
|
100
|
+
return manager === PackageManager.Unknown ? 'npm' : manager;
|
|
52
101
|
}
|
|
53
102
|
function dedupe(items) {
|
|
54
103
|
return [...new Set(items)];
|
package/dist/review-packet.d.ts
CHANGED
|
@@ -44,6 +44,14 @@ export interface IBuildReviewPacketOptions {
|
|
|
44
44
|
since?: string;
|
|
45
45
|
staged?: boolean;
|
|
46
46
|
files?: readonly string[];
|
|
47
|
+
/**
|
|
48
|
+
* Working-tree default only: when undefined/true, non-ignored untracked
|
|
49
|
+
* files are included — so a just-generated, never-staged source file is
|
|
50
|
+
* visible to the review (and to the missing-test heuristic). Set false to
|
|
51
|
+
* restore the legacy tracked-only `git diff` view. Ignored when
|
|
52
|
+
* `since`/`staged`/`files` is set.
|
|
53
|
+
*/
|
|
54
|
+
untracked?: boolean;
|
|
47
55
|
}
|
|
48
56
|
export declare function buildReviewPacket(inspection: ISharkcraftInspection, options?: IBuildReviewPacketOptions): IReviewPacket;
|
|
49
57
|
//# sourceMappingURL=review-packet.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"review-packet.d.ts","sourceRoot":"","sources":["../src/review-packet.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"review-packet.d.ts","sourceRoot":"","sources":["../src/review-packet.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AAKvE,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC,kEAAkE;IAClE,aAAa,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,8CAA8C;IAC9C,aAAa,EAAE,SAAS;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACxE,2CAA2C;IAC3C,iBAAiB,EAAE,SAAS;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC3D,iBAAiB,EAAE,SAAS;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC5D,2DAA2D;IAC3D,kBAAkB,EAAE,SAAS;QAC3B,MAAM,EAAE,MAAM,CAAC;QACf,IAAI,EAAE,MAAM,CAAC;QACb,eAAe,EAAE,MAAM,CAAC;QACxB,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,EAAE,MAAM,CAAC;KACjB,EAAE,CAAC;IACJ,+DAA+D;IAC/D,qBAAqB,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,4DAA4D;IAC5D,oBAAoB,EAAE,SAAS,MAAM,EAAE,CAAC;IACxC,mEAAmE;IACnE,oBAAoB,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,yBAAyB;IACxC;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1B;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAwCD,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,qBAAqB,EACjC,OAAO,GAAE,yBAA8B,GACtC,aAAa,CA+Ef"}
|
package/dist/review-packet.js
CHANGED
|
@@ -1,24 +1,23 @@
|
|
|
1
|
-
import { spawnSync } from 'node:child_process';
|
|
2
1
|
import { evaluateBoundaries, loadTsconfigPaths, scanImports } from '@shrkcrft/boundaries';
|
|
3
2
|
import { matchAffectedConventions } from '@shrkcrft/paths';
|
|
4
3
|
import { resolveVerificationCommands } from "./resolve-verification-commands.js";
|
|
5
4
|
import { rankAll } from "./task-ranker.js";
|
|
5
|
+
import { getChangedFiles } from "./git-helpers.js";
|
|
6
6
|
function gitDiffFiles(cwd, opts) {
|
|
7
7
|
if (opts.files && opts.files.length > 0)
|
|
8
8
|
return [...opts.files];
|
|
9
|
-
const args = ['diff', '--name-only'];
|
|
10
9
|
if (opts.staged)
|
|
11
|
-
|
|
10
|
+
return getChangedFiles(cwd, { staged: true });
|
|
12
11
|
if (opts.since)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
12
|
+
return getChangedFiles(cwd, { since: opts.since });
|
|
13
|
+
// Default working-tree view: include non-ignored untracked files so a
|
|
14
|
+
// just-generated, never-staged source file is visible to review (and to the
|
|
15
|
+
// "is each new src/ file tested?" heuristic below). Mirrors `shrk changes
|
|
16
|
+
// summary` and every other working-tree caller; `.gitignore` stays honored.
|
|
17
|
+
// `{ untracked: false }` restores the legacy tracked-only `git diff` view.
|
|
18
|
+
if (opts.untracked === false)
|
|
19
|
+
return getChangedFiles(cwd, {});
|
|
20
|
+
return getChangedFiles(cwd, { includeWorktree: true });
|
|
22
21
|
}
|
|
23
22
|
function buildPseudoTask(files) {
|
|
24
23
|
// Use the top-level dir of each file as a pseudo-task description so the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sharkcraft-inspector.d.ts","sourceRoot":"","sources":["../src/sharkcraft-inspector.ts"],"names":[],"mappings":"AAOA,OAAO,EAAoB,KAAK,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAC/E,OAAO,EAAE,KAAK,iBAAiB,EAAqB,MAAM,kBAAkB,CAAC;AAC7E,OAAO,EACL,KAAK,eAAe,EACpB,KAAK,yBAAyB,EAC9B,cAAc,EAIf,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC9C,OAAO,EAAE,KAAK,mBAAmB,EAAyB,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACxG,OAAO,EAAE,KAAK,mBAAmB,EAAyB,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACxG,OAAO,EAAiB,KAAK,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAC3E,OAAO,EAAwC,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACzF,OAAO,EAAE,gBAAgB,EAA6B,MAAM,sBAAsB,CAAC;AACnF,OAAO,EAAqC,KAAK,aAAa,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"sharkcraft-inspector.d.ts","sourceRoot":"","sources":["../src/sharkcraft-inspector.ts"],"names":[],"mappings":"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"}
|
|
@@ -14,6 +14,7 @@ import { BoundaryRegistry, loadBoundaryRulesFromFile } from '@shrkcrft/boundarie
|
|
|
14
14
|
import { DoctorSeverity } from "./doctor-result.js";
|
|
15
15
|
import { diagnoseActionHints } from "./action-hint-diagnostics.js";
|
|
16
16
|
import { buildCodeIntelligenceChecks } from "./code-intelligence-doctor.js";
|
|
17
|
+
import { loadSearchTuning } from "./search-tuning-registry.js";
|
|
17
18
|
import { buildDelegateRecipeChecks } from "./delegate-doctor.js";
|
|
18
19
|
import { computeFileFingerprint, createInspectorCache, } from "./inspector-cache.js";
|
|
19
20
|
import { DEFAULT_SLOW_LOADER_THRESHOLD_MS, LARGE_FILE_THRESHOLD_BYTES, } from "./loader-diagnostics.js";
|
|
@@ -422,8 +423,23 @@ export async function inspectSharkcraft(options = {}) {
|
|
|
422
423
|
await loadFile(rel, 'knowledge');
|
|
423
424
|
for (const rel of c.ruleFiles ?? [])
|
|
424
425
|
await loadFile(rel, 'rules');
|
|
425
|
-
|
|
426
|
+
const loadedPathRels = new Set();
|
|
427
|
+
for (const rel of c.pathFiles ?? []) {
|
|
428
|
+
loadedPathRels.add(rel);
|
|
426
429
|
await loadFile(rel, 'paths');
|
|
430
|
+
}
|
|
431
|
+
// `pathConventionFiles` is a manifest slot distinct from `pathFiles` (see the
|
|
432
|
+
// plugin-api comment "separate from pathFiles"), but it still feeds the path
|
|
433
|
+
// domain. Previously NO loader consumed it, so a pack shipping conventions
|
|
434
|
+
// here loaded nothing. Load it through the same path loader, skipping any rel
|
|
435
|
+
// already handled by `pathFiles` so a file listed in both slots doesn't
|
|
436
|
+
// double-load (entry ids are also deduped by `loadFile`).
|
|
437
|
+
for (const rel of c.pathConventionFiles ?? []) {
|
|
438
|
+
if (loadedPathRels.has(rel))
|
|
439
|
+
continue;
|
|
440
|
+
loadedPathRels.add(rel);
|
|
441
|
+
await loadFile(rel, 'paths');
|
|
442
|
+
}
|
|
427
443
|
for (const rel of c.docsFiles ?? [])
|
|
428
444
|
await loadFile(rel, 'docs');
|
|
429
445
|
for (const rel of c.templateFiles ?? [])
|
|
@@ -550,7 +566,7 @@ export async function inspectSharkcraft(options = {}) {
|
|
|
550
566
|
if (tracked.skipped)
|
|
551
567
|
warnings.push(...tracked.warnings);
|
|
552
568
|
}
|
|
553
|
-
|
|
569
|
+
const inspection = {
|
|
554
570
|
projectRoot: workspace.projectRoot,
|
|
555
571
|
workspace,
|
|
556
572
|
hasSharkcraftFolder: workspace.hasSharkcraftFolder,
|
|
@@ -581,6 +597,16 @@ export async function inspectSharkcraft(options = {}) {
|
|
|
581
597
|
cacheEnabled: cache.enabled,
|
|
582
598
|
cacheDir: cache.dir,
|
|
583
599
|
};
|
|
600
|
+
// Warm the pack search-tuning cache (best-effort; existsSync-gated + cached
|
|
601
|
+
// per projectRoot) so the synchronous buildTaskPacket → rankAll path can read
|
|
602
|
+
// listSearchTuning and apply pack boostIds — the same tuning `shrk search` uses.
|
|
603
|
+
try {
|
|
604
|
+
await loadSearchTuning(inspection);
|
|
605
|
+
}
|
|
606
|
+
catch {
|
|
607
|
+
/* tuning is best-effort */
|
|
608
|
+
}
|
|
609
|
+
return inspection;
|
|
584
610
|
}
|
|
585
611
|
export function runDoctor(inspection) {
|
|
586
612
|
const checks = [];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spec-evidence.test.d.ts","sourceRoot":"","sources":["../../../src/spec/__tests__/spec-evidence.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import { mapChecklistToEvidence, } from "../spec-evidence.js";
|
|
3
|
+
describe('mapChecklistToEvidence', () => {
|
|
4
|
+
it('marks exactly the unimplemented criterion UNMET', () => {
|
|
5
|
+
const criteria = [
|
|
6
|
+
{ id: 'ac-1', text: 'Add a coverage flag to spec verify' },
|
|
7
|
+
{ id: 'ac-2', text: 'Export a telemetry dashboard widget' },
|
|
8
|
+
];
|
|
9
|
+
// The changeset implements ac-1 only (coverage), nothing for ac-2.
|
|
10
|
+
const changedFiles = [
|
|
11
|
+
'packages/cli/src/commands/spec.command.ts',
|
|
12
|
+
'packages/inspector/src/spec/__tests__/spec-coverage.test.ts',
|
|
13
|
+
];
|
|
14
|
+
const fileContents = {
|
|
15
|
+
'packages/cli/src/commands/spec.command.ts': [
|
|
16
|
+
'export function specVerifyCoverageReport() {',
|
|
17
|
+
' return mapChecklistToEvidence();',
|
|
18
|
+
'}',
|
|
19
|
+
].join('\n'),
|
|
20
|
+
'packages/inspector/src/spec/__tests__/spec-coverage.test.ts': [
|
|
21
|
+
"import { specVerifyCoverageReport } from '../x.ts';",
|
|
22
|
+
"it('reports coverage', () => {});",
|
|
23
|
+
].join('\n'),
|
|
24
|
+
};
|
|
25
|
+
const report = mapChecklistToEvidence({ criteria, changedFiles, fileContents });
|
|
26
|
+
expect(report.criteria).toHaveLength(2);
|
|
27
|
+
const covered = report.criteria.find((c) => c.id === 'ac-1');
|
|
28
|
+
const unmet = report.criteria.find((c) => c.id === 'ac-2');
|
|
29
|
+
expect(covered.covered).toBe(true);
|
|
30
|
+
expect(covered.evidence.length).toBeGreaterThan(0);
|
|
31
|
+
// The exported symbol naming the feature is concrete backing evidence.
|
|
32
|
+
expect(covered.evidence.some((e) => e.kind === 'symbol')).toBe(true);
|
|
33
|
+
expect(unmet.covered).toBe(false);
|
|
34
|
+
expect(unmet.evidence).toHaveLength(0);
|
|
35
|
+
expect(report.coveredCount).toBe(1);
|
|
36
|
+
expect(report.unmetCount).toBe(1);
|
|
37
|
+
// Exactly the unimplemented criterion is UNMET.
|
|
38
|
+
expect(report.criteria.filter((c) => !c.covered).map((c) => c.id)).toEqual(['ac-2']);
|
|
39
|
+
});
|
|
40
|
+
it('recognises a new test file as backing evidence', () => {
|
|
41
|
+
const report = mapChecklistToEvidence({
|
|
42
|
+
criteria: [{ id: 'ac-1', text: 'Wire a deterministic compaction pass' }],
|
|
43
|
+
changedFiles: ['packages/compress/src/__tests__/compaction.test.ts'],
|
|
44
|
+
fileContents: {
|
|
45
|
+
'packages/compress/src/__tests__/compaction.test.ts': "it('compacts', () => {});",
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
const c = report.criteria[0];
|
|
49
|
+
expect(c.covered).toBe(true);
|
|
50
|
+
expect(c.evidence.every((e) => e.kind === 'test')).toBe(true);
|
|
51
|
+
expect(report.unmetCount).toBe(0);
|
|
52
|
+
});
|
|
53
|
+
it('flags a registration / array membership as evidence', () => {
|
|
54
|
+
const report = mapChecklistToEvidence({
|
|
55
|
+
criteria: [{ id: 'ac-1', text: 'Register the telemetry handler' }],
|
|
56
|
+
changedFiles: ['packages/cli/src/registry.ts'],
|
|
57
|
+
fileContents: {
|
|
58
|
+
'packages/cli/src/registry.ts': [
|
|
59
|
+
'const handlerMap = {',
|
|
60
|
+
' telemetry: telemetryHandler,',
|
|
61
|
+
'};',
|
|
62
|
+
].join('\n'),
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
const c = report.criteria[0];
|
|
66
|
+
expect(c.covered).toBe(true);
|
|
67
|
+
expect(c.evidence.some((e) => e.kind === 'registration' || e.kind === 'route')).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
it('reports every criterion UNMET when the changeset is empty', () => {
|
|
70
|
+
const report = mapChecklistToEvidence({
|
|
71
|
+
criteria: [
|
|
72
|
+
{ id: 'ac-1', text: 'Add coverage mapping' },
|
|
73
|
+
{ id: 'ac-2', text: 'Add telemetry dashboard' },
|
|
74
|
+
],
|
|
75
|
+
changedFiles: [],
|
|
76
|
+
fileContents: {},
|
|
77
|
+
});
|
|
78
|
+
expect(report.coveredCount).toBe(0);
|
|
79
|
+
expect(report.unmetCount).toBe(2);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic feature-checklist → changeset-evidence mapper.
|
|
3
|
+
*
|
|
4
|
+
* For each acceptance criterion of a spec, scan the changeset (the set of
|
|
5
|
+
* tracked + untracked changed files plus their current contents) for
|
|
6
|
+
* backing evidence:
|
|
7
|
+
*
|
|
8
|
+
* - a new exported symbol whose name relates to the criterion,
|
|
9
|
+
* - a new registration / array membership,
|
|
10
|
+
* - a new route / command registration,
|
|
11
|
+
* - a touched companion file whose path relates to the criterion,
|
|
12
|
+
* - a new test file.
|
|
13
|
+
*
|
|
14
|
+
* A criterion with zero footprint across the whole changeset is `UNMET`
|
|
15
|
+
* (claimed-but-unimplemented). There is NO model in the loop: matching is a
|
|
16
|
+
* pure keyword/identifier intersection between the criterion text and the
|
|
17
|
+
* identifiers/paths added in the changeset.
|
|
18
|
+
*
|
|
19
|
+
* This module is read-only and side-effect free; the caller is responsible
|
|
20
|
+
* for collecting `changedFiles` + `fileContents` (see the CLI
|
|
21
|
+
* `collectChangedPaths` helper and `spec verify --coverage`).
|
|
22
|
+
*/
|
|
23
|
+
/** The shape of evidence backing (or claiming to back) a criterion. */
|
|
24
|
+
export type SpecEvidenceKind = 'symbol' | 'registration' | 'route' | 'companion' | 'test';
|
|
25
|
+
export interface ISpecEvidenceItem {
|
|
26
|
+
readonly kind: SpecEvidenceKind;
|
|
27
|
+
/** Changeset-relative path the evidence was found in. */
|
|
28
|
+
readonly file: string;
|
|
29
|
+
/** The matched identifier / path token / line fragment. */
|
|
30
|
+
readonly detail: string;
|
|
31
|
+
/** The criterion keyword that produced the match. */
|
|
32
|
+
readonly matched: string;
|
|
33
|
+
}
|
|
34
|
+
export interface ICriterionCoverage {
|
|
35
|
+
readonly id: string;
|
|
36
|
+
readonly text: string;
|
|
37
|
+
/** True iff at least one piece of evidence was found in the changeset. */
|
|
38
|
+
readonly covered: boolean;
|
|
39
|
+
readonly evidence: readonly ISpecEvidenceItem[];
|
|
40
|
+
}
|
|
41
|
+
export interface IChecklistCriterionInput {
|
|
42
|
+
readonly id: string;
|
|
43
|
+
readonly text: string;
|
|
44
|
+
}
|
|
45
|
+
export interface IMapChecklistToEvidenceInput {
|
|
46
|
+
readonly criteria: readonly IChecklistCriterionInput[];
|
|
47
|
+
readonly changedFiles: readonly string[];
|
|
48
|
+
/** Map of changed-file path → current file contents. Missing = treated as empty. */
|
|
49
|
+
readonly fileContents: Readonly<Record<string, string>>;
|
|
50
|
+
}
|
|
51
|
+
export interface IChecklistEvidenceReport {
|
|
52
|
+
readonly criteria: readonly ICriterionCoverage[];
|
|
53
|
+
readonly coveredCount: number;
|
|
54
|
+
/** Criteria with zero footprint in the changeset (claimed-but-unimplemented). */
|
|
55
|
+
readonly unmetCount: number;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Map a spec checklist to changeset evidence. Pure; deterministic for a
|
|
59
|
+
* given input (stable iteration over `changedFiles` and sorted keywords).
|
|
60
|
+
*/
|
|
61
|
+
export declare function mapChecklistToEvidence(input: IMapChecklistToEvidenceInput): IChecklistEvidenceReport;
|
|
62
|
+
//# sourceMappingURL=spec-evidence.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spec-evidence.d.ts","sourceRoot":"","sources":["../../src/spec/spec-evidence.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,uEAAuE;AACvE,MAAM,MAAM,gBAAgB,GAAG,QAAQ,GAAG,cAAc,GAAG,OAAO,GAAG,WAAW,GAAG,MAAM,CAAC;AAE1F,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;IAChC,yDAAyD;IACzD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,2DAA2D;IAC3D,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,qDAAqD;IACrD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,0EAA0E;IAC1E,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,SAAS,iBAAiB,EAAE,CAAC;CACjD;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,CAAC,QAAQ,EAAE,SAAS,wBAAwB,EAAE,CAAC;IACvD,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,oFAAoF;IACpF,QAAQ,CAAC,YAAY,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CACzD;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,QAAQ,EAAE,SAAS,kBAAkB,EAAE,CAAC;IACjD,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,iFAAiF;IACjF,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAgCD;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,4BAA4B,GAClC,wBAAwB,CAQ1B"}
|