@shrkcrft/cli 0.1.0-alpha.26 → 0.1.0-alpha.27
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 +12 -0
- package/dist/command-registry.d.ts.map +1 -1
- package/dist/command-registry.js +25 -0
- package/dist/commands/changelog-data.d.ts.map +1 -1
- package/dist/commands/changelog-data.js +21 -0
- package/dist/commands/delegate.command.d.ts +76 -1
- package/dist/commands/delegate.command.d.ts.map +1 -1
- package/dist/commands/delegate.command.js +585 -25
- package/dist/commands/finish.command.js +4 -4
- package/dist/commands/help.command.d.ts.map +1 -1
- package/dist/commands/help.command.js +73 -0
- package/dist/commands/registry-resolve.d.ts +11 -4
- package/dist/commands/registry-resolve.d.ts.map +1 -1
- package/dist/commands/registry-resolve.js +50 -24
- package/dist/commands/trace.command.d.ts.map +1 -1
- package/dist/commands/trace.command.js +7 -1
- package/dist/commands/wiring.command.d.ts.map +1 -1
- package/dist/commands/wiring.command.js +88 -14
- package/dist/exit-codes.d.ts +41 -0
- package/dist/exit-codes.d.ts.map +1 -1
- package/dist/exit-codes.js +85 -0
- package/dist/finish/run-finish.d.ts +22 -3
- package/dist/finish/run-finish.d.ts.map +1 -1
- package/dist/finish/run-finish.js +185 -15
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +21 -4
- package/dist/output/output-compression.d.ts.map +1 -1
- package/dist/output/output-compression.js +4 -1
- package/package.json +33 -33
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run-finish.d.ts","sourceRoot":"","sources":["../../src/finish/run-finish.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"run-finish.d.ts","sourceRoot":"","sources":["../../src/finish/run-finish.ts"],"names":[],"mappings":"AAAA,OAAO,EAOL,KAAK,oBAAoB,EAC1B,MAAM,qBAAqB,CAAC;AAiB7B,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAE5C,eAAO,MAAM,aAAa,EAAG,sBAA+B,CAAC;AAQ7D,2FAA2F;AAC3F,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;AAE3D,8EAA8E;AAC9E,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,YAAY,GAAG,SAAS,GAAG,QAAQ,GAAG,YAAY,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,CAAC;IAClG,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;IAClC,uEAAuE;IACvE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,oEAAoE;IACpE,QAAQ,CAAC,KAAK,EAAE,SAAS,WAAW,EAAE,CAAC;IACvC;;;;;;OAMG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,oBAAoB,CAAC,EAAE,MAAM,CAAC;IACvC,yEAAyE;IACzE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,OAAO,aAAa,CAAC;IACtC,QAAQ,CAAC,KAAK,EAAE;QACd,QAAQ,CAAC,IAAI,EAAE,UAAU,GAAG,QAAQ,GAAG,OAAO,GAAG,OAAO,CAAC;QACzD,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;QAClC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;KAC5B,CAAC;IACF,QAAQ,CAAC,KAAK,EAAE,SAAS,WAAW,EAAE,CAAC;IACvC,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B;;;;;;;;OAQG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,cAAc,CAAC;IACnD,6EAA6E;IAC7E,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,mEAAmE;IACnE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,2EAA2E;IAC3E,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,UAAU,GAAG,QAAQ,GAAG,OAAO,GAAG,OAAO,CAAC;IACzD,QAAQ,CAAC,KAAK,EAAE,oBAAoB,CAAC;CACtC;AAmBD;;;;;;;;GAQG;AACH,wBAAsB,cAAc,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,aAAa,CAAC,CAmJnF"}
|
|
@@ -1,9 +1,23 @@
|
|
|
1
|
-
import { buildImportHygieneReport, filterViolationsToChangedScope, inspectSharkcraft, resolveChangedFiles, resolveProjectConfig, } from '@shrkcrft/inspector';
|
|
1
|
+
import { buildImportHygieneReport, filterViolationsToChangedScope, gitShowFile, inspectSharkcraft, resolveChangedFiles, resolveProjectConfig, } from '@shrkcrft/inspector';
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
3
|
import * as nodePath from 'node:path';
|
|
4
|
-
import { evaluateBoundaries, loadTsconfigPaths, runPolicyLint, runWiring, scanImports, } from '@shrkcrft/boundaries';
|
|
4
|
+
import { buildRegistrationGraph, evaluateBoundaries, loadTsconfigPaths, providedTokensFromEntries, registrationTouchesChanged, registrationUnprovided, runPolicyLint, runWiring, scanImports, } from '@shrkcrft/boundaries';
|
|
5
5
|
import { computeDeletedOrphans } from "../diff/deleted-orphans.js";
|
|
6
|
+
import { ExitCode } from "../exit-codes.js";
|
|
6
7
|
export const FINISH_SCHEMA = 'sharkcraft.finish/v1';
|
|
8
|
+
/** Files in the boundary/import/wiring domain — a change outside it evaluates nothing there. */
|
|
9
|
+
const CODE_FILE = /\.(?:m|c)?[jt]sx?$/;
|
|
10
|
+
function codeFilesOf(files) {
|
|
11
|
+
return files.filter((f) => CODE_FILE.test(f));
|
|
12
|
+
}
|
|
13
|
+
/** The base ref the unprovided gate diffs against to spot removed providers. */
|
|
14
|
+
function baseRefFor(input) {
|
|
15
|
+
if (input.mode === 'files')
|
|
16
|
+
return undefined; // an explicit file list has no diff base
|
|
17
|
+
if (input.mode === 'since' && input.scope.since)
|
|
18
|
+
return input.scope.since;
|
|
19
|
+
return 'HEAD'; // worktree + staged both diff vs HEAD
|
|
20
|
+
}
|
|
7
21
|
/** Map the changed-scope onto the orphan check's diff inputs. */
|
|
8
22
|
function orphanOptsFor(input) {
|
|
9
23
|
if (input.mode === 'files')
|
|
@@ -29,6 +43,11 @@ export async function runFinishGates(input) {
|
|
|
29
43
|
const { cwd } = input;
|
|
30
44
|
const changed = resolveChangedFiles(input.scope);
|
|
31
45
|
const changedFiles = changed.files;
|
|
46
|
+
// The boundary/import/wiring engines only reason about code files. A change to
|
|
47
|
+
// non-code files (docs, JSON, config) evaluates NOTHING in those gates, so they
|
|
48
|
+
// must SKIP, not trivially pass — otherwise a markdown-only change paints green
|
|
49
|
+
// ("evaluated nothing" must read as not-verified, never a `0`).
|
|
50
|
+
const codeChanged = codeFilesOf(changedFiles);
|
|
32
51
|
const gates = [];
|
|
33
52
|
// ── boundaries (changed-only) ────────────────────────────────────────
|
|
34
53
|
const inspection = await inspectSharkcraft({ cwd });
|
|
@@ -36,8 +55,8 @@ export async function runFinishGates(input) {
|
|
|
36
55
|
if (boundaryRules.length === 0) {
|
|
37
56
|
gates.push(skip('boundaries', 'no boundary rules configured'));
|
|
38
57
|
}
|
|
39
|
-
else if (
|
|
40
|
-
gates.push(skip('boundaries', 'no files in changed scope'));
|
|
58
|
+
else if (codeChanged.length === 0) {
|
|
59
|
+
gates.push(skip('boundaries', 'no code files in changed scope'));
|
|
41
60
|
}
|
|
42
61
|
else {
|
|
43
62
|
const scan = scanImports({ projectRoot: cwd });
|
|
@@ -62,11 +81,11 @@ export async function runFinishGates(input) {
|
|
|
62
81
|
});
|
|
63
82
|
}
|
|
64
83
|
// ── import hygiene (changed-only) ────────────────────────────────────
|
|
65
|
-
if (
|
|
66
|
-
gates.push(skip('imports', 'no files in changed scope'));
|
|
84
|
+
if (codeChanged.length === 0) {
|
|
85
|
+
gates.push(skip('imports', 'no code files in changed scope'));
|
|
67
86
|
}
|
|
68
87
|
else {
|
|
69
|
-
const report = buildImportHygieneReport(cwd, { files:
|
|
88
|
+
const report = buildImportHygieneReport(cwd, { files: codeChanged });
|
|
70
89
|
const errors = report.counts?.['error'] ?? (report.verdict === 'errors' ? report.findings.length : 0);
|
|
71
90
|
const warnings = report.counts?.['warning'] ?? (report.verdict === 'warnings' ? report.findings.length : 0);
|
|
72
91
|
gates.push({
|
|
@@ -96,34 +115,61 @@ export async function runFinishGates(input) {
|
|
|
96
115
|
? `config did not load: ${loaded.error.message}`
|
|
97
116
|
: 'no sharkcraft config (gate not applicable)';
|
|
98
117
|
gates.push(skip('wiring', detail));
|
|
118
|
+
gates.push(skip('unprovided', detail));
|
|
99
119
|
gates.push(skip('policy', detail));
|
|
100
120
|
}
|
|
101
121
|
else {
|
|
102
|
-
gates.push(wiringGate(cwd, loaded.value.config.wiringRules ?? [], changedFiles), policyGate(cwd, loaded.value.config.policyRules ?? [], changedFiles));
|
|
122
|
+
gates.push(wiringGate(cwd, loaded.value.config.wiringRules ?? [], changedFiles), unprovidedGate(cwd, loaded.value.config.registrationGraph ?? [], changedFiles, baseRefFor(input)), policyGate(cwd, loaded.value.config.policyRules ?? [], changedFiles));
|
|
103
123
|
}
|
|
104
124
|
// ── deleted-orphans (write-safety) ───────────────────────────────────
|
|
105
125
|
gates.push(await orphansGate(input));
|
|
126
|
+
// ── architecture (advisory): cycles the change participates in ───────
|
|
127
|
+
gates.push(await archGate(cwd, changedFiles));
|
|
106
128
|
// ── impact summary (informational, best-effort) ──────────────────────
|
|
107
129
|
const impact = await impactSummary(cwd, changedFiles);
|
|
108
|
-
// ── fold into one verdict
|
|
109
|
-
|
|
130
|
+
// ── fold into one honest 0/1/2 verdict ───────────────────────────────
|
|
131
|
+
// Only NON-advisory ("deciding") gates decide the verdict. A deciding fail (or
|
|
132
|
+
// a config that could not load) is `1`. Otherwise, if NO deciding gate actually
|
|
133
|
+
// evaluated anything (all skipped), the run verified nothing → `2` (never a
|
|
134
|
+
// green `0`). Only when at least one deciding gate ran over a real scope and
|
|
135
|
+
// none failed is it a true `0`.
|
|
136
|
+
const deciding = gates.filter((g) => !g.advisory);
|
|
137
|
+
const failed = deciding.filter((g) => g.status === 'fail');
|
|
138
|
+
const anyEvaluated = deciding.some((g) => g.status === 'pass');
|
|
110
139
|
const warnings = gates.reduce((n, g) => n + g.warnings, 0);
|
|
111
|
-
|
|
140
|
+
let verdict;
|
|
141
|
+
let exit;
|
|
142
|
+
if (failed.length > 0 || configError) {
|
|
143
|
+
verdict = 'fail';
|
|
144
|
+
exit = ExitCode.Failure;
|
|
145
|
+
}
|
|
146
|
+
else if (!anyEvaluated) {
|
|
147
|
+
verdict = 'not-verified';
|
|
148
|
+
exit = ExitCode.NotVerified;
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
verdict = 'pass';
|
|
152
|
+
exit = ExitCode.VerifiedPass;
|
|
153
|
+
}
|
|
112
154
|
const skipped = gates.filter((g) => g.status === 'skipped').map((g) => g.name);
|
|
155
|
+
const advisoryWarned = gates.filter((g) => g.advisory && g.warnings > 0).map((g) => g.name);
|
|
113
156
|
const summary = verdict === 'fail'
|
|
114
157
|
? `Not safe to finish: ${configError ? 'config failed to load; ' : ''}${failed.map((g) => `${g.name} (${g.errors} error(s))`).join(', ') || 'see gates'}.`
|
|
115
|
-
:
|
|
116
|
-
?
|
|
117
|
-
: `Safe to finish: every applicable gate passed${warnings > 0 ? ` (${warnings} non-blocking warning(s))` : ''}${skipped.length > 0 ? `; skipped: ${skipped.join(', ')}` : ''}.`;
|
|
158
|
+
: verdict === 'not-verified'
|
|
159
|
+
? `Not verified: no gate evaluated the changed scope${changedFiles.length === 0 ? ' (nothing changed)' : ' (nothing in it is gate-relevant)'} — this is NOT a pass. Re-run over a scope with code changes, or gate explicitly.`
|
|
160
|
+
: `Safe to finish: every applicable gate passed${warnings > 0 ? ` (${warnings} non-blocking warning(s)${advisoryWarned.length > 0 ? ` — see ${advisoryWarned.join(', ')}` : ''})` : ''}${skipped.length > 0 ? `; skipped: ${skipped.join(', ')}` : ''}.`;
|
|
118
161
|
const nextAction = verdict === 'fail'
|
|
119
162
|
? 'Fix every failing gate item (each carries file:line), then re-run `shrk finish`.'
|
|
120
|
-
:
|
|
163
|
+
: verdict === 'not-verified'
|
|
164
|
+
? 'Nothing was verified — do not treat this as done.'
|
|
165
|
+
: 'Safe to declare done.';
|
|
121
166
|
return {
|
|
122
167
|
schema: FINISH_SCHEMA,
|
|
123
168
|
scope: { mode: input.mode, files: changedFiles, fileCount: changedFiles.length },
|
|
124
169
|
gates,
|
|
125
170
|
impact,
|
|
126
171
|
verdict,
|
|
172
|
+
exit,
|
|
127
173
|
warnings,
|
|
128
174
|
...(configError ? { configError } : {}),
|
|
129
175
|
summary,
|
|
@@ -133,6 +179,10 @@ export async function runFinishGates(input) {
|
|
|
133
179
|
function skip(name, detail) {
|
|
134
180
|
return { name, status: 'skipped', detail, errors: 0, warnings: 0, items: [] };
|
|
135
181
|
}
|
|
182
|
+
/** A skipped ADVISORY gate — reports its detail but never decides the verdict. */
|
|
183
|
+
function advisorySkip(name, detail) {
|
|
184
|
+
return { name, status: 'skipped', detail, errors: 0, warnings: 0, items: [], advisory: true };
|
|
185
|
+
}
|
|
136
186
|
function wiringGate(cwd, rules, changedFiles) {
|
|
137
187
|
if (rules.length === 0)
|
|
138
188
|
return skip('wiring', 'no wiring rules configured');
|
|
@@ -188,6 +238,126 @@ function policyGate(cwd, rules, changedFiles) {
|
|
|
188
238
|
],
|
|
189
239
|
};
|
|
190
240
|
}
|
|
241
|
+
/**
|
|
242
|
+
* The runtime-wiring `unprovided` gate — the silent-at-runtime class imports
|
|
243
|
+
* can't see: a token DECLARED or INJECTED but never PROVIDED (typecheck-green,
|
|
244
|
+
* absent at runtime). Backed by the registration/DI graph, scoped to the
|
|
245
|
+
* changeset so only tokens THIS change touches decide the verdict. Skips (never
|
|
246
|
+
* fails) when no idioms are configured or the change touched no wiring — so a
|
|
247
|
+
* repo that never modeled its DI is not penalized.
|
|
248
|
+
*/
|
|
249
|
+
function unprovidedGate(cwd, idioms, changedFiles, baseRef) {
|
|
250
|
+
if (idioms.length === 0)
|
|
251
|
+
return skip('unprovided', 'no registration idioms configured');
|
|
252
|
+
const graph = buildRegistrationGraph(cwd, idioms);
|
|
253
|
+
const diag = graph.diagnostics;
|
|
254
|
+
// Two ways THIS change can leave a token unprovided:
|
|
255
|
+
// (1) a declared/injected site added in a changed file with no provider —
|
|
256
|
+
// caught by scoping the graph query to the changed files;
|
|
257
|
+
// (2) the last PROVIDER removed from a changed file — which leaves NO site in
|
|
258
|
+
// the changed file, so (1) structurally can't see it. Recover it by
|
|
259
|
+
// diffing the base content of the changed files (providerRegressions).
|
|
260
|
+
const scoped = registrationUnprovided(graph, changedFiles);
|
|
261
|
+
const regressions = baseRef ? providerRegressions(cwd, idioms, graph, changedFiles, baseRef) : [];
|
|
262
|
+
const byToken = new Map();
|
|
263
|
+
for (const u of [...scoped, ...regressions])
|
|
264
|
+
if (!byToken.has(u.token))
|
|
265
|
+
byToken.set(u.token, u);
|
|
266
|
+
const unprovided = [...byToken.values()].sort((a, b) => a.token.localeCompare(b.token));
|
|
267
|
+
// Skip (evaluated nothing) only when the change touched no registration site
|
|
268
|
+
// AND removed no provider AND has no misconfigured idiom — never a silent pass.
|
|
269
|
+
if (!registrationTouchesChanged(graph, changedFiles) && unprovided.length === 0 && diag.length === 0) {
|
|
270
|
+
return skip('unprovided', 'no registration sites in the changed scope');
|
|
271
|
+
}
|
|
272
|
+
return {
|
|
273
|
+
name: 'unprovided',
|
|
274
|
+
status: unprovided.length > 0 || diag.length > 0 ? 'fail' : 'pass',
|
|
275
|
+
detail: `${unprovided.length} unprovided token(s) attributable to the change${diag.length > 0 ? `, ${diag.length} misconfigured idiom(s)` : ''}`,
|
|
276
|
+
errors: unprovided.length + diag.length,
|
|
277
|
+
warnings: 0,
|
|
278
|
+
items: [
|
|
279
|
+
...unprovided.map((u) => {
|
|
280
|
+
const site = u.declared[0] ?? u.consumed[0];
|
|
281
|
+
return {
|
|
282
|
+
...(site ? { file: site.file, line: site.line } : {}),
|
|
283
|
+
message: `"${u.token}" declared/injected but never provided (silent at runtime)`,
|
|
284
|
+
};
|
|
285
|
+
}),
|
|
286
|
+
...diag.map((d) => ({ message: `misconfigured idiom: ${d}` })),
|
|
287
|
+
],
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Tokens whose LAST provider this change removed. Reads the base content of each
|
|
292
|
+
* changed code file, extracts what it USED to provide, and keeps any token now
|
|
293
|
+
* provided nowhere in the worktree but still declared/consumed — the runtime
|
|
294
|
+
* break (`inject(T)` → undefined) that deleting a provider registration causes,
|
|
295
|
+
* which leaves no site in the changed file for post-change scoping to catch.
|
|
296
|
+
*/
|
|
297
|
+
function providerRegressions(cwd, idioms, graph, changedFiles, baseRef) {
|
|
298
|
+
const codeChanged = codeFilesOf(changedFiles);
|
|
299
|
+
if (codeChanged.length === 0)
|
|
300
|
+
return [];
|
|
301
|
+
const baseEntries = [];
|
|
302
|
+
for (const f of codeChanged) {
|
|
303
|
+
const content = gitShowFile(cwd, baseRef, f);
|
|
304
|
+
if (content !== null)
|
|
305
|
+
baseEntries.push({ path: f, content });
|
|
306
|
+
}
|
|
307
|
+
if (baseEntries.length === 0)
|
|
308
|
+
return [];
|
|
309
|
+
const wasProvided = providedTokensFromEntries(idioms, baseEntries);
|
|
310
|
+
const out = [];
|
|
311
|
+
for (const token of wasProvided) {
|
|
312
|
+
const node = graph.tokens.find((t) => t.token === token);
|
|
313
|
+
if (node && node.provided.length === 0 && (node.declared.length > 0 || node.consumed.length > 0)) {
|
|
314
|
+
out.push({ token, declared: node.declared, consumed: node.consumed });
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return out;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Advisory architecture gate: does any changed file participate in a runtime
|
|
321
|
+
* import cycle? Distinct from the `boundaries` gate (which sees layer violations,
|
|
322
|
+
* not cycles). Deliberately ADVISORY — a pre-existing cycle a change merely
|
|
323
|
+
* touches must not be attributed to this changeset, so it reports as a
|
|
324
|
+
* non-blocking warning and never fails the composite. Type-only import edges are
|
|
325
|
+
* excluded (they erase at emit and can't cause a runtime cycle). Best-effort:
|
|
326
|
+
* a missing graph index degrades to skip, never fail.
|
|
327
|
+
*/
|
|
328
|
+
async function archGate(cwd, changedFiles) {
|
|
329
|
+
const codeChanged = codeFilesOf(changedFiles);
|
|
330
|
+
if (codeChanged.length === 0)
|
|
331
|
+
return advisorySkip('arch', 'no code files in changed scope');
|
|
332
|
+
try {
|
|
333
|
+
const { GraphStore, GraphQueryApi } = await import('@shrkcrft/graph');
|
|
334
|
+
if (!new GraphStore(cwd).exists()) {
|
|
335
|
+
return advisorySkip('arch', 'code-graph index missing — run `shrk graph index`');
|
|
336
|
+
}
|
|
337
|
+
const api = GraphQueryApi.fromStore(cwd);
|
|
338
|
+
const cycles = api.cycles(); // runtime cycles only (type-only edges excluded)
|
|
339
|
+
const scope = new Set(codeChanged.map((f) => f.replace(/\\/g, '/').replace(/^\.\//, '')));
|
|
340
|
+
const touching = cycles.filter((c) => (c.paths ?? c.nodeIds.map((id) => id.replace(/^file:/, ''))).some((p) => scope.has(p)));
|
|
341
|
+
if (touching.length === 0) {
|
|
342
|
+
return advisorySkip('arch', `no changed file participates in an import cycle (${cycles.length} runtime cycle(s) in repo)`);
|
|
343
|
+
}
|
|
344
|
+
return {
|
|
345
|
+
name: 'arch',
|
|
346
|
+
status: 'pass', // advisory: reports cycles as warnings, never fails the composite
|
|
347
|
+
advisory: true,
|
|
348
|
+
detail: `${touching.length} changed file(s) participate in a runtime import cycle (advisory — not attributed to this change)`,
|
|
349
|
+
errors: 0,
|
|
350
|
+
warnings: touching.length,
|
|
351
|
+
items: touching.slice(0, 15).map((c) => {
|
|
352
|
+
const paths = c.paths ?? c.nodeIds.map((id) => id.replace(/^file:/, ''));
|
|
353
|
+
return { message: `import cycle (size ${c.size}): ${paths.join(' → ')}` };
|
|
354
|
+
}),
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
catch (e) {
|
|
358
|
+
return advisorySkip('arch', `arch check unavailable: ${e instanceof Error ? e.message : String(e)}`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
191
361
|
async function orphansGate(input) {
|
|
192
362
|
const opts = orphanOptsFor(input);
|
|
193
363
|
if (!opts)
|
package/dist/main.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":";AAEA,OAAO,EACL,eAAe,
|
|
1
|
+
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":";AAEA,OAAO,EACL,eAAe,EAMhB,MAAM,uBAAuB,CAAC;AA8X/B,wBAAgB,aAAa,IAAI,eAAe,CAiY/C;AAED,wBAAsB,MAAM,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CA+CrE;AAwID;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAoDxE"}
|
package/dist/main.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { loadDotenv } from "./env/load-dotenv.js";
|
|
3
|
-
import { CommandRegistry, extractGlobalCompress, extractGlobalCwd, parseArgs, } from "./command-registry.js";
|
|
4
|
-
import { argvHasStrict, promoteForStrict } from "./exit-codes.js";
|
|
3
|
+
import { CommandRegistry, extractGlobalCompress, extractGlobalCwd, extractGlobalExitTrailer, parseArgs, } from "./command-registry.js";
|
|
4
|
+
import { argvHasExitTrailer, argvHasStrict, emitPipeExitSignal, promoteForStrict } from "./exit-codes.js";
|
|
5
5
|
import { runCommandWithCompression } from "./output/output-compression.js";
|
|
6
6
|
import { initCommand } from "./commands/init.command.js";
|
|
7
7
|
import { inspectCommand } from "./commands/inspect.command.js";
|
|
@@ -516,15 +516,29 @@ export async function runCli(argv) {
|
|
|
516
516
|
// and each command's own `--strict` semantics (e.g. `check --strict`) still
|
|
517
517
|
// apply beneath this (a25 §1.1).
|
|
518
518
|
const strict = argvHasStrict(argv);
|
|
519
|
+
// Derive the command path from an argv with the global --exit-trailer stripped:
|
|
520
|
+
// otherwise a LEADING `--exit-trailer` makes extractCommandPath break on the '-'
|
|
521
|
+
// and return "" — zeroing out both the pipe-exit signal and the usage record.
|
|
522
|
+
const command = extractCommandPath(extractGlobalExitTrailer(probeArgv).rest);
|
|
519
523
|
let exitCode = 0;
|
|
520
524
|
try {
|
|
521
525
|
exitCode = promoteForStrict(await runCliInner(argv), strict);
|
|
526
|
+
// Keep the honest 0/1/2 verdict readable through a trailing pipe (the shape
|
|
527
|
+
// an agent reaches for first). A no-op for non-gate verbs. Skipped inside the
|
|
528
|
+
// smart-context worker child and the --compress re-run child, whose stdout is
|
|
529
|
+
// a pipe by construction — a spurious "stdout is piped" note there (the user
|
|
530
|
+
// never piped) would be noise, not signal.
|
|
531
|
+
if (!process.env.SHRK_WORKER_EXITCODE_FILE && !process.env.SHRK_COMPRESS_CHILD) {
|
|
532
|
+
emitPipeExitSignal(command, exitCode, {
|
|
533
|
+
piped: !process.stdout.isTTY,
|
|
534
|
+
trailer: argvHasExitTrailer(argv),
|
|
535
|
+
});
|
|
536
|
+
}
|
|
522
537
|
return exitCode;
|
|
523
538
|
}
|
|
524
539
|
finally {
|
|
525
540
|
// append one local usage entry. Failure is silent.
|
|
526
541
|
try {
|
|
527
|
-
const command = extractCommandPath(probeArgv);
|
|
528
542
|
if (command.length > 0) {
|
|
529
543
|
const flags = sanitizeFlagNames(probeArgv);
|
|
530
544
|
const enabled = await isUsageEnabled(probeCwd ?? process.cwd());
|
|
@@ -566,8 +580,11 @@ async function runCliInner(argv) {
|
|
|
566
580
|
const registry = buildRegistry();
|
|
567
581
|
// Pre-parse the global --cwd so it can appear anywhere (incl. before the command).
|
|
568
582
|
const { cwd: globalCwd, rest: cwdCleanArgv } = extractGlobalCwd(argv);
|
|
583
|
+
// Strip the global --exit-trailer here so no per-command flag-guard sees it;
|
|
584
|
+
// runCli reads it off the raw argv to decide whether to print the trailer.
|
|
585
|
+
const { rest: trailerCleanArgv } = extractGlobalExitTrailer(cwdCleanArgv);
|
|
569
586
|
// Pre-parse the global --compress / --ccr output-compression flags.
|
|
570
|
-
const { directive: compressDirective, rest: cleanArgv } = extractGlobalCompress(
|
|
587
|
+
const { directive: compressDirective, rest: cleanArgv } = extractGlobalCompress(trailerCleanArgv);
|
|
571
588
|
const [first] = cleanArgv;
|
|
572
589
|
// `--compress` / `--ccr` on a real command: re-run it and compress its stdout.
|
|
573
590
|
// (Meta verbs like --help/--version and bare invocations are left untouched.)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"output-compression.d.ts","sourceRoot":"","sources":["../../src/output/output-compression.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,wBAAwB,CAAC;AAQvE;;;;;;;;;;;GAWG;AACH,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,SAAS,MAAM,EAAE,EAC5B,SAAS,EAAE,wBAAwB,EACnC,GAAG,EAAE,MAAM,GACV,MAAM,
|
|
1
|
+
{"version":3,"file":"output-compression.d.ts","sourceRoot":"","sources":["../../src/output/output-compression.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,wBAAwB,CAAC;AAQvE;;;;;;;;;;;GAWG;AACH,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,SAAS,MAAM,EAAE,EAC5B,SAAS,EAAE,wBAAwB,EACnC,GAAG,EAAE,MAAM,GACV,MAAM,CAmDR"}
|
|
@@ -32,7 +32,10 @@ export function runCommandWithCompression(childArgv, directive, cwd) {
|
|
|
32
32
|
const res = spawnSync(runtime, [entry, ...childArgv], {
|
|
33
33
|
encoding: 'utf8',
|
|
34
34
|
maxBuffer: MAX_BUFFER,
|
|
35
|
-
|
|
35
|
+
// Mark the child so it suppresses the piped-stdout exit note: its stdout is
|
|
36
|
+
// captured here (always "piped") but the USER may not have piped anything —
|
|
37
|
+
// the parent decides the note from the real user-facing stdout instead.
|
|
38
|
+
env: { ...process.env, SHRK_COMPRESS_CHILD: '1' },
|
|
36
39
|
});
|
|
37
40
|
if (res.error) {
|
|
38
41
|
process.stderr.write(`--compress: failed to run the command (${res.error.message}); emitting nothing.\n`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shrkcrft/cli",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.27",
|
|
4
4
|
"description": "SharkCraft CLI (`shrk`): structured project intelligence for AI coding agents.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "SharkCraft contributors",
|
|
@@ -47,38 +47,38 @@
|
|
|
47
47
|
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@shrkcrft/core": "^0.1.0-alpha.
|
|
51
|
-
"@shrkcrft/compress": "^0.1.0-alpha.
|
|
52
|
-
"@shrkcrft/config": "^0.1.0-alpha.
|
|
53
|
-
"@shrkcrft/workspace": "^0.1.0-alpha.
|
|
54
|
-
"@shrkcrft/knowledge": "^0.1.0-alpha.
|
|
55
|
-
"@shrkcrft/context": "^0.1.0-alpha.
|
|
56
|
-
"@shrkcrft/rules": "^0.1.0-alpha.
|
|
57
|
-
"@shrkcrft/paths": "^0.1.0-alpha.
|
|
58
|
-
"@shrkcrft/templates": "^0.1.0-alpha.
|
|
59
|
-
"@shrkcrft/plugin-api": "^0.1.0-alpha.
|
|
60
|
-
"@shrkcrft/dashboard": "^0.1.0-alpha.
|
|
61
|
-
"@shrkcrft/dashboard-api": "^0.1.0-alpha.
|
|
62
|
-
"@shrkcrft/pipelines": "^0.1.0-alpha.
|
|
63
|
-
"@shrkcrft/presets": "^0.1.0-alpha.
|
|
64
|
-
"@shrkcrft/boundaries": "^0.1.0-alpha.
|
|
65
|
-
"@shrkcrft/graph": "^0.1.0-alpha.
|
|
66
|
-
"@shrkcrft/rule-graph": "^0.1.0-alpha.
|
|
67
|
-
"@shrkcrft/structural-search": "^0.1.0-alpha.
|
|
68
|
-
"@shrkcrft/impact-engine": "^0.1.0-alpha.
|
|
69
|
-
"@shrkcrft/context-planner": "^0.1.0-alpha.
|
|
70
|
-
"@shrkcrft/architecture-guard": "^0.1.0-alpha.
|
|
71
|
-
"@shrkcrft/framework-scanners": "^0.1.0-alpha.
|
|
72
|
-
"@shrkcrft/api-surface-diff": "^0.1.0-alpha.
|
|
73
|
-
"@shrkcrft/quality-gates": "^0.1.0-alpha.
|
|
74
|
-
"@shrkcrft/migrate": "^0.1.0-alpha.
|
|
75
|
-
"@shrkcrft/generator": "^0.1.0-alpha.
|
|
76
|
-
"@shrkcrft/importer": "^0.1.0-alpha.
|
|
77
|
-
"@shrkcrft/inspector": "^0.1.0-alpha.
|
|
78
|
-
"@shrkcrft/ai": "^0.1.0-alpha.
|
|
79
|
-
"@shrkcrft/embeddings": "^0.1.0-alpha.
|
|
80
|
-
"@shrkcrft/shared": "^0.1.0-alpha.
|
|
81
|
-
"@shrkcrft/mcp-server": "^0.1.0-alpha.
|
|
50
|
+
"@shrkcrft/core": "^0.1.0-alpha.27",
|
|
51
|
+
"@shrkcrft/compress": "^0.1.0-alpha.27",
|
|
52
|
+
"@shrkcrft/config": "^0.1.0-alpha.27",
|
|
53
|
+
"@shrkcrft/workspace": "^0.1.0-alpha.27",
|
|
54
|
+
"@shrkcrft/knowledge": "^0.1.0-alpha.27",
|
|
55
|
+
"@shrkcrft/context": "^0.1.0-alpha.27",
|
|
56
|
+
"@shrkcrft/rules": "^0.1.0-alpha.27",
|
|
57
|
+
"@shrkcrft/paths": "^0.1.0-alpha.27",
|
|
58
|
+
"@shrkcrft/templates": "^0.1.0-alpha.27",
|
|
59
|
+
"@shrkcrft/plugin-api": "^0.1.0-alpha.27",
|
|
60
|
+
"@shrkcrft/dashboard": "^0.1.0-alpha.27",
|
|
61
|
+
"@shrkcrft/dashboard-api": "^0.1.0-alpha.27",
|
|
62
|
+
"@shrkcrft/pipelines": "^0.1.0-alpha.27",
|
|
63
|
+
"@shrkcrft/presets": "^0.1.0-alpha.27",
|
|
64
|
+
"@shrkcrft/boundaries": "^0.1.0-alpha.27",
|
|
65
|
+
"@shrkcrft/graph": "^0.1.0-alpha.27",
|
|
66
|
+
"@shrkcrft/rule-graph": "^0.1.0-alpha.27",
|
|
67
|
+
"@shrkcrft/structural-search": "^0.1.0-alpha.27",
|
|
68
|
+
"@shrkcrft/impact-engine": "^0.1.0-alpha.27",
|
|
69
|
+
"@shrkcrft/context-planner": "^0.1.0-alpha.27",
|
|
70
|
+
"@shrkcrft/architecture-guard": "^0.1.0-alpha.27",
|
|
71
|
+
"@shrkcrft/framework-scanners": "^0.1.0-alpha.27",
|
|
72
|
+
"@shrkcrft/api-surface-diff": "^0.1.0-alpha.27",
|
|
73
|
+
"@shrkcrft/quality-gates": "^0.1.0-alpha.27",
|
|
74
|
+
"@shrkcrft/migrate": "^0.1.0-alpha.27",
|
|
75
|
+
"@shrkcrft/generator": "^0.1.0-alpha.27",
|
|
76
|
+
"@shrkcrft/importer": "^0.1.0-alpha.27",
|
|
77
|
+
"@shrkcrft/inspector": "^0.1.0-alpha.27",
|
|
78
|
+
"@shrkcrft/ai": "^0.1.0-alpha.27",
|
|
79
|
+
"@shrkcrft/embeddings": "^0.1.0-alpha.27",
|
|
80
|
+
"@shrkcrft/shared": "^0.1.0-alpha.27",
|
|
81
|
+
"@shrkcrft/mcp-server": "^0.1.0-alpha.27",
|
|
82
82
|
"@huggingface/transformers": "^3.7.5"
|
|
83
83
|
},
|
|
84
84
|
"publishConfig": {
|