@shrkcrft/rule-graph 0.1.0-alpha.10
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/bridge/bridge-builder.d.ts +24 -0
- package/dist/bridge/bridge-builder.d.ts.map +1 -0
- package/dist/bridge/bridge-builder.js +208 -0
- package/dist/bridge/knowledge-rule-matching.d.ts +33 -0
- package/dist/bridge/knowledge-rule-matching.d.ts.map +1 -0
- package/dist/bridge/knowledge-rule-matching.js +65 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/query/rule-graph-query-api.d.ts +37 -0
- package/dist/query/rule-graph-query-api.d.ts.map +1 -0
- package/dist/query/rule-graph-query-api.js +107 -0
- package/dist/schema/bridge-schema.d.ts +32 -0
- package/dist/schema/bridge-schema.d.ts.map +1 -0
- package/dist/schema/bridge-schema.js +1 -0
- package/dist/store/bridge-store.d.ts +19 -0
- package/dist/store/bridge-store.d.ts.map +1 -0
- package/dist/store/bridge-store.js +145 -0
- package/package.json +53 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type ISharkcraftInspection } from '@shrkcrft/inspector';
|
|
2
|
+
import type { IBridgeManifest } from '../schema/bridge-schema.js';
|
|
3
|
+
export interface IBridgeBuildOptions {
|
|
4
|
+
projectRoot: string;
|
|
5
|
+
/** Optional pre-loaded inspection; built on the fly when absent. */
|
|
6
|
+
inspection?: ISharkcraftInspection;
|
|
7
|
+
}
|
|
8
|
+
export interface IBridgeBuildResult {
|
|
9
|
+
manifest: IBridgeManifest;
|
|
10
|
+
durationMs: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Build bridge edges from the code graph (file nodes) to asset
|
|
14
|
+
* registries (boundary rules, path conventions, templates).
|
|
15
|
+
*
|
|
16
|
+
* Inputs:
|
|
17
|
+
* - graph store (must already exist; run `shrk graph index` first)
|
|
18
|
+
* - inspection (defaults to `inspectSharkcraft({ cwd: projectRoot })`)
|
|
19
|
+
*
|
|
20
|
+
* Output: writes to `.sharkcraft/bridge/` with schema
|
|
21
|
+
* `sharkcraft.rule-graph/v1`.
|
|
22
|
+
*/
|
|
23
|
+
export declare function buildBridge(options: IBridgeBuildOptions): Promise<IBridgeBuildResult>;
|
|
24
|
+
//# sourceMappingURL=bridge-builder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bridge-builder.d.ts","sourceRoot":"","sources":["../../src/bridge/bridge-builder.ts"],"names":[],"mappings":"AASA,OAAO,EAEL,KAAK,qBAAqB,EAC3B,MAAM,qBAAqB,CAAC;AAE7B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAKlE,MAAM,WAAW,mBAAmB;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,oEAAoE;IACpE,UAAU,CAAC,EAAE,qBAAqB,CAAC;CACpC;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,eAAe,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,WAAW,CAC/B,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,kBAAkB,CAAC,CAkJ7B"}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { globToRegex } from '@shrkcrft/boundaries';
|
|
3
|
+
import { EdgeKind, GraphStore, NodeKind, } from '@shrkcrft/graph';
|
|
4
|
+
import { inspectSharkcraft, } from '@shrkcrft/inspector';
|
|
5
|
+
import { BridgeStore } from "../store/bridge-store.js";
|
|
6
|
+
import { deriveApplicability } from "./knowledge-rule-matching.js";
|
|
7
|
+
const BRIDGE_SOURCE = 'rule-graph-bridge@v1';
|
|
8
|
+
/**
|
|
9
|
+
* Build bridge edges from the code graph (file nodes) to asset
|
|
10
|
+
* registries (boundary rules, path conventions, templates).
|
|
11
|
+
*
|
|
12
|
+
* Inputs:
|
|
13
|
+
* - graph store (must already exist; run `shrk graph index` first)
|
|
14
|
+
* - inspection (defaults to `inspectSharkcraft({ cwd: projectRoot })`)
|
|
15
|
+
*
|
|
16
|
+
* Output: writes to `.sharkcraft/bridge/` with schema
|
|
17
|
+
* `sharkcraft.rule-graph/v1`.
|
|
18
|
+
*/
|
|
19
|
+
export async function buildBridge(options) {
|
|
20
|
+
const start = Date.now();
|
|
21
|
+
const { projectRoot } = options;
|
|
22
|
+
const graphStore = new GraphStore(projectRoot);
|
|
23
|
+
if (!graphStore.exists()) {
|
|
24
|
+
throw new Error("code-graph store missing. Run 'shrk graph index' before 'shrk rule-graph index'.");
|
|
25
|
+
}
|
|
26
|
+
const graph = graphStore.loadSnapshot();
|
|
27
|
+
const inspection = options.inspection ?? (await inspectSharkcraft({ cwd: projectRoot }));
|
|
28
|
+
const files = [];
|
|
29
|
+
for (const n of graph.nodes.values()) {
|
|
30
|
+
if (n.kind === NodeKind.File && n.path)
|
|
31
|
+
files.push(n);
|
|
32
|
+
}
|
|
33
|
+
const nodes = [];
|
|
34
|
+
const edges = [];
|
|
35
|
+
const sourceCounts = { rule: 0, path: 0, template: 0 };
|
|
36
|
+
/**
|
|
37
|
+
* Track which file ids have at least one `applies-rule` edge — this
|
|
38
|
+
* is the set that backs the §3.2 "bridge coverage" doctor check. We
|
|
39
|
+
* deliberately exclude `matches-path` and `covered-by-template` here
|
|
40
|
+
* (those signal location / generation, not policy).
|
|
41
|
+
*/
|
|
42
|
+
const filesWithRule = new Set();
|
|
43
|
+
// ── Boundary rules ─────────────────────────────────────────────────
|
|
44
|
+
const boundaries = inspection.boundaryRegistry.list();
|
|
45
|
+
for (const b of boundaries) {
|
|
46
|
+
nodes.push({
|
|
47
|
+
id: `boundary:${b.id}`,
|
|
48
|
+
kind: NodeKind.Boundary,
|
|
49
|
+
label: b.title ?? b.id,
|
|
50
|
+
data: {
|
|
51
|
+
severity: b.severity ?? 'error',
|
|
52
|
+
...(b.tags ? { tags: [...b.tags] } : {}),
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
const regexes = b.from.map((p) => globToRegex(p));
|
|
56
|
+
for (const f of files) {
|
|
57
|
+
if (!regexes.some((re) => re.test(f.path)))
|
|
58
|
+
continue;
|
|
59
|
+
edges.push(edge(f.id, `boundary:${b.id}`, EdgeKind.AppliesRule, {
|
|
60
|
+
source: 'boundary',
|
|
61
|
+
severity: b.severity ?? 'error',
|
|
62
|
+
}));
|
|
63
|
+
sourceCounts['rule'] += 1;
|
|
64
|
+
filesWithRule.add(f.id);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// ── Knowledge rules (IKnowledgeEntry type=rule) ────────────────────
|
|
68
|
+
// Heuristic bridge via metadata.appliesTo or tag-based fallback. See
|
|
69
|
+
// `deriveApplicability` for the rationale.
|
|
70
|
+
const knowledgeRules = inspection.ruleService.list();
|
|
71
|
+
for (const r of knowledgeRules) {
|
|
72
|
+
const applicability = deriveApplicability(r);
|
|
73
|
+
if (applicability.source === 'none')
|
|
74
|
+
continue;
|
|
75
|
+
nodes.push({
|
|
76
|
+
id: `rule:${r.id}`,
|
|
77
|
+
kind: NodeKind.Rule,
|
|
78
|
+
label: r.title ?? r.id,
|
|
79
|
+
data: {
|
|
80
|
+
applicabilitySource: applicability.source,
|
|
81
|
+
...(r.tags ? { tags: [...r.tags] } : {}),
|
|
82
|
+
...(r.priority ? { priority: r.priority } : {}),
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
const regexes = applicability.patterns.map((p) => globToRegex(p));
|
|
86
|
+
const tagSet = new Set(applicability.fileTags);
|
|
87
|
+
const severity = (r.priority === 'critical' || r.priority === 'high') ? 'error' : 'warning';
|
|
88
|
+
for (const f of files) {
|
|
89
|
+
const pathMatch = regexes.some((re) => re.test(f.path));
|
|
90
|
+
const tagMatch = tagSet.size > 0 && (f.tags ?? []).some((t) => tagSet.has(t));
|
|
91
|
+
if (!pathMatch && !tagMatch)
|
|
92
|
+
continue;
|
|
93
|
+
edges.push(edge(f.id, `rule:${r.id}`, EdgeKind.AppliesRule, {
|
|
94
|
+
source: 'knowledge',
|
|
95
|
+
severity,
|
|
96
|
+
via: pathMatch ? 'path' : 'tag',
|
|
97
|
+
}));
|
|
98
|
+
sourceCounts['rule'] += 1;
|
|
99
|
+
filesWithRule.add(f.id);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// ── Path conventions ───────────────────────────────────────────────
|
|
103
|
+
const paths = inspection.pathService.list();
|
|
104
|
+
for (const p of paths) {
|
|
105
|
+
const target = p.metadata?.path ?? '';
|
|
106
|
+
if (!target)
|
|
107
|
+
continue;
|
|
108
|
+
nodes.push({
|
|
109
|
+
id: `path:${p.id}`,
|
|
110
|
+
kind: NodeKind.Path,
|
|
111
|
+
label: p.title ?? p.id,
|
|
112
|
+
data: { target },
|
|
113
|
+
});
|
|
114
|
+
const prefix = target.replace(/\/+$/, '');
|
|
115
|
+
for (const f of files) {
|
|
116
|
+
if (!isUnderPrefix(f.path, prefix))
|
|
117
|
+
continue;
|
|
118
|
+
edges.push(edge(f.id, `path:${p.id}`, EdgeKind.MatchesPath, { prefix }));
|
|
119
|
+
sourceCounts['path'] += 1;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// ── Templates (string + invertible-function targetPath) ────────────
|
|
123
|
+
const templates = inspection.templateRegistry.list();
|
|
124
|
+
for (const t of templates) {
|
|
125
|
+
nodes.push({
|
|
126
|
+
id: `template:${t.id}`,
|
|
127
|
+
kind: NodeKind.Template,
|
|
128
|
+
label: t.name ?? t.id,
|
|
129
|
+
});
|
|
130
|
+
const pattern = resolveTemplatePattern(t);
|
|
131
|
+
if (!pattern)
|
|
132
|
+
continue;
|
|
133
|
+
const re = globToRegex(pattern);
|
|
134
|
+
for (const f of files) {
|
|
135
|
+
if (!re.test(f.path))
|
|
136
|
+
continue;
|
|
137
|
+
edges.push(edge(f.id, `template:${t.id}`, EdgeKind.CoveredByTemplate, { pattern }));
|
|
138
|
+
sourceCounts['template'] += 1;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const store = new BridgeStore(projectRoot);
|
|
142
|
+
const filesTotal = files.length;
|
|
143
|
+
const filesCoveredByRules = filesWithRule.size;
|
|
144
|
+
const manifest = store.writeSnapshot(nodes, edges, {
|
|
145
|
+
projectRoot,
|
|
146
|
+
lastBuiltAt: new Date().toISOString(),
|
|
147
|
+
lastBuildDurationMs: Date.now() - start,
|
|
148
|
+
nodesByKind: {},
|
|
149
|
+
edgesByKind: {},
|
|
150
|
+
sourceCounts,
|
|
151
|
+
filesTotal,
|
|
152
|
+
filesCoveredByRules,
|
|
153
|
+
filesUncoveredByRules: filesTotal - filesCoveredByRules,
|
|
154
|
+
});
|
|
155
|
+
return { manifest, durationMs: Date.now() - start };
|
|
156
|
+
}
|
|
157
|
+
function edge(from, to, kind, data) {
|
|
158
|
+
return {
|
|
159
|
+
id: createHash('sha1').update(`${from}|${to}|${kind}`).digest('hex'),
|
|
160
|
+
from,
|
|
161
|
+
to,
|
|
162
|
+
kind,
|
|
163
|
+
source: BRIDGE_SOURCE,
|
|
164
|
+
...(data ? { data } : {}),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function isUnderPrefix(filePath, prefix) {
|
|
168
|
+
if (!prefix)
|
|
169
|
+
return false;
|
|
170
|
+
if (prefix === '.' || prefix === '/')
|
|
171
|
+
return true;
|
|
172
|
+
return filePath === prefix || filePath.startsWith(prefix + '/');
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Invert a template's path resolver to a glob pattern by substituting
|
|
176
|
+
* `*` for every declared variable. Handles:
|
|
177
|
+
* - string targetPath
|
|
178
|
+
* - function targetPath called with { var: '*' } for each declared var
|
|
179
|
+
* - first-file targetPath from files() / changes() resolvers
|
|
180
|
+
*
|
|
181
|
+
* Returns undefined when the template doesn't expose a single-pattern
|
|
182
|
+
* target (multi-file with divergent paths) or when the resolver throws.
|
|
183
|
+
*/
|
|
184
|
+
function resolveTemplatePattern(t) {
|
|
185
|
+
const dummy = {};
|
|
186
|
+
for (const v of t.variables ?? [])
|
|
187
|
+
dummy[v.name] = '*';
|
|
188
|
+
try {
|
|
189
|
+
if (typeof t.targetPath === 'string')
|
|
190
|
+
return t.targetPath;
|
|
191
|
+
if (typeof t.targetPath === 'function')
|
|
192
|
+
return t.targetPath(dummy);
|
|
193
|
+
if (t.files) {
|
|
194
|
+
const f = t.files(dummy);
|
|
195
|
+
if (f.length === 1)
|
|
196
|
+
return f[0]?.targetPath;
|
|
197
|
+
}
|
|
198
|
+
if (t.changes) {
|
|
199
|
+
const c = t.changes(dummy);
|
|
200
|
+
if (c.length > 0 && c[0])
|
|
201
|
+
return c[0].targetPath;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Heuristic file matchers for IKnowledgeEntry-style rules.
|
|
3
|
+
*
|
|
4
|
+
* Rules in the knowledge model don't carry path patterns directly the
|
|
5
|
+
* way boundary rules do. The bridge needs to know "which files does
|
|
6
|
+
* this rule apply to?" — we derive that from two sources, in order:
|
|
7
|
+
*
|
|
8
|
+
* 1. **Explicit**: `rule.metadata.appliesTo` — an array of glob
|
|
9
|
+
* patterns. Authoritative when present.
|
|
10
|
+
* 2. **Tag-based heuristics**: well-known tags map to file
|
|
11
|
+
* patterns. For example a rule tagged `mcp` likely applies to
|
|
12
|
+
* files under `packages/mcp-server/**`. The map is intentionally
|
|
13
|
+
* conservative — when no tag matches, the rule is *not* bridged
|
|
14
|
+
* (better silent than noisy).
|
|
15
|
+
*
|
|
16
|
+
* Authors who want predictable bridging set `metadata.appliesTo`.
|
|
17
|
+
*/
|
|
18
|
+
export interface IRuleApplicability {
|
|
19
|
+
/** Glob patterns to match against file paths. */
|
|
20
|
+
patterns: readonly string[];
|
|
21
|
+
/**
|
|
22
|
+
* When non-empty, the rule applies to every file whose `tags` array
|
|
23
|
+
* intersects this set. Currently used by `testing`-tagged rules.
|
|
24
|
+
*/
|
|
25
|
+
fileTags: readonly string[];
|
|
26
|
+
/** How we derived the applicability — useful for diagnostics. */
|
|
27
|
+
source: 'metadata' | 'tags' | 'none';
|
|
28
|
+
}
|
|
29
|
+
export declare function deriveApplicability(rule: {
|
|
30
|
+
tags?: readonly string[];
|
|
31
|
+
metadata?: Readonly<Record<string, unknown>>;
|
|
32
|
+
}): IRuleApplicability;
|
|
33
|
+
//# sourceMappingURL=knowledge-rule-matching.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"knowledge-rule-matching.d.ts","sourceRoot":"","sources":["../../src/bridge/knowledge-rule-matching.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAyBH,MAAM,WAAW,kBAAkB;IACjC,iDAAiD;IACjD,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B;;;OAGG;IACH,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,iEAAiE;IACjE,MAAM,EAAE,UAAU,GAAG,MAAM,GAAG,MAAM,CAAC;CACtC;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE;IACxC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACzB,QAAQ,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CAC9C,GAAG,kBAAkB,CAwBrB"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Heuristic file matchers for IKnowledgeEntry-style rules.
|
|
3
|
+
*
|
|
4
|
+
* Rules in the knowledge model don't carry path patterns directly the
|
|
5
|
+
* way boundary rules do. The bridge needs to know "which files does
|
|
6
|
+
* this rule apply to?" — we derive that from two sources, in order:
|
|
7
|
+
*
|
|
8
|
+
* 1. **Explicit**: `rule.metadata.appliesTo` — an array of glob
|
|
9
|
+
* patterns. Authoritative when present.
|
|
10
|
+
* 2. **Tag-based heuristics**: well-known tags map to file
|
|
11
|
+
* patterns. For example a rule tagged `mcp` likely applies to
|
|
12
|
+
* files under `packages/mcp-server/**`. The map is intentionally
|
|
13
|
+
* conservative — when no tag matches, the rule is *not* bridged
|
|
14
|
+
* (better silent than noisy).
|
|
15
|
+
*
|
|
16
|
+
* Authors who want predictable bridging set `metadata.appliesTo`.
|
|
17
|
+
*/
|
|
18
|
+
const TAG_PATTERNS = new Map([
|
|
19
|
+
// Engine packages.
|
|
20
|
+
['mcp', ['packages/mcp-server/**']],
|
|
21
|
+
['cli', ['packages/cli/**']],
|
|
22
|
+
['dashboard', ['packages/dashboard/**', 'packages/dashboard-api/**']],
|
|
23
|
+
['generator', ['packages/generator/**']],
|
|
24
|
+
['importer', ['packages/importer/**']],
|
|
25
|
+
['inspector', ['packages/inspector/**']],
|
|
26
|
+
['packs', ['packages/packs/**']],
|
|
27
|
+
['core', ['packages/core/**']],
|
|
28
|
+
// Asset categories.
|
|
29
|
+
['boundaries', ['sharkcraft/boundaries.ts', 'packages/boundaries/**']],
|
|
30
|
+
['rules', ['sharkcraft/rules.ts', 'packages/rules/**']],
|
|
31
|
+
['paths', ['sharkcraft/paths.ts', 'packages/paths/**']],
|
|
32
|
+
['templates', ['sharkcraft/templates.ts', 'packages/templates/**']],
|
|
33
|
+
['pipelines', ['sharkcraft/pipelines.ts', 'packages/pipelines/**']],
|
|
34
|
+
['presets', ['sharkcraft/presets.ts', 'packages/presets/**']],
|
|
35
|
+
// Cross-cutting concerns.
|
|
36
|
+
['imports', ['packages/**/*.ts']],
|
|
37
|
+
['testing', []], // signal-only; the bridge attaches via the file's `tags: test` separately
|
|
38
|
+
['tests', []],
|
|
39
|
+
]);
|
|
40
|
+
export function deriveApplicability(rule) {
|
|
41
|
+
const explicit = rule.metadata?.['appliesTo'];
|
|
42
|
+
if (Array.isArray(explicit) && explicit.every((p) => typeof p === 'string')) {
|
|
43
|
+
return { patterns: explicit, fileTags: [], source: 'metadata' };
|
|
44
|
+
}
|
|
45
|
+
const tags = rule.tags ?? [];
|
|
46
|
+
const patterns = [];
|
|
47
|
+
const fileTags = [];
|
|
48
|
+
for (const tag of tags) {
|
|
49
|
+
if (tag === 'testing' || tag === 'tests') {
|
|
50
|
+
fileTags.push('test');
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const mapped = TAG_PATTERNS.get(tag);
|
|
54
|
+
if (mapped) {
|
|
55
|
+
for (const p of mapped) {
|
|
56
|
+
if (!patterns.includes(p))
|
|
57
|
+
patterns.push(p);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (patterns.length === 0 && fileTags.length === 0) {
|
|
62
|
+
return { patterns: [], fileTags: [], source: 'none' };
|
|
63
|
+
}
|
|
64
|
+
return { patterns, fileTags, source: 'tags' };
|
|
65
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export * from './schema/bridge-schema.js';
|
|
2
|
+
export * from './store/bridge-store.js';
|
|
3
|
+
export * from './bridge/bridge-builder.js';
|
|
4
|
+
export * from './bridge/knowledge-rule-matching.js';
|
|
5
|
+
export * from './query/rule-graph-query-api.js';
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,2BAA2B,CAAC;AAC1C,cAAc,yBAAyB,CAAC;AACxC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,qCAAqC,CAAC;AACpD,cAAc,iCAAiC,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Public surface of @shrkcrft/rule-graph.
|
|
2
|
+
export * from "./schema/bridge-schema.js";
|
|
3
|
+
export * from "./store/bridge-store.js";
|
|
4
|
+
export * from "./bridge/bridge-builder.js";
|
|
5
|
+
export * from "./bridge/knowledge-rule-matching.js";
|
|
6
|
+
export * from "./query/rule-graph-query-api.js";
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type IEdge, type IGraphSnapshot, type INode } from '@shrkcrft/graph';
|
|
2
|
+
import type { IBridgeSnapshot } from '../schema/bridge-schema.js';
|
|
3
|
+
export interface IBridgeEdgeHit {
|
|
4
|
+
edge: IEdge;
|
|
5
|
+
target: INode;
|
|
6
|
+
}
|
|
7
|
+
export interface IRuleGraphForFile {
|
|
8
|
+
fileNodeId: string;
|
|
9
|
+
path: string;
|
|
10
|
+
rules: readonly IBridgeEdgeHit[];
|
|
11
|
+
paths: readonly IBridgeEdgeHit[];
|
|
12
|
+
templates: readonly IBridgeEdgeHit[];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Query API over the merged code-graph + bridge-graph snapshots.
|
|
16
|
+
*
|
|
17
|
+
* Loaded once per request; reads are O(edges-touching-the-anchor) once
|
|
18
|
+
* the inbound + outbound indexes are built.
|
|
19
|
+
*/
|
|
20
|
+
export declare class RuleGraphQueryApi {
|
|
21
|
+
private readonly graphSnap;
|
|
22
|
+
private readonly bridgeSnap;
|
|
23
|
+
private readonly outByFrom;
|
|
24
|
+
private readonly inByTo;
|
|
25
|
+
private readonly mergedNodes;
|
|
26
|
+
private readonly fileByPath;
|
|
27
|
+
constructor(graphSnap: IGraphSnapshot, bridgeSnap: IBridgeSnapshot);
|
|
28
|
+
static fromStores(projectRoot: string): RuleGraphQueryApi;
|
|
29
|
+
static missingDescription(projectRoot: string): string | undefined;
|
|
30
|
+
/** Resolve a file path to its file node, or undefined. */
|
|
31
|
+
findFile(path: string): INode | undefined;
|
|
32
|
+
/** Everything that applies to a file: rules, paths, templates. */
|
|
33
|
+
forFile(path: string): IRuleGraphForFile | undefined;
|
|
34
|
+
/** Files that a given rule / path / template applies to. */
|
|
35
|
+
filesFor(bridgeNodeId: string): readonly INode[];
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=rule-graph-query-api.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rule-graph-query-api.d.ts","sourceRoot":"","sources":["../../src/query/rule-graph-query-api.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,KAAK,EACV,KAAK,cAAc,EACnB,KAAK,KAAK,EACX,MAAM,iBAAiB,CAAC;AAEzB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAElE,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,KAAK,CAAC;IACZ,MAAM,EAAE,KAAK,CAAC;CACf;AAED,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,SAAS,cAAc,EAAE,CAAC;IACjC,KAAK,EAAE,SAAS,cAAc,EAAE,CAAC;IACjC,SAAS,EAAE,SAAS,cAAc,EAAE,CAAC;CACtC;AAED;;;;;GAKG;AACH,qBAAa,iBAAiB;IAO1B,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,UAAU;IAP7B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAwC;IAClE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAwC;IAC/D,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA6B;IACzD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA6B;gBAGrC,SAAS,EAAE,cAAc,EACzB,UAAU,EAAE,eAAe;IAyB9C,MAAM,CAAC,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,iBAAiB;IAMzD,MAAM,CAAC,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAQlE,0DAA0D;IAC1D,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,KAAK,GAAG,SAAS;IAIzC,kEAAkE;IAClE,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,iBAAiB,GAAG,SAAS;IAiBpD,4DAA4D;IAC5D,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,KAAK,EAAE;CAiBjD"}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { EdgeKind, GraphStore, } from '@shrkcrft/graph';
|
|
2
|
+
import { BridgeStore } from "../store/bridge-store.js";
|
|
3
|
+
/**
|
|
4
|
+
* Query API over the merged code-graph + bridge-graph snapshots.
|
|
5
|
+
*
|
|
6
|
+
* Loaded once per request; reads are O(edges-touching-the-anchor) once
|
|
7
|
+
* the inbound + outbound indexes are built.
|
|
8
|
+
*/
|
|
9
|
+
export class RuleGraphQueryApi {
|
|
10
|
+
graphSnap;
|
|
11
|
+
bridgeSnap;
|
|
12
|
+
outByFrom;
|
|
13
|
+
inByTo;
|
|
14
|
+
mergedNodes;
|
|
15
|
+
fileByPath;
|
|
16
|
+
constructor(graphSnap, bridgeSnap) {
|
|
17
|
+
this.graphSnap = graphSnap;
|
|
18
|
+
this.bridgeSnap = bridgeSnap;
|
|
19
|
+
const out = new Map();
|
|
20
|
+
const inn = new Map();
|
|
21
|
+
for (const e of [...graphSnap.edges.values(), ...bridgeSnap.edges.values()]) {
|
|
22
|
+
const o = out.get(e.from);
|
|
23
|
+
if (o)
|
|
24
|
+
o.push(e);
|
|
25
|
+
else
|
|
26
|
+
out.set(e.from, [e]);
|
|
27
|
+
const i = inn.get(e.to);
|
|
28
|
+
if (i)
|
|
29
|
+
i.push(e);
|
|
30
|
+
else
|
|
31
|
+
inn.set(e.to, [e]);
|
|
32
|
+
}
|
|
33
|
+
const merged = new Map();
|
|
34
|
+
for (const n of graphSnap.nodes.values())
|
|
35
|
+
merged.set(n.id, n);
|
|
36
|
+
for (const n of bridgeSnap.nodes.values())
|
|
37
|
+
merged.set(n.id, n);
|
|
38
|
+
const fileByPath = new Map();
|
|
39
|
+
for (const n of graphSnap.nodes.values()) {
|
|
40
|
+
if (n.path && n.kind === 'file')
|
|
41
|
+
fileByPath.set(n.path, n);
|
|
42
|
+
}
|
|
43
|
+
this.outByFrom = out;
|
|
44
|
+
this.inByTo = inn;
|
|
45
|
+
this.mergedNodes = merged;
|
|
46
|
+
this.fileByPath = fileByPath;
|
|
47
|
+
}
|
|
48
|
+
static fromStores(projectRoot) {
|
|
49
|
+
const g = new GraphStore(projectRoot).loadSnapshot();
|
|
50
|
+
const b = new BridgeStore(projectRoot).loadSnapshot();
|
|
51
|
+
return new RuleGraphQueryApi(g, b);
|
|
52
|
+
}
|
|
53
|
+
static missingDescription(projectRoot) {
|
|
54
|
+
const g = new GraphStore(projectRoot).exists();
|
|
55
|
+
const b = new BridgeStore(projectRoot).exists();
|
|
56
|
+
if (!g)
|
|
57
|
+
return "Code-graph store missing. Run 'shrk graph index' then 'shrk rule-graph index'.";
|
|
58
|
+
if (!b)
|
|
59
|
+
return "Bridge store missing. Run 'shrk rule-graph index'.";
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
/** Resolve a file path to its file node, or undefined. */
|
|
63
|
+
findFile(path) {
|
|
64
|
+
return this.fileByPath.get(path);
|
|
65
|
+
}
|
|
66
|
+
/** Everything that applies to a file: rules, paths, templates. */
|
|
67
|
+
forFile(path) {
|
|
68
|
+
const file = this.findFile(path);
|
|
69
|
+
if (!file)
|
|
70
|
+
return undefined;
|
|
71
|
+
const out = this.outByFrom.get(file.id) ?? [];
|
|
72
|
+
const rules = [];
|
|
73
|
+
const paths = [];
|
|
74
|
+
const templates = [];
|
|
75
|
+
for (const e of out) {
|
|
76
|
+
const target = this.mergedNodes.get(e.to);
|
|
77
|
+
if (!target)
|
|
78
|
+
continue;
|
|
79
|
+
if (e.kind === EdgeKind.AppliesRule)
|
|
80
|
+
rules.push({ edge: e, target });
|
|
81
|
+
else if (e.kind === EdgeKind.MatchesPath)
|
|
82
|
+
paths.push({ edge: e, target });
|
|
83
|
+
else if (e.kind === EdgeKind.CoveredByTemplate)
|
|
84
|
+
templates.push({ edge: e, target });
|
|
85
|
+
}
|
|
86
|
+
return { fileNodeId: file.id, path, rules, paths, templates };
|
|
87
|
+
}
|
|
88
|
+
/** Files that a given rule / path / template applies to. */
|
|
89
|
+
filesFor(bridgeNodeId) {
|
|
90
|
+
const inn = this.inByTo.get(bridgeNodeId) ?? [];
|
|
91
|
+
const out = [];
|
|
92
|
+
const seen = new Set();
|
|
93
|
+
for (const e of inn) {
|
|
94
|
+
if (e.kind !== EdgeKind.AppliesRule &&
|
|
95
|
+
e.kind !== EdgeKind.MatchesPath &&
|
|
96
|
+
e.kind !== EdgeKind.CoveredByTemplate)
|
|
97
|
+
continue;
|
|
98
|
+
if (seen.has(e.from))
|
|
99
|
+
continue;
|
|
100
|
+
seen.add(e.from);
|
|
101
|
+
const n = this.mergedNodes.get(e.from);
|
|
102
|
+
if (n)
|
|
103
|
+
out.push(n);
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { IEdge, INode } from '@shrkcrft/graph';
|
|
2
|
+
export declare const RULE_GRAPH_SCHEMA: "sharkcraft.rule-graph/v1";
|
|
3
|
+
export type RuleGraphSchemaVersion = typeof RULE_GRAPH_SCHEMA;
|
|
4
|
+
export interface IBridgeManifest {
|
|
5
|
+
schema: RuleGraphSchemaVersion;
|
|
6
|
+
projectRoot: string;
|
|
7
|
+
lastBuiltAt: string;
|
|
8
|
+
lastBuildDurationMs: number;
|
|
9
|
+
/** SHA-256 of the bridge store's JSONL files. */
|
|
10
|
+
digest: string;
|
|
11
|
+
/** Per-kind counters at build time. */
|
|
12
|
+
nodesByKind: Readonly<Record<string, number>>;
|
|
13
|
+
edgesByKind: Readonly<Record<string, number>>;
|
|
14
|
+
/** Counters by bridge source (rule / path / template). */
|
|
15
|
+
sourceCounts: Readonly<Record<string, number>>;
|
|
16
|
+
/**
|
|
17
|
+
* Coverage of the file set by `applies-rule` edges (boundaries +
|
|
18
|
+
* knowledge rules). Templates and path conventions are NOT counted —
|
|
19
|
+
* the roadmap (§3.2) defines coverage gap specifically as "files with
|
|
20
|
+
* no applicable rule". Optional for forward-compat with manifests
|
|
21
|
+
* written before the field existed.
|
|
22
|
+
*/
|
|
23
|
+
filesTotal?: number;
|
|
24
|
+
filesCoveredByRules?: number;
|
|
25
|
+
filesUncoveredByRules?: number;
|
|
26
|
+
}
|
|
27
|
+
export interface IBridgeSnapshot {
|
|
28
|
+
manifest: IBridgeManifest;
|
|
29
|
+
nodes: ReadonlyMap<string, INode>;
|
|
30
|
+
edges: ReadonlyMap<string, IEdge>;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=bridge-schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bridge-schema.d.ts","sourceRoot":"","sources":["../../src/schema/bridge-schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAEpD,eAAO,MAAM,iBAAiB,EAAG,0BAAmC,CAAC;AAErE,MAAM,MAAM,sBAAsB,GAAG,OAAO,iBAAiB,CAAC;AAE9D,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,sBAAsB,CAAC;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,iDAAiD;IACjD,MAAM,EAAE,MAAM,CAAC;IACf,uCAAuC;IACvC,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC9C,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC9C,0DAA0D;IAC1D,YAAY,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC/C;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,eAAe,CAAC;IAC1B,KAAK,EAAE,WAAW,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IAClC,KAAK,EAAE,WAAW,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;CACnC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const RULE_GRAPH_SCHEMA = 'sharkcraft.rule-graph/v1';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { IEdge, INode } from '@shrkcrft/graph';
|
|
2
|
+
import { type IBridgeManifest, type IBridgeSnapshot } from '../schema/bridge-schema.js';
|
|
3
|
+
/**
|
|
4
|
+
* On-disk store for bridge nodes + edges produced by `@shrkcrft/rule-graph`.
|
|
5
|
+
*
|
|
6
|
+
* Lives at `<root>/.sharkcraft/bridge/` so a `shrk graph index` rebuild of
|
|
7
|
+
* the code graph does not stomp on bridge data. Both stores merge in
|
|
8
|
+
* memory at query time.
|
|
9
|
+
*/
|
|
10
|
+
export declare class BridgeStore {
|
|
11
|
+
private readonly projectRoot;
|
|
12
|
+
readonly storeDir: string;
|
|
13
|
+
constructor(projectRoot: string);
|
|
14
|
+
exists(): boolean;
|
|
15
|
+
clear(): void;
|
|
16
|
+
writeSnapshot(nodes: readonly INode[], edges: readonly IEdge[], partial: Omit<IBridgeManifest, 'schema' | 'digest'>): IBridgeManifest;
|
|
17
|
+
loadSnapshot(): IBridgeSnapshot;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=bridge-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bridge-store.d.ts","sourceRoot":"","sources":["../../src/store/bridge-store.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAEL,KAAK,eAAe,EACpB,KAAK,eAAe,EACrB,MAAM,4BAA4B,CAAC;AAMpC;;;;;;GAMG;AACH,qBAAa,WAAW;IAGV,OAAO,CAAC,QAAQ,CAAC,WAAW;IAFxC,SAAgB,QAAQ,EAAE,MAAM,CAAC;gBAEJ,WAAW,EAAE,MAAM;IAIhD,MAAM,IAAI,OAAO;IAIjB,KAAK,IAAI,IAAI;IAMb,aAAa,CACX,KAAK,EAAE,SAAS,KAAK,EAAE,EACvB,KAAK,EAAE,SAAS,KAAK,EAAE,EACvB,OAAO,EAAE,IAAI,CAAC,eAAe,EAAE,QAAQ,GAAG,QAAQ,CAAC,GAClD,eAAe;IAoClB,YAAY,IAAI,eAAe;CAoChC"}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync, } from 'node:fs';
|
|
3
|
+
import * as nodePath from 'node:path';
|
|
4
|
+
import { RULE_GRAPH_SCHEMA, } from "../schema/bridge-schema.js";
|
|
5
|
+
const NODES_DIR = 'nodes';
|
|
6
|
+
const EDGES_DIR = 'edges';
|
|
7
|
+
const META_FILE = 'meta.json';
|
|
8
|
+
/**
|
|
9
|
+
* On-disk store for bridge nodes + edges produced by `@shrkcrft/rule-graph`.
|
|
10
|
+
*
|
|
11
|
+
* Lives at `<root>/.sharkcraft/bridge/` so a `shrk graph index` rebuild of
|
|
12
|
+
* the code graph does not stomp on bridge data. Both stores merge in
|
|
13
|
+
* memory at query time.
|
|
14
|
+
*/
|
|
15
|
+
export class BridgeStore {
|
|
16
|
+
projectRoot;
|
|
17
|
+
storeDir;
|
|
18
|
+
constructor(projectRoot) {
|
|
19
|
+
this.projectRoot = projectRoot;
|
|
20
|
+
this.storeDir = nodePath.join(projectRoot, '.sharkcraft', 'bridge');
|
|
21
|
+
}
|
|
22
|
+
exists() {
|
|
23
|
+
return existsSync(nodePath.join(this.storeDir, META_FILE));
|
|
24
|
+
}
|
|
25
|
+
clear() {
|
|
26
|
+
if (existsSync(this.storeDir)) {
|
|
27
|
+
rmSync(this.storeDir, { recursive: true, force: true });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
writeSnapshot(nodes, edges, partial) {
|
|
31
|
+
const nodesDir = nodePath.join(this.storeDir, NODES_DIR);
|
|
32
|
+
const edgesDir = nodePath.join(this.storeDir, EDGES_DIR);
|
|
33
|
+
if (existsSync(nodesDir))
|
|
34
|
+
rmSync(nodesDir, { recursive: true, force: true });
|
|
35
|
+
if (existsSync(edgesDir))
|
|
36
|
+
rmSync(edgesDir, { recursive: true, force: true });
|
|
37
|
+
mkdirSync(nodesDir, { recursive: true });
|
|
38
|
+
mkdirSync(edgesDir, { recursive: true });
|
|
39
|
+
const nodesByKind = bucket(nodes, (n) => n.kind);
|
|
40
|
+
const edgesByKind = bucket(edges, (e) => e.kind);
|
|
41
|
+
const nodeCounts = {};
|
|
42
|
+
const edgeCounts = {};
|
|
43
|
+
for (const [kind, list] of Object.entries(nodesByKind)) {
|
|
44
|
+
list.sort((a, b) => a.id.localeCompare(b.id));
|
|
45
|
+
writeJsonl(nodePath.join(nodesDir, `${kind}.jsonl`), list);
|
|
46
|
+
nodeCounts[kind] = list.length;
|
|
47
|
+
}
|
|
48
|
+
for (const [kind, list] of Object.entries(edgesByKind)) {
|
|
49
|
+
list.sort((a, b) => a.id.localeCompare(b.id));
|
|
50
|
+
writeJsonl(nodePath.join(edgesDir, `${kind}.jsonl`), list);
|
|
51
|
+
edgeCounts[kind] = list.length;
|
|
52
|
+
}
|
|
53
|
+
const digest = computeDigest(this.storeDir);
|
|
54
|
+
const manifest = {
|
|
55
|
+
schema: RULE_GRAPH_SCHEMA,
|
|
56
|
+
digest,
|
|
57
|
+
...partial,
|
|
58
|
+
nodesByKind: nodeCounts,
|
|
59
|
+
edgesByKind: edgeCounts,
|
|
60
|
+
};
|
|
61
|
+
writeFileSync(nodePath.join(this.storeDir, META_FILE), JSON.stringify(manifest, null, 2));
|
|
62
|
+
return manifest;
|
|
63
|
+
}
|
|
64
|
+
loadSnapshot() {
|
|
65
|
+
if (!this.exists()) {
|
|
66
|
+
throw new Error(`bridge store not found under ${this.storeDir}. Run 'shrk rule-graph index'.`);
|
|
67
|
+
}
|
|
68
|
+
const manifest = JSON.parse(readFileSync(nodePath.join(this.storeDir, META_FILE), 'utf8'));
|
|
69
|
+
if (manifest.schema !== RULE_GRAPH_SCHEMA) {
|
|
70
|
+
throw new Error(`bridge schema mismatch: store=${manifest.schema}, expected=${RULE_GRAPH_SCHEMA}.`);
|
|
71
|
+
}
|
|
72
|
+
const nodes = new Map();
|
|
73
|
+
const nodesDir = nodePath.join(this.storeDir, NODES_DIR);
|
|
74
|
+
if (existsSync(nodesDir)) {
|
|
75
|
+
for (const fname of readdirSync(nodesDir)) {
|
|
76
|
+
if (!fname.endsWith('.jsonl'))
|
|
77
|
+
continue;
|
|
78
|
+
for (const row of readJsonl(nodePath.join(nodesDir, fname))) {
|
|
79
|
+
nodes.set(row.id, row);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const edges = new Map();
|
|
84
|
+
const edgesDir = nodePath.join(this.storeDir, EDGES_DIR);
|
|
85
|
+
if (existsSync(edgesDir)) {
|
|
86
|
+
for (const fname of readdirSync(edgesDir)) {
|
|
87
|
+
if (!fname.endsWith('.jsonl'))
|
|
88
|
+
continue;
|
|
89
|
+
for (const row of readJsonl(nodePath.join(edgesDir, fname))) {
|
|
90
|
+
edges.set(row.id, row);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return { manifest, nodes, edges };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function bucket(list, key) {
|
|
98
|
+
const out = {};
|
|
99
|
+
for (const item of list) {
|
|
100
|
+
const k = key(item);
|
|
101
|
+
if (!out[k])
|
|
102
|
+
out[k] = [];
|
|
103
|
+
out[k].push(item);
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
function writeJsonl(path, rows) {
|
|
108
|
+
const body = rows.map((r) => JSON.stringify(r)).join('\n');
|
|
109
|
+
writeFileSync(path, body.length > 0 ? body + '\n' : '');
|
|
110
|
+
}
|
|
111
|
+
function readJsonl(path) {
|
|
112
|
+
const raw = readFileSync(path, 'utf8');
|
|
113
|
+
if (!raw)
|
|
114
|
+
return [];
|
|
115
|
+
const out = [];
|
|
116
|
+
for (const line of raw.split('\n')) {
|
|
117
|
+
const t = line.trim();
|
|
118
|
+
if (!t)
|
|
119
|
+
continue;
|
|
120
|
+
out.push(JSON.parse(t));
|
|
121
|
+
}
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
function computeDigest(storeDir) {
|
|
125
|
+
const hash = createHash('sha256');
|
|
126
|
+
const targets = [];
|
|
127
|
+
for (const sub of [NODES_DIR, EDGES_DIR]) {
|
|
128
|
+
const dir = nodePath.join(storeDir, sub);
|
|
129
|
+
if (!existsSync(dir))
|
|
130
|
+
continue;
|
|
131
|
+
for (const fname of readdirSync(dir).sort()) {
|
|
132
|
+
if (!fname.endsWith('.jsonl'))
|
|
133
|
+
continue;
|
|
134
|
+
targets.push(nodePath.join(dir, fname));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
targets.sort();
|
|
138
|
+
for (const t of targets) {
|
|
139
|
+
hash.update(nodePath.relative(storeDir, t));
|
|
140
|
+
hash.update('\0');
|
|
141
|
+
hash.update(readFileSync(t));
|
|
142
|
+
hash.update('\0');
|
|
143
|
+
}
|
|
144
|
+
return hash.digest('hex');
|
|
145
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@shrkcrft/rule-graph",
|
|
3
|
+
"version": "0.1.0-alpha.10",
|
|
4
|
+
"description": "SharkCraft rule-graph: bridges code-graph file nodes to rules / paths / templates / boundaries so 'which assets apply to this file?' is a 1-hop query.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "SharkCraft contributors",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"README.md",
|
|
20
|
+
"LICENSE"
|
|
21
|
+
],
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/sharkcraft/sharkcraft.git",
|
|
25
|
+
"directory": "packages/rule-graph"
|
|
26
|
+
},
|
|
27
|
+
"homepage": "https://github.com/sharkcraft/sharkcraft",
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/sharkcraft/sharkcraft/issues"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"sharkcraft",
|
|
33
|
+
"rule-graph",
|
|
34
|
+
"bridge",
|
|
35
|
+
"code-intelligence"
|
|
36
|
+
],
|
|
37
|
+
"engines": {
|
|
38
|
+
"bun": ">=1.1.0",
|
|
39
|
+
"node": ">=18"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@shrkcrft/core": "^0.1.0-alpha.10",
|
|
46
|
+
"@shrkcrft/boundaries": "^0.1.0-alpha.10",
|
|
47
|
+
"@shrkcrft/graph": "^0.1.0-alpha.10",
|
|
48
|
+
"@shrkcrft/inspector": "^0.1.0-alpha.10"
|
|
49
|
+
},
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"access": "public"
|
|
52
|
+
}
|
|
53
|
+
}
|