@shrkcrft/cli 0.1.0-alpha.22 → 0.1.0-alpha.24
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-registry.d.ts +9 -0
- package/dist/command-registry.d.ts.map +1 -1
- package/dist/command-registry.js +15 -0
- package/dist/commands/ai-status.command.d.ts.map +1 -1
- package/dist/commands/ai-status.command.js +10 -0
- package/dist/commands/apply.command.d.ts.map +1 -1
- package/dist/commands/apply.command.js +9 -2
- package/dist/commands/ask.command.d.ts.map +1 -1
- package/dist/commands/ask.command.js +2 -1
- package/dist/commands/changes.command.d.ts.map +1 -1
- package/dist/commands/changes.command.js +14 -4
- package/dist/commands/check.command.d.ts.map +1 -1
- package/dist/commands/check.command.js +139 -7
- package/dist/commands/command-catalog.d.ts +11 -0
- package/dist/commands/command-catalog.d.ts.map +1 -1
- package/dist/commands/command-catalog.js +291 -0
- package/dist/commands/commands.command.d.ts +15 -0
- package/dist/commands/commands.command.d.ts.map +1 -1
- package/dist/commands/commands.command.js +162 -3
- package/dist/commands/compress.command.d.ts.map +1 -1
- package/dist/commands/compress.command.js +39 -4
- package/dist/commands/context.command.d.ts.map +1 -1
- package/dist/commands/context.command.js +3 -1
- package/dist/commands/daily.commands.d.ts.map +1 -1
- package/dist/commands/daily.commands.js +48 -5
- package/dist/commands/diff-check.command.d.ts.map +1 -1
- package/dist/commands/diff-check.command.js +6 -2
- package/dist/commands/feedback.command.d.ts.map +1 -1
- package/dist/commands/feedback.command.js +4 -0
- package/dist/commands/finish.command.d.ts +3 -0
- package/dist/commands/finish.command.d.ts.map +1 -0
- package/dist/commands/finish.command.js +70 -0
- package/dist/commands/framework.command.d.ts.map +1 -1
- package/dist/commands/framework.command.js +20 -3
- package/dist/commands/gate.command.d.ts.map +1 -1
- package/dist/commands/gate.command.js +98 -13
- package/dist/commands/graph-code-subverbs.d.ts.map +1 -1
- package/dist/commands/graph-code-subverbs.js +215 -81
- package/dist/commands/graph.command.d.ts.map +1 -1
- package/dist/commands/graph.command.js +6 -0
- package/dist/commands/help.command.d.ts.map +1 -1
- package/dist/commands/help.command.js +20 -2
- package/dist/commands/impact.command.d.ts.map +1 -1
- package/dist/commands/impact.command.js +86 -1
- package/dist/commands/knowledge.command.d.ts.map +1 -1
- package/dist/commands/knowledge.command.js +21 -1
- package/dist/commands/onboard.command.d.ts.map +1 -1
- package/dist/commands/onboard.command.js +11 -2
- package/dist/commands/orchestrate.command.d.ts.map +1 -1
- package/dist/commands/orchestrate.command.js +6 -1
- package/dist/commands/pack-author.command.d.ts.map +1 -1
- package/dist/commands/pack-author.command.js +13 -1
- package/dist/commands/packs.command.d.ts.map +1 -1
- package/dist/commands/packs.command.js +95 -17
- package/dist/commands/paths.command.d.ts.map +1 -1
- package/dist/commands/paths.command.js +8 -1
- package/dist/commands/pipelines.command.d.ts.map +1 -1
- package/dist/commands/pipelines.command.js +2 -1
- package/dist/commands/plan-context.command.d.ts.map +1 -1
- package/dist/commands/plan-context.command.js +6 -3
- package/dist/commands/plan-simulate.command.js +1 -1
- package/dist/commands/policy-lint.command.d.ts.map +1 -1
- package/dist/commands/policy-lint.command.js +20 -7
- package/dist/commands/recommend.command.d.ts.map +1 -1
- package/dist/commands/recommend.command.js +10 -2
- package/dist/commands/registry.command.d.ts.map +1 -1
- package/dist/commands/registry.command.js +117 -9
- package/dist/commands/report.command.js +1 -1
- package/dist/commands/reuse.command.d.ts.map +1 -1
- package/dist/commands/reuse.command.js +17 -5
- package/dist/commands/review.command.d.ts.map +1 -1
- package/dist/commands/review.command.js +20 -2
- package/dist/commands/rule-graph-subverbs.d.ts.map +1 -1
- package/dist/commands/rule-graph-subverbs.js +31 -4
- package/dist/commands/search-structural.command.d.ts.map +1 -1
- package/dist/commands/search-structural.command.js +2 -2
- package/dist/commands/search.command.d.ts.map +1 -1
- package/dist/commands/search.command.js +19 -1
- package/dist/commands/simulate.command.d.ts.map +1 -1
- package/dist/commands/simulate.command.js +6 -1
- package/dist/commands/smart-context.command.d.ts.map +1 -1
- package/dist/commands/smart-context.command.js +101 -41
- package/dist/commands/spec.command.d.ts.map +1 -1
- package/dist/commands/spec.command.js +73 -13
- package/dist/commands/task-context.command.d.ts.map +1 -1
- package/dist/commands/task-context.command.js +26 -2
- package/dist/commands/test.command.d.ts.map +1 -1
- package/dist/commands/test.command.js +17 -0
- package/dist/commands/trace.command.d.ts.map +1 -1
- package/dist/commands/trace.command.js +87 -4
- package/dist/commands/wiring.command.d.ts +12 -0
- package/dist/commands/wiring.command.d.ts.map +1 -0
- package/dist/commands/wiring.command.js +384 -0
- package/dist/diff/collect-changed-paths.d.ts +5 -3
- package/dist/diff/collect-changed-paths.d.ts.map +1 -1
- package/dist/diff/collect-changed-paths.js +73 -36
- package/dist/diff/deleted-orphans.d.ts +42 -0
- package/dist/diff/deleted-orphans.d.ts.map +1 -0
- package/dist/diff/deleted-orphans.js +46 -0
- package/dist/finish/run-finish.d.ts +62 -0
- package/dist/finish/run-finish.d.ts.map +1 -0
- package/dist/finish/run-finish.js +239 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +19 -1
- package/dist/status/freshness.d.ts +30 -0
- package/dist/status/freshness.d.ts.map +1 -0
- package/dist/status/freshness.js +81 -0
- package/package.json +33 -33
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import { buildRegistrationGraph, explainWiring, registrationChain, registrationGraphSignature, registrationOrphans, registrationUnprovided, } from '@shrkcrft/boundaries';
|
|
2
|
+
import { resolveProjectConfig } from '@shrkcrft/inspector';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import * as nodePath from 'node:path';
|
|
6
|
+
import { flagBool, resolveCwd } from "../command-registry.js";
|
|
7
|
+
import { asJson, header, kv } from "../output/format-output.js";
|
|
8
|
+
const SITE_DISPLAY_CAP = 50;
|
|
9
|
+
/**
|
|
10
|
+
* Render an {@link IWiringExplain} to stdout. Shared by `wiring explain`,
|
|
11
|
+
* `wiring test`, and `check wiring --explain` so all three speak the same
|
|
12
|
+
* dialect. JSON emits the full payload; text mirrors `search tuning explain`
|
|
13
|
+
* (header → loaded sets → per-site detail → the set-difference → verdict).
|
|
14
|
+
* Always returns 0 — explain is informational, the verdict is in the output.
|
|
15
|
+
*/
|
|
16
|
+
export function renderWiringExplain(report, wantJson) {
|
|
17
|
+
if (wantJson) {
|
|
18
|
+
process.stdout.write(asJson(report) + '\n');
|
|
19
|
+
return 0;
|
|
20
|
+
}
|
|
21
|
+
process.stdout.write(header(`Wiring explain: ${report.ruleId} (${report.mode})`));
|
|
22
|
+
if (report.description)
|
|
23
|
+
process.stdout.write(` ${report.description}\n`);
|
|
24
|
+
if (report.groupBy)
|
|
25
|
+
process.stdout.write(kv('groupBy', report.groupBy) + '\n');
|
|
26
|
+
process.stdout.write(kv('declared', `${report.declared.distinctCount} distinct across ${report.declared.filesScanned} file(s)`) +
|
|
27
|
+
'\n');
|
|
28
|
+
process.stdout.write(kv('registered', `${report.registered.distinctCount} distinct across ${report.registered.filesScanned} file(s)`) + '\n');
|
|
29
|
+
if (report.declared.error)
|
|
30
|
+
process.stdout.write(` ! declared side: ${report.declared.error}\n`);
|
|
31
|
+
if (report.registered.error) {
|
|
32
|
+
process.stdout.write(` ! registered side: ${report.registered.error}\n`);
|
|
33
|
+
}
|
|
34
|
+
writeSites('Declared sites', report.declared.sites);
|
|
35
|
+
writeSites('Registered sites', report.registered.sites);
|
|
36
|
+
if (report.declaredNotRegistered.length > 0) {
|
|
37
|
+
process.stdout.write(`\nDeclared but NOT registered (${report.declaredNotRegistered.length}):\n`);
|
|
38
|
+
for (const s of report.declaredNotRegistered.slice(0, SITE_DISPLAY_CAP)) {
|
|
39
|
+
process.stdout.write(` ✗ ${s.token} (${s.file}:${s.line})\n`);
|
|
40
|
+
}
|
|
41
|
+
if (report.declaredNotRegistered.length > SITE_DISPLAY_CAP) {
|
|
42
|
+
process.stdout.write(` … (${report.declaredNotRegistered.length - SITE_DISPLAY_CAP} more)\n`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (report.registeredNotDeclared.length > 0) {
|
|
46
|
+
process.stdout.write(`\nRegistered but NOT declared (parity, ${report.registeredNotDeclared.length}):\n`);
|
|
47
|
+
for (const s of report.registeredNotDeclared.slice(0, SITE_DISPLAY_CAP)) {
|
|
48
|
+
process.stdout.write(` ✗ ${s.token} (${s.file}:${s.line})\n`);
|
|
49
|
+
}
|
|
50
|
+
if (report.registeredNotDeclared.length > SITE_DISPLAY_CAP) {
|
|
51
|
+
process.stdout.write(` … (${report.registeredNotDeclared.length - SITE_DISPLAY_CAP} more)\n`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
for (const d of report.diagnostics)
|
|
55
|
+
process.stdout.write(` ! ${d}\n`);
|
|
56
|
+
process.stdout.write(`\nVerdict: ${report.verdict}\n`);
|
|
57
|
+
return 0;
|
|
58
|
+
}
|
|
59
|
+
function writeSites(label, sites) {
|
|
60
|
+
process.stdout.write(`\n${label} (${sites.length}):\n`);
|
|
61
|
+
if (sites.length === 0) {
|
|
62
|
+
process.stdout.write(' (none extracted)\n');
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
for (const s of sites.slice(0, SITE_DISPLAY_CAP)) {
|
|
66
|
+
process.stdout.write(` • ${s.token} (${s.file}:${s.line})\n`);
|
|
67
|
+
}
|
|
68
|
+
if (sites.length > SITE_DISPLAY_CAP) {
|
|
69
|
+
process.stdout.write(` … (${sites.length - SITE_DISPLAY_CAP} more)\n`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/** Light structural check: a candidate must at least name an id + both sides. */
|
|
73
|
+
function validateCandidate(raw) {
|
|
74
|
+
if (raw === null || typeof raw !== 'object') {
|
|
75
|
+
return { error: 'candidate must be a JSON object describing a wiring rule' };
|
|
76
|
+
}
|
|
77
|
+
const r = raw;
|
|
78
|
+
if (typeof r['id'] !== 'string' || r['id'].length === 0) {
|
|
79
|
+
return { error: 'candidate is missing a non-empty string "id"' };
|
|
80
|
+
}
|
|
81
|
+
const declared = r['declared'];
|
|
82
|
+
if (declared === null || typeof declared !== 'object' || !Array.isArray(declared.files)) {
|
|
83
|
+
return { error: 'candidate "declared" must be a source object with a files[] glob list' };
|
|
84
|
+
}
|
|
85
|
+
if (r['registered'] === undefined) {
|
|
86
|
+
return { error: 'candidate is missing "registered" (a source object or an array of them)' };
|
|
87
|
+
}
|
|
88
|
+
// Deeper misconfiguration (bad regex / no capture group) is surfaced as a
|
|
89
|
+
// diagnostic by the engine, not rejected here — that is the point of a dry run.
|
|
90
|
+
return { rule: raw };
|
|
91
|
+
}
|
|
92
|
+
async function wiringExplain(args) {
|
|
93
|
+
const cwd = resolveCwd(args);
|
|
94
|
+
const wantJson = flagBool(args, 'json');
|
|
95
|
+
const ruleId = args.positional[1];
|
|
96
|
+
if (!ruleId) {
|
|
97
|
+
process.stderr.write('Usage: shrk wiring explain <ruleId> [--json]\n');
|
|
98
|
+
return 2;
|
|
99
|
+
}
|
|
100
|
+
const loaded = await resolveProjectConfig(cwd);
|
|
101
|
+
if (!loaded.ok) {
|
|
102
|
+
const msg = loaded.error.message;
|
|
103
|
+
if (wantJson)
|
|
104
|
+
process.stdout.write(asJson({ ok: false, error: msg }) + '\n');
|
|
105
|
+
else
|
|
106
|
+
process.stderr.write(`Could not load config: ${msg}\n`);
|
|
107
|
+
return 1;
|
|
108
|
+
}
|
|
109
|
+
const rules = loaded.value.config.wiringRules ?? [];
|
|
110
|
+
const rule = rules.find((r) => r.id === ruleId);
|
|
111
|
+
if (!rule) {
|
|
112
|
+
const ids = rules.map((r) => r.id);
|
|
113
|
+
if (wantJson) {
|
|
114
|
+
process.stdout.write(asJson({ ok: false, error: 'not-found', ruleId, available: ids }) + '\n');
|
|
115
|
+
return 2;
|
|
116
|
+
}
|
|
117
|
+
process.stderr.write(`No wiring rule "${ruleId}". Configured rules: ${ids.length > 0 ? ids.join(', ') : '(none)'}\n`);
|
|
118
|
+
return 2;
|
|
119
|
+
}
|
|
120
|
+
return renderWiringExplain(explainWiring(cwd, rule), wantJson);
|
|
121
|
+
}
|
|
122
|
+
async function wiringTest(args) {
|
|
123
|
+
const cwd = resolveCwd(args);
|
|
124
|
+
const wantJson = flagBool(args, 'json');
|
|
125
|
+
const candidateArg = args.positional[1];
|
|
126
|
+
if (!candidateArg) {
|
|
127
|
+
process.stderr.write('Usage: shrk wiring test <candidate.json | inline-json> [--json]\n' +
|
|
128
|
+
' Dry-runs an ephemeral wiring rule against the live tree without writing config.\n');
|
|
129
|
+
return 2;
|
|
130
|
+
}
|
|
131
|
+
// A leading `{` is treated as inline JSON; otherwise the arg is a file path.
|
|
132
|
+
let source;
|
|
133
|
+
if (candidateArg.trimStart().startsWith('{')) {
|
|
134
|
+
source = candidateArg;
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
if (!existsSync(candidateArg)) {
|
|
138
|
+
const msg = `candidate file not found: ${candidateArg}`;
|
|
139
|
+
if (wantJson)
|
|
140
|
+
process.stdout.write(asJson({ ok: false, error: msg }) + '\n');
|
|
141
|
+
else
|
|
142
|
+
process.stderr.write(msg + '\n');
|
|
143
|
+
return 2;
|
|
144
|
+
}
|
|
145
|
+
source = readFileSync(candidateArg, 'utf8');
|
|
146
|
+
}
|
|
147
|
+
let parsed;
|
|
148
|
+
try {
|
|
149
|
+
parsed = JSON.parse(source);
|
|
150
|
+
}
|
|
151
|
+
catch (e) {
|
|
152
|
+
const msg = `candidate is not valid JSON: ${e instanceof Error ? e.message : String(e)}`;
|
|
153
|
+
if (wantJson)
|
|
154
|
+
process.stdout.write(asJson({ ok: false, error: msg }) + '\n');
|
|
155
|
+
else
|
|
156
|
+
process.stderr.write(msg + '\n');
|
|
157
|
+
return 2;
|
|
158
|
+
}
|
|
159
|
+
const { rule, error } = validateCandidate(parsed);
|
|
160
|
+
if (!rule) {
|
|
161
|
+
if (wantJson)
|
|
162
|
+
process.stdout.write(asJson({ ok: false, error }) + '\n');
|
|
163
|
+
else
|
|
164
|
+
process.stderr.write(`Invalid candidate: ${error}\n`);
|
|
165
|
+
return 2;
|
|
166
|
+
}
|
|
167
|
+
return renderWiringExplain(explainWiring(cwd, rule), wantJson);
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Load the configured DI/registration idioms and build the graph, cached by a
|
|
171
|
+
* signature of the exact source files it reads + an idiom hash. Repeated session
|
|
172
|
+
* queries (chain + unprovided + orphans) reuse ONE scan; any edit to a matched
|
|
173
|
+
* file shifts the signature and rebuilds, so the cache can never return a stale
|
|
174
|
+
* verdict (no `shrk graph index` required — the cache tracks its real data
|
|
175
|
+
* source, not the unrelated code-graph digest). Best-effort — any read/write
|
|
176
|
+
* error falls back to a fresh build.
|
|
177
|
+
*/
|
|
178
|
+
async function loadRegistrationGraph(cwd) {
|
|
179
|
+
const loaded = await resolveProjectConfig(cwd);
|
|
180
|
+
if (!loaded.ok)
|
|
181
|
+
return { ok: false, idioms: [], error: loaded.error.message };
|
|
182
|
+
const idioms = loaded.value.config.registrationGraph ?? [];
|
|
183
|
+
if (idioms.length === 0)
|
|
184
|
+
return { ok: true, idioms: [] };
|
|
185
|
+
const cacheKey = registrationCacheKey(cwd, idioms);
|
|
186
|
+
const cachePath = nodePath.join(cwd, '.sharkcraft', 'cache', 'registration-graph.json');
|
|
187
|
+
if (cacheKey) {
|
|
188
|
+
const cached = readRegistrationCache(cachePath, cacheKey);
|
|
189
|
+
if (cached)
|
|
190
|
+
return { ok: true, idioms, graph: cached };
|
|
191
|
+
}
|
|
192
|
+
const graph = buildRegistrationGraph(cwd, idioms);
|
|
193
|
+
if (cacheKey)
|
|
194
|
+
writeRegistrationCache(cachePath, cacheKey, graph);
|
|
195
|
+
return { ok: true, idioms, graph };
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* `<file-signature>:<idiom-hash>` — keyed on the mtime/size signature of the
|
|
199
|
+
* exact files the graph is built from (its real data source), NOT the code-graph
|
|
200
|
+
* index digest. Any source edit shifts the signature even when no reindex has
|
|
201
|
+
* run, so the persisted cache can never return a stale wiring verdict. Undefined
|
|
202
|
+
* only if signing itself throws (then the query rebuilds every time).
|
|
203
|
+
*/
|
|
204
|
+
function registrationCacheKey(cwd, idioms) {
|
|
205
|
+
try {
|
|
206
|
+
const signature = registrationGraphSignature(cwd, idioms);
|
|
207
|
+
const idiomHash = createHash('sha1').update(JSON.stringify(idioms)).digest('hex').slice(0, 16);
|
|
208
|
+
return `${signature}:${idiomHash}`;
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function readRegistrationCache(cachePath, key) {
|
|
215
|
+
try {
|
|
216
|
+
if (!existsSync(cachePath))
|
|
217
|
+
return undefined;
|
|
218
|
+
const cached = JSON.parse(readFileSync(cachePath, 'utf8'));
|
|
219
|
+
return cached.key === key && cached.graph ? cached.graph : undefined;
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function writeRegistrationCache(cachePath, key, graph) {
|
|
226
|
+
try {
|
|
227
|
+
mkdirSync(nodePath.dirname(cachePath), { recursive: true });
|
|
228
|
+
writeFileSync(cachePath, JSON.stringify({ key, graph }));
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
// best-effort cache; a write failure never breaks the query.
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function noIdiomsHint(wantJson) {
|
|
235
|
+
if (wantJson) {
|
|
236
|
+
process.stdout.write(asJson({ schema: 'sharkcraft.registration-graph/v1', idioms: [], tokens: [] }) + '\n');
|
|
237
|
+
return 0;
|
|
238
|
+
}
|
|
239
|
+
process.stdout.write(header('Registration graph'));
|
|
240
|
+
process.stdout.write(' No registration idioms configured. Declare `registrationGraph[]` in\n' +
|
|
241
|
+
' sharkcraft.config.ts (declared/provided/consumed shapes) to model your DI\n' +
|
|
242
|
+
' wiring as a queryable graph — see docs/wiring.md.\n');
|
|
243
|
+
return 0;
|
|
244
|
+
}
|
|
245
|
+
function siteLine(s) {
|
|
246
|
+
return `${s.file}:${s.line} [${s.idiom}]`;
|
|
247
|
+
}
|
|
248
|
+
async function wiringChain(args) {
|
|
249
|
+
const cwd = resolveCwd(args);
|
|
250
|
+
const wantJson = flagBool(args, 'json');
|
|
251
|
+
const token = args.positional[1];
|
|
252
|
+
if (!token) {
|
|
253
|
+
process.stderr.write('Usage: shrk wiring chain <token> [--json]\n');
|
|
254
|
+
return 2;
|
|
255
|
+
}
|
|
256
|
+
const loaded = await loadRegistrationGraph(cwd);
|
|
257
|
+
if (!loaded.ok) {
|
|
258
|
+
if (wantJson)
|
|
259
|
+
process.stdout.write(asJson({ ok: false, error: loaded.error }) + '\n');
|
|
260
|
+
else
|
|
261
|
+
process.stderr.write(`Could not load config: ${loaded.error}\n`);
|
|
262
|
+
return 1;
|
|
263
|
+
}
|
|
264
|
+
if (!loaded.graph)
|
|
265
|
+
return noIdiomsHint(wantJson);
|
|
266
|
+
const chain = registrationChain(loaded.graph, token);
|
|
267
|
+
if (!chain) {
|
|
268
|
+
if (wantJson) {
|
|
269
|
+
process.stdout.write(asJson({ ok: false, error: 'not-found', token }) + '\n');
|
|
270
|
+
return 1;
|
|
271
|
+
}
|
|
272
|
+
process.stdout.write(header(`Wiring chain: ${token}`));
|
|
273
|
+
process.stdout.write(' Token not found in the registration graph.\n');
|
|
274
|
+
return 1;
|
|
275
|
+
}
|
|
276
|
+
if (wantJson) {
|
|
277
|
+
process.stdout.write(asJson(chain) + '\n');
|
|
278
|
+
return 0;
|
|
279
|
+
}
|
|
280
|
+
process.stdout.write(header(`Wiring chain: ${token}`));
|
|
281
|
+
const section = (label, sites) => {
|
|
282
|
+
process.stdout.write(`\n${label} (${sites.length}):\n`);
|
|
283
|
+
if (sites.length === 0)
|
|
284
|
+
process.stdout.write(' (none)\n');
|
|
285
|
+
for (const s of sites)
|
|
286
|
+
process.stdout.write(` • ${siteLine(s)}\n`);
|
|
287
|
+
};
|
|
288
|
+
section('declared', chain.declared);
|
|
289
|
+
section('provided', chain.provided);
|
|
290
|
+
section('consumed', chain.consumed);
|
|
291
|
+
if (!chain.isProvided && (chain.isDeclared || chain.isConsumed)) {
|
|
292
|
+
process.stdout.write('\n ⚠ UNPROVIDED — declared/injected but never provided (silent at runtime).\n');
|
|
293
|
+
}
|
|
294
|
+
else if (chain.isProvided && !chain.isConsumed) {
|
|
295
|
+
process.stdout.write('\n ⚠ ORPHAN — provided but nothing consumes it.\n');
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
process.stdout.write('\n ✓ declared → provided → consumed.\n');
|
|
299
|
+
}
|
|
300
|
+
return 0;
|
|
301
|
+
}
|
|
302
|
+
async function wiringUnprovided(args) {
|
|
303
|
+
const cwd = resolveCwd(args);
|
|
304
|
+
const wantJson = flagBool(args, 'json');
|
|
305
|
+
const loaded = await loadRegistrationGraph(cwd);
|
|
306
|
+
if (!loaded.ok) {
|
|
307
|
+
if (wantJson)
|
|
308
|
+
process.stdout.write(asJson({ ok: false, error: loaded.error }) + '\n');
|
|
309
|
+
else
|
|
310
|
+
process.stderr.write(`Could not load config: ${loaded.error}\n`);
|
|
311
|
+
return 1;
|
|
312
|
+
}
|
|
313
|
+
if (!loaded.graph)
|
|
314
|
+
return noIdiomsHint(wantJson);
|
|
315
|
+
const unprovided = registrationUnprovided(loaded.graph);
|
|
316
|
+
if (wantJson) {
|
|
317
|
+
process.stdout.write(asJson({ schema: loaded.graph.schema, total: unprovided.length, unprovided }) + '\n');
|
|
318
|
+
return unprovided.length > 0 ? 1 : 0;
|
|
319
|
+
}
|
|
320
|
+
process.stdout.write(header('Unprovided tokens (declared/injected but never provided)'));
|
|
321
|
+
if (unprovided.length === 0) {
|
|
322
|
+
process.stdout.write(' ✓ Every declared/injected token has a provider. ✓\n');
|
|
323
|
+
return 0;
|
|
324
|
+
}
|
|
325
|
+
process.stdout.write(` ${unprovided.length} token(s) resolve to nothing at runtime:\n`);
|
|
326
|
+
for (const u of unprovided) {
|
|
327
|
+
const site = u.declared[0] ?? u.consumed[0];
|
|
328
|
+
const where = site ? ` (${siteLine(site)})` : '';
|
|
329
|
+
process.stdout.write(` ✗ ${u.token}${where}\n`);
|
|
330
|
+
}
|
|
331
|
+
return 1;
|
|
332
|
+
}
|
|
333
|
+
async function wiringOrphans(args) {
|
|
334
|
+
const cwd = resolveCwd(args);
|
|
335
|
+
const wantJson = flagBool(args, 'json');
|
|
336
|
+
const loaded = await loadRegistrationGraph(cwd);
|
|
337
|
+
if (!loaded.ok) {
|
|
338
|
+
if (wantJson)
|
|
339
|
+
process.stdout.write(asJson({ ok: false, error: loaded.error }) + '\n');
|
|
340
|
+
else
|
|
341
|
+
process.stderr.write(`Could not load config: ${loaded.error}\n`);
|
|
342
|
+
return 1;
|
|
343
|
+
}
|
|
344
|
+
if (!loaded.graph)
|
|
345
|
+
return noIdiomsHint(wantJson);
|
|
346
|
+
const orphans = registrationOrphans(loaded.graph);
|
|
347
|
+
if (wantJson) {
|
|
348
|
+
process.stdout.write(asJson({ schema: loaded.graph.schema, total: orphans.length, orphans }) + '\n');
|
|
349
|
+
return 0;
|
|
350
|
+
}
|
|
351
|
+
process.stdout.write(header('Orphan registrations (provided but nothing consumes)'));
|
|
352
|
+
if (orphans.length === 0) {
|
|
353
|
+
process.stdout.write(' ✓ Every provided token is consumed somewhere. ✓\n');
|
|
354
|
+
return 0;
|
|
355
|
+
}
|
|
356
|
+
process.stdout.write(` ${orphans.length} provided token(s) nothing injects:\n`);
|
|
357
|
+
for (const o of orphans) {
|
|
358
|
+
const site = o.provided[0];
|
|
359
|
+
process.stdout.write(` • ${o.token}${site ? ` (${siteLine(site)})` : ''}\n`);
|
|
360
|
+
}
|
|
361
|
+
return 0;
|
|
362
|
+
}
|
|
363
|
+
const WIRING_USAGE = 'shrk wiring explain <ruleId> | test <candidate.json|inline> | chain <token> | unprovided | orphans [--json]';
|
|
364
|
+
export const wiringCommand = {
|
|
365
|
+
name: 'wiring',
|
|
366
|
+
description: 'Author-loop + runtime-wiring queries (no config write): `explain <ruleId>` / `test <candidate>` show what a wiring rule extracts; `chain <token>` / `unprovided` / `orphans` query the DI/registration graph (declared→provided→consumed) for the silent-at-runtime bugs imports can\'t see.',
|
|
367
|
+
usage: WIRING_USAGE,
|
|
368
|
+
booleanFlags: new Set(['json']),
|
|
369
|
+
async run(args) {
|
|
370
|
+
const sub = args.positional[0];
|
|
371
|
+
if (sub === 'explain')
|
|
372
|
+
return wiringExplain(args);
|
|
373
|
+
if (sub === 'test')
|
|
374
|
+
return wiringTest(args);
|
|
375
|
+
if (sub === 'chain')
|
|
376
|
+
return wiringChain(args);
|
|
377
|
+
if (sub === 'unprovided')
|
|
378
|
+
return wiringUnprovided(args);
|
|
379
|
+
if (sub === 'orphans')
|
|
380
|
+
return wiringOrphans(args);
|
|
381
|
+
process.stderr.write(`Usage: ${WIRING_USAGE}\n`);
|
|
382
|
+
return 2;
|
|
383
|
+
},
|
|
384
|
+
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CLI-side `--since <ref>` helper. Thin wrapper over `git diff
|
|
3
|
-
* --name-status <ref
|
|
4
|
-
* deleted file set + an `isAvailable` flag so callers can fall back
|
|
2
|
+
* CLI-side `--since <ref>` helper. Thin wrapper over a two-dot `git diff
|
|
3
|
+
* --name-status <ref>` (working tree vs `<ref>`) that returns the changed file
|
|
4
|
+
* set + a deleted file set + an `isAvailable` flag so callers can fall back
|
|
5
5
|
* to whole-tree mode cleanly.
|
|
6
6
|
*
|
|
7
7
|
* Default ref resolution when `--since` is passed without a value:
|
|
@@ -23,5 +23,7 @@ export declare function resolveDefaultSinceRef(cwd: string): string | undefined;
|
|
|
23
23
|
export declare function collectChangedPaths(opts: {
|
|
24
24
|
cwd: string;
|
|
25
25
|
ref?: string;
|
|
26
|
+
/** Read the staged (index) diff instead of diffing against a ref. */
|
|
27
|
+
staged?: boolean;
|
|
26
28
|
}): IChangedPaths;
|
|
27
29
|
//# sourceMappingURL=collect-changed-paths.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"collect-changed-paths.d.ts","sourceRoot":"","sources":["../../src/diff/collect-changed-paths.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,WAAW,EAAE,OAAO,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAMD,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,
|
|
1
|
+
{"version":3,"file":"collect-changed-paths.d.ts","sourceRoot":"","sources":["../../src/diff/collect-changed-paths.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,WAAW,EAAE,OAAO,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAMD,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAOtE;AAoCD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE;IACxC,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,qEAAqE;IACrE,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,GAAG,aAAa,CAwDhB"}
|
|
@@ -1,22 +1,72 @@
|
|
|
1
|
-
import { execSync } from 'node:child_process';
|
|
2
1
|
import * as nodePath from 'node:path';
|
|
2
|
+
import { runGitLines } from '@shrkcrft/shared';
|
|
3
3
|
const DEFAULT_REF_CANDIDATES = ['origin/main', 'main', 'origin/master', 'master'];
|
|
4
4
|
const PER_PROCESS_CACHE = new Map();
|
|
5
5
|
export function resolveDefaultSinceRef(cwd) {
|
|
6
6
|
for (const candidate of DEFAULT_REF_CANDIDATES) {
|
|
7
|
-
|
|
8
|
-
execSync(`git -C "${cwd}" rev-parse --verify --quiet "${candidate}"`, {
|
|
9
|
-
stdio: 'pipe',
|
|
10
|
-
});
|
|
7
|
+
if (runGitLines(cwd, ['rev-parse', '--verify', '--quiet', candidate]).ok) {
|
|
11
8
|
return candidate;
|
|
12
9
|
}
|
|
13
|
-
catch {
|
|
14
|
-
// try next
|
|
15
|
-
}
|
|
16
10
|
}
|
|
17
11
|
return undefined;
|
|
18
12
|
}
|
|
13
|
+
function toPosix(path) {
|
|
14
|
+
return nodePath.normalize(path).split(nodePath.sep).join('/');
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Split `git diff --name-status` lines into changed vs deleted POSIX paths.
|
|
18
|
+
* Rename/copy detection is ON by default for `git diff`, so a rename emits a
|
|
19
|
+
* TAB-separated `R<score>\t<old>\t<new>` (copy: `C<score>\t<old>\t<new>`) — the
|
|
20
|
+
* status field is `R100`, not a bare letter. The old path of a RENAME is gone
|
|
21
|
+
* (so the orphan check must treat it as deleted); a COPY keeps its source. Every
|
|
22
|
+
* other status (A/M/T/…) carries a single path. Parsing the status by its first
|
|
23
|
+
* letter + splitting on TAB is what makes a `git mv` that orphans an importer
|
|
24
|
+
* visible to `check orphans` / `finish` instead of silently dropped.
|
|
25
|
+
*/
|
|
26
|
+
function parseNameStatus(lines) {
|
|
27
|
+
const changed = [];
|
|
28
|
+
const deleted = [];
|
|
29
|
+
for (const line of lines) {
|
|
30
|
+
const parts = line.split('\t');
|
|
31
|
+
if (parts.length < 2)
|
|
32
|
+
continue;
|
|
33
|
+
const code = parts[0][0];
|
|
34
|
+
if (code === 'D') {
|
|
35
|
+
deleted.push(toPosix(parts[1]));
|
|
36
|
+
}
|
|
37
|
+
else if ((code === 'R' || code === 'C') && parts.length >= 3) {
|
|
38
|
+
// R<score>\t<old>\t<new> (rename) / C<score>\t<old>\t<new> (copy).
|
|
39
|
+
if (code === 'R')
|
|
40
|
+
deleted.push(toPosix(parts[1])); // old path renamed away
|
|
41
|
+
changed.push(toPosix(parts[2])); // new path is added
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
changed.push(toPosix(parts[1]));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return { changed, deleted };
|
|
48
|
+
}
|
|
19
49
|
export function collectChangedPaths(opts) {
|
|
50
|
+
if (opts.staged) {
|
|
51
|
+
const cacheKey = `${opts.cwd}::staged`;
|
|
52
|
+
const cached = PER_PROCESS_CACHE.get(cacheKey);
|
|
53
|
+
if (cached)
|
|
54
|
+
return cached;
|
|
55
|
+
const out = runGitLines(opts.cwd, ['diff', '--name-status', '--cached']);
|
|
56
|
+
if (!out.ok) {
|
|
57
|
+
return {
|
|
58
|
+
ref: 'STAGED',
|
|
59
|
+
changed: [],
|
|
60
|
+
deleted: [],
|
|
61
|
+
isAvailable: false,
|
|
62
|
+
...(out.error ? { error: out.error } : {}),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const { changed, deleted } = parseNameStatus(out.lines);
|
|
66
|
+
const result = { ref: 'STAGED', changed, deleted, isAvailable: true };
|
|
67
|
+
PER_PROCESS_CACHE.set(cacheKey, result);
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
20
70
|
const ref = opts.ref ?? resolveDefaultSinceRef(opts.cwd);
|
|
21
71
|
if (!ref) {
|
|
22
72
|
return {
|
|
@@ -27,42 +77,29 @@ export function collectChangedPaths(opts) {
|
|
|
27
77
|
error: 'no resolvable default branch (tried origin/main, main, origin/master, master)',
|
|
28
78
|
};
|
|
29
79
|
}
|
|
30
|
-
|
|
80
|
+
// Namespace the ref cache key so a branch/tag literally named `staged` can't
|
|
81
|
+
// collide with the `::staged` index-diff key above.
|
|
82
|
+
const cacheKey = `${opts.cwd}::ref::${ref}`;
|
|
31
83
|
const cached = PER_PROCESS_CACHE.get(cacheKey);
|
|
32
84
|
if (cached)
|
|
33
85
|
return cached;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
for (const line of stdout.split('\n')) {
|
|
42
|
-
const trimmed = line.trim();
|
|
43
|
-
if (!trimmed)
|
|
44
|
-
continue;
|
|
45
|
-
const m = trimmed.match(/^(\w)\s+(.+)$/);
|
|
46
|
-
if (!m)
|
|
47
|
-
continue;
|
|
48
|
-
const [, status, path] = m;
|
|
49
|
-
const norm = nodePath.normalize(path).split(nodePath.sep).join('/');
|
|
50
|
-
if (status === 'D')
|
|
51
|
-
deleted.push(norm);
|
|
52
|
-
else
|
|
53
|
-
changed.push(norm);
|
|
54
|
-
}
|
|
55
|
-
const result = { ref, changed, deleted, isAvailable: true };
|
|
56
|
-
PER_PROCESS_CACHE.set(cacheKey, result);
|
|
57
|
-
return result;
|
|
58
|
-
}
|
|
59
|
-
catch (e) {
|
|
86
|
+
// Two-dot diff against the ref to match the inspector's
|
|
87
|
+
// `getChangedFiles({ since })` behavior — includes both committed
|
|
88
|
+
// changes and the working tree relative to <ref>. `runGitLines` is
|
|
89
|
+
// shell-free + high-maxBuffer, so a large changeset no longer ENOBUFS-crashes
|
|
90
|
+
// the `impact --deleted` orphan check (this is its only write-safety guard).
|
|
91
|
+
const out = runGitLines(opts.cwd, ['diff', '--name-status', ref]);
|
|
92
|
+
if (!out.ok) {
|
|
60
93
|
return {
|
|
61
94
|
ref,
|
|
62
95
|
changed: [],
|
|
63
96
|
deleted: [],
|
|
64
97
|
isAvailable: false,
|
|
65
|
-
error
|
|
98
|
+
...(out.error ? { error: out.error } : {}),
|
|
66
99
|
};
|
|
67
100
|
}
|
|
101
|
+
const { changed, deleted } = parseNameStatus(out.lines);
|
|
102
|
+
const result = { ref, changed, deleted, isAvailable: true };
|
|
103
|
+
PER_PROCESS_CACHE.set(cacheKey, result);
|
|
104
|
+
return result;
|
|
68
105
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { IDeletedOrphanReport } from '@shrkcrft/impact-engine';
|
|
2
|
+
/**
|
|
3
|
+
* Why the scan could not produce an orphan report (so callers pick an exit
|
|
4
|
+
* code / message): the diff was unavailable (no resolvable ref, git error) or
|
|
5
|
+
* the code-graph store is missing.
|
|
6
|
+
*/
|
|
7
|
+
export type DeletedOrphanScanFailure = 'diff-unavailable' | 'graph-missing';
|
|
8
|
+
/** Result of {@link computeDeletedOrphans}. */
|
|
9
|
+
export interface IDeletedOrphanScan {
|
|
10
|
+
/** True when the diff resolved (and, when anything was deleted, the graph too). */
|
|
11
|
+
readonly ok: boolean;
|
|
12
|
+
/** What was diffed: `'STAGED'` for `--staged`, otherwise the resolved ref. */
|
|
13
|
+
readonly ref: string;
|
|
14
|
+
/** Deleted files read from the diff (empty list is a valid, clean result). */
|
|
15
|
+
readonly deleted: readonly string[];
|
|
16
|
+
/**
|
|
17
|
+
* The alias-resolved orphan report. Present iff `ok` AND something was
|
|
18
|
+
* deleted — when `deleted` is empty the scan short-circuits clean without
|
|
19
|
+
* loading the graph, so there is no report (and none is needed).
|
|
20
|
+
*/
|
|
21
|
+
readonly report?: IDeletedOrphanReport;
|
|
22
|
+
/** Why the scan failed. Present iff not `ok`. */
|
|
23
|
+
readonly reason?: DeletedOrphanScanFailure;
|
|
24
|
+
/** Human-readable failure detail. */
|
|
25
|
+
readonly error?: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Diff-driven reverse-closure: read the files DELETED in the changeset (vs
|
|
29
|
+
* `since`, or the staged index when `staged`), then query the code-graph
|
|
30
|
+
* snapshot for surviving files that still import them or reference a symbol
|
|
31
|
+
* they declared (alias-resolved, incl. barrel re-exports). The shared core
|
|
32
|
+
* behind `impact --deleted`, `check orphans`, and the composite `finish` gate.
|
|
33
|
+
*
|
|
34
|
+
* Never throws: a bad diff → `{ reason: 'diff-unavailable' }`, a missing index
|
|
35
|
+
* → `{ reason: 'graph-missing' }`. Graph + engine are imported lazily so a
|
|
36
|
+
* command that never hits the orphan path doesn't pay to load them.
|
|
37
|
+
*/
|
|
38
|
+
export declare function computeDeletedOrphans(cwd: string, opts: {
|
|
39
|
+
since?: string;
|
|
40
|
+
staged?: boolean;
|
|
41
|
+
}): Promise<IDeletedOrphanScan>;
|
|
42
|
+
//# sourceMappingURL=deleted-orphans.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deleted-orphans.d.ts","sourceRoot":"","sources":["../../src/diff/deleted-orphans.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAGpE;;;;GAIG;AACH,MAAM,MAAM,wBAAwB,GAAG,kBAAkB,GAAG,eAAe,CAAC;AAE5E,+CAA+C;AAC/C,MAAM,WAAW,kBAAkB;IACjC,mFAAmF;IACnF,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,8EAA8E;IAC9E,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,8EAA8E;IAC9E,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC;;;;OAIG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,oBAAoB,CAAC;IACvC,iDAAiD;IACjD,QAAQ,CAAC,MAAM,CAAC,EAAE,wBAAwB,CAAC;IAC3C,qCAAqC;IACrC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,qBAAqB,CACzC,GAAG,EAAE,MAAM,EACX,IAAI,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,GACzC,OAAO,CAAC,kBAAkB,CAAC,CAmC7B"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { collectChangedPaths } from "./collect-changed-paths.js";
|
|
2
|
+
/**
|
|
3
|
+
* Diff-driven reverse-closure: read the files DELETED in the changeset (vs
|
|
4
|
+
* `since`, or the staged index when `staged`), then query the code-graph
|
|
5
|
+
* snapshot for surviving files that still import them or reference a symbol
|
|
6
|
+
* they declared (alias-resolved, incl. barrel re-exports). The shared core
|
|
7
|
+
* behind `impact --deleted`, `check orphans`, and the composite `finish` gate.
|
|
8
|
+
*
|
|
9
|
+
* Never throws: a bad diff → `{ reason: 'diff-unavailable' }`, a missing index
|
|
10
|
+
* → `{ reason: 'graph-missing' }`. Graph + engine are imported lazily so a
|
|
11
|
+
* command that never hits the orphan path doesn't pay to load them.
|
|
12
|
+
*/
|
|
13
|
+
export async function computeDeletedOrphans(cwd, opts) {
|
|
14
|
+
const changed = collectChangedPaths({
|
|
15
|
+
cwd,
|
|
16
|
+
...(opts.since ? { ref: opts.since } : {}),
|
|
17
|
+
...(opts.staged ? { staged: true } : {}),
|
|
18
|
+
});
|
|
19
|
+
if (!changed.isAvailable) {
|
|
20
|
+
return {
|
|
21
|
+
ok: false,
|
|
22
|
+
ref: changed.ref,
|
|
23
|
+
deleted: [],
|
|
24
|
+
reason: 'diff-unavailable',
|
|
25
|
+
...(changed.error ? { error: changed.error } : {}),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
// Nothing deleted → clean by definition; don't pay to load the graph (and
|
|
29
|
+
// don't fail when no index exists — there is nothing to check).
|
|
30
|
+
if (changed.deleted.length === 0) {
|
|
31
|
+
return { ok: true, ref: changed.ref, deleted: [] };
|
|
32
|
+
}
|
|
33
|
+
const { GraphStore, GraphQueryApi } = await import('@shrkcrft/graph');
|
|
34
|
+
if (!new GraphStore(cwd).exists()) {
|
|
35
|
+
return {
|
|
36
|
+
ok: false,
|
|
37
|
+
ref: changed.ref,
|
|
38
|
+
deleted: changed.deleted,
|
|
39
|
+
reason: 'graph-missing',
|
|
40
|
+
error: 'code-graph store missing — run `shrk graph index` first.',
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
const { findDeletedOrphans } = await import('@shrkcrft/impact-engine');
|
|
44
|
+
const report = findDeletedOrphans(GraphQueryApi.fromStore(cwd), changed.deleted);
|
|
45
|
+
return { ok: true, ref: changed.ref, deleted: changed.deleted, report };
|
|
46
|
+
}
|