@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.
Files changed (108) hide show
  1. package/dist/command-registry.d.ts +9 -0
  2. package/dist/command-registry.d.ts.map +1 -1
  3. package/dist/command-registry.js +15 -0
  4. package/dist/commands/ai-status.command.d.ts.map +1 -1
  5. package/dist/commands/ai-status.command.js +10 -0
  6. package/dist/commands/apply.command.d.ts.map +1 -1
  7. package/dist/commands/apply.command.js +9 -2
  8. package/dist/commands/ask.command.d.ts.map +1 -1
  9. package/dist/commands/ask.command.js +2 -1
  10. package/dist/commands/changes.command.d.ts.map +1 -1
  11. package/dist/commands/changes.command.js +14 -4
  12. package/dist/commands/check.command.d.ts.map +1 -1
  13. package/dist/commands/check.command.js +139 -7
  14. package/dist/commands/command-catalog.d.ts +11 -0
  15. package/dist/commands/command-catalog.d.ts.map +1 -1
  16. package/dist/commands/command-catalog.js +291 -0
  17. package/dist/commands/commands.command.d.ts +15 -0
  18. package/dist/commands/commands.command.d.ts.map +1 -1
  19. package/dist/commands/commands.command.js +162 -3
  20. package/dist/commands/compress.command.d.ts.map +1 -1
  21. package/dist/commands/compress.command.js +39 -4
  22. package/dist/commands/context.command.d.ts.map +1 -1
  23. package/dist/commands/context.command.js +3 -1
  24. package/dist/commands/daily.commands.d.ts.map +1 -1
  25. package/dist/commands/daily.commands.js +48 -5
  26. package/dist/commands/diff-check.command.d.ts.map +1 -1
  27. package/dist/commands/diff-check.command.js +6 -2
  28. package/dist/commands/feedback.command.d.ts.map +1 -1
  29. package/dist/commands/feedback.command.js +4 -0
  30. package/dist/commands/finish.command.d.ts +3 -0
  31. package/dist/commands/finish.command.d.ts.map +1 -0
  32. package/dist/commands/finish.command.js +70 -0
  33. package/dist/commands/framework.command.d.ts.map +1 -1
  34. package/dist/commands/framework.command.js +20 -3
  35. package/dist/commands/gate.command.d.ts.map +1 -1
  36. package/dist/commands/gate.command.js +98 -13
  37. package/dist/commands/graph-code-subverbs.d.ts.map +1 -1
  38. package/dist/commands/graph-code-subverbs.js +215 -81
  39. package/dist/commands/graph.command.d.ts.map +1 -1
  40. package/dist/commands/graph.command.js +6 -0
  41. package/dist/commands/help.command.d.ts.map +1 -1
  42. package/dist/commands/help.command.js +20 -2
  43. package/dist/commands/impact.command.d.ts.map +1 -1
  44. package/dist/commands/impact.command.js +86 -1
  45. package/dist/commands/knowledge.command.d.ts.map +1 -1
  46. package/dist/commands/knowledge.command.js +21 -1
  47. package/dist/commands/onboard.command.d.ts.map +1 -1
  48. package/dist/commands/onboard.command.js +11 -2
  49. package/dist/commands/orchestrate.command.d.ts.map +1 -1
  50. package/dist/commands/orchestrate.command.js +6 -1
  51. package/dist/commands/pack-author.command.d.ts.map +1 -1
  52. package/dist/commands/pack-author.command.js +13 -1
  53. package/dist/commands/packs.command.d.ts.map +1 -1
  54. package/dist/commands/packs.command.js +95 -17
  55. package/dist/commands/paths.command.d.ts.map +1 -1
  56. package/dist/commands/paths.command.js +8 -1
  57. package/dist/commands/pipelines.command.d.ts.map +1 -1
  58. package/dist/commands/pipelines.command.js +2 -1
  59. package/dist/commands/plan-context.command.d.ts.map +1 -1
  60. package/dist/commands/plan-context.command.js +6 -3
  61. package/dist/commands/plan-simulate.command.js +1 -1
  62. package/dist/commands/policy-lint.command.d.ts.map +1 -1
  63. package/dist/commands/policy-lint.command.js +20 -7
  64. package/dist/commands/recommend.command.d.ts.map +1 -1
  65. package/dist/commands/recommend.command.js +10 -2
  66. package/dist/commands/registry.command.d.ts.map +1 -1
  67. package/dist/commands/registry.command.js +117 -9
  68. package/dist/commands/report.command.js +1 -1
  69. package/dist/commands/reuse.command.d.ts.map +1 -1
  70. package/dist/commands/reuse.command.js +17 -5
  71. package/dist/commands/review.command.d.ts.map +1 -1
  72. package/dist/commands/review.command.js +20 -2
  73. package/dist/commands/rule-graph-subverbs.d.ts.map +1 -1
  74. package/dist/commands/rule-graph-subverbs.js +31 -4
  75. package/dist/commands/search-structural.command.d.ts.map +1 -1
  76. package/dist/commands/search-structural.command.js +2 -2
  77. package/dist/commands/search.command.d.ts.map +1 -1
  78. package/dist/commands/search.command.js +19 -1
  79. package/dist/commands/simulate.command.d.ts.map +1 -1
  80. package/dist/commands/simulate.command.js +6 -1
  81. package/dist/commands/smart-context.command.d.ts.map +1 -1
  82. package/dist/commands/smart-context.command.js +101 -41
  83. package/dist/commands/spec.command.d.ts.map +1 -1
  84. package/dist/commands/spec.command.js +73 -13
  85. package/dist/commands/task-context.command.d.ts.map +1 -1
  86. package/dist/commands/task-context.command.js +26 -2
  87. package/dist/commands/test.command.d.ts.map +1 -1
  88. package/dist/commands/test.command.js +17 -0
  89. package/dist/commands/trace.command.d.ts.map +1 -1
  90. package/dist/commands/trace.command.js +87 -4
  91. package/dist/commands/wiring.command.d.ts +12 -0
  92. package/dist/commands/wiring.command.d.ts.map +1 -0
  93. package/dist/commands/wiring.command.js +384 -0
  94. package/dist/diff/collect-changed-paths.d.ts +5 -3
  95. package/dist/diff/collect-changed-paths.d.ts.map +1 -1
  96. package/dist/diff/collect-changed-paths.js +73 -36
  97. package/dist/diff/deleted-orphans.d.ts +42 -0
  98. package/dist/diff/deleted-orphans.d.ts.map +1 -0
  99. package/dist/diff/deleted-orphans.js +46 -0
  100. package/dist/finish/run-finish.d.ts +62 -0
  101. package/dist/finish/run-finish.d.ts.map +1 -0
  102. package/dist/finish/run-finish.js +239 -0
  103. package/dist/main.d.ts.map +1 -1
  104. package/dist/main.js +19 -1
  105. package/dist/status/freshness.d.ts +30 -0
  106. package/dist/status/freshness.d.ts.map +1 -0
  107. package/dist/status/freshness.js +81 -0
  108. package/package.json +33 -33
@@ -0,0 +1,62 @@
1
+ import { type IChangedScopeOptions } from '@shrkcrft/inspector';
2
+ export declare const FINISH_SCHEMA: "sharkcraft.finish/v1";
3
+ /** Outcome of one sub-gate. `skipped` = nothing to evaluate (loud, never silent green). */
4
+ export type FinishGateStatus = 'pass' | 'fail' | 'skipped';
5
+ /** One failing/relevant item, with file:line where the engine provides it. */
6
+ export interface IFinishItem {
7
+ readonly file?: string;
8
+ readonly line?: number;
9
+ readonly message: string;
10
+ }
11
+ export interface IFinishGate {
12
+ readonly name: 'boundaries' | 'imports' | 'wiring' | 'policy' | 'orphans';
13
+ readonly status: FinishGateStatus;
14
+ /** One-line reason (e.g. why skipped, or the error/warning counts). */
15
+ readonly detail: string;
16
+ readonly errors: number;
17
+ readonly warnings: number;
18
+ /** Failing/notable items (capped by the renderer, full in JSON). */
19
+ readonly items: readonly IFinishItem[];
20
+ }
21
+ export interface IFinishImpact {
22
+ readonly ran: boolean;
23
+ readonly risk?: string;
24
+ readonly directDependents?: number;
25
+ readonly transitiveDependents?: number;
26
+ /** Why the summary could not run (no graph index / no changed files). */
27
+ readonly note?: string;
28
+ }
29
+ export interface IFinishReport {
30
+ readonly schema: typeof FINISH_SCHEMA;
31
+ readonly scope: {
32
+ readonly mode: 'worktree' | 'staged' | 'since' | 'files';
33
+ readonly files: readonly string[];
34
+ readonly fileCount: number;
35
+ };
36
+ readonly gates: readonly IFinishGate[];
37
+ readonly impact: IFinishImpact;
38
+ /** `fail` iff any gate failed OR the config could not load; else `pass`. */
39
+ readonly verdict: 'pass' | 'fail';
40
+ /** Total warning-severity findings across gates (non-blocking). */
41
+ readonly warnings: number;
42
+ /** Set when sharkcraft.config.ts could not be loaded — forces a `fail`. */
43
+ readonly configError?: string;
44
+ readonly summary: string;
45
+ readonly nextAction: string;
46
+ }
47
+ export interface IRunFinishInput {
48
+ readonly cwd: string;
49
+ readonly mode: 'worktree' | 'staged' | 'since' | 'files';
50
+ readonly scope: IChangedScopeOptions;
51
+ }
52
+ /**
53
+ * The composite "is this changeset safe to finish?" orchestrator. Runs every
54
+ * deterministic CHANGED-ONLY gate inline — boundaries + import-hygiene + wiring
55
+ * + policy + deleted-orphans — plus a best-effort impact summary, and folds
56
+ * them into ONE pass/fail. This is the single trustworthy "done?" call an
57
+ * autonomous agent needs and can't reliably assemble by hand (only shrk can run
58
+ * the alias-resolved layer/wiring gates). Honors the `0-rules → skipped`
59
+ * semantics so a no-op sub-check is reported, never silently passed. Read-only.
60
+ */
61
+ export declare function runFinishGates(input: IRunFinishInput): Promise<IFinishReport>;
62
+ //# sourceMappingURL=run-finish.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run-finish.d.ts","sourceRoot":"","sources":["../../src/finish/run-finish.ts"],"names":[],"mappings":"AAAA,OAAO,EAML,KAAK,oBAAoB,EAC1B,MAAM,qBAAqB,CAAC;AAa7B,eAAO,MAAM,aAAa,EAAG,sBAA+B,CAAC;AAE7D,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,QAAQ,GAAG,SAAS,CAAC;IAC1E,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;CACxC;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,4EAA4E;IAC5E,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC;IAClC,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;AAYD;;;;;;;;GAQG;AACH,wBAAsB,cAAc,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,aAAa,CAAC,CAkHnF"}
@@ -0,0 +1,239 @@
1
+ import { buildImportHygieneReport, filterViolationsToChangedScope, inspectSharkcraft, resolveChangedFiles, resolveProjectConfig, } from '@shrkcrft/inspector';
2
+ import { existsSync } from 'node:fs';
3
+ import * as nodePath from 'node:path';
4
+ import { evaluateBoundaries, loadTsconfigPaths, runPolicyLint, runWiring, scanImports, } from '@shrkcrft/boundaries';
5
+ import { computeDeletedOrphans } from "../diff/deleted-orphans.js";
6
+ export const FINISH_SCHEMA = 'sharkcraft.finish/v1';
7
+ /** Map the changed-scope onto the orphan check's diff inputs. */
8
+ function orphanOptsFor(input) {
9
+ if (input.mode === 'files')
10
+ return undefined; // a file list has no diff to read deletions from
11
+ if (input.mode === 'staged')
12
+ return { staged: true };
13
+ if (input.mode === 'since' && input.scope.since)
14
+ return { since: input.scope.since };
15
+ // Worktree mode: diff deletions vs HEAD so the orphan gate's "deleted" set
16
+ // matches the working-tree scope the other gates use (not the whole branch).
17
+ return { since: 'HEAD' };
18
+ }
19
+ /**
20
+ * The composite "is this changeset safe to finish?" orchestrator. Runs every
21
+ * deterministic CHANGED-ONLY gate inline — boundaries + import-hygiene + wiring
22
+ * + policy + deleted-orphans — plus a best-effort impact summary, and folds
23
+ * them into ONE pass/fail. This is the single trustworthy "done?" call an
24
+ * autonomous agent needs and can't reliably assemble by hand (only shrk can run
25
+ * the alias-resolved layer/wiring gates). Honors the `0-rules → skipped`
26
+ * semantics so a no-op sub-check is reported, never silently passed. Read-only.
27
+ */
28
+ export async function runFinishGates(input) {
29
+ const { cwd } = input;
30
+ const changed = resolveChangedFiles(input.scope);
31
+ const changedFiles = changed.files;
32
+ const gates = [];
33
+ // ── boundaries (changed-only) ────────────────────────────────────────
34
+ const inspection = await inspectSharkcraft({ cwd });
35
+ const boundaryRules = inspection.boundaryRegistry.list();
36
+ if (boundaryRules.length === 0) {
37
+ gates.push(skip('boundaries', 'no boundary rules configured'));
38
+ }
39
+ else if (changedFiles.length === 0) {
40
+ gates.push(skip('boundaries', 'no files in changed scope'));
41
+ }
42
+ else {
43
+ const scan = scanImports({ projectRoot: cwd });
44
+ const tsconfigPaths = loadTsconfigPaths(cwd);
45
+ const evalResult = evaluateBoundaries(scan, boundaryRules, {
46
+ ...(tsconfigPaths.aliases.size > 0 ? { tsconfigPaths } : {}),
47
+ });
48
+ const filtered = filterViolationsToChangedScope(evalResult.violations, input.scope);
49
+ const errors = filtered.includedViolations.filter((v) => v.severity === 'error');
50
+ const warnings = filtered.includedViolations.filter((v) => v.severity === 'warning');
51
+ gates.push({
52
+ name: 'boundaries',
53
+ status: errors.length > 0 ? 'fail' : 'pass',
54
+ detail: `${errors.length} error(s), ${warnings.length} warning(s) across ${changedFiles.length} changed file(s)`,
55
+ errors: errors.length,
56
+ warnings: warnings.length,
57
+ items: [...errors, ...warnings].map((v) => ({
58
+ file: v.file,
59
+ line: v.line,
60
+ message: `[${v.severity}] ${v.ruleId}: ${v.message}`,
61
+ })),
62
+ });
63
+ }
64
+ // ── import hygiene (changed-only) ────────────────────────────────────
65
+ if (changedFiles.length === 0) {
66
+ gates.push(skip('imports', 'no files in changed scope'));
67
+ }
68
+ else {
69
+ const report = buildImportHygieneReport(cwd, { files: changedFiles });
70
+ const errors = report.counts?.['error'] ?? (report.verdict === 'errors' ? report.findings.length : 0);
71
+ const warnings = report.counts?.['warning'] ?? (report.verdict === 'warnings' ? report.findings.length : 0);
72
+ gates.push({
73
+ name: 'imports',
74
+ status: report.verdict === 'errors' ? 'fail' : 'pass',
75
+ detail: `verdict=${report.verdict} (${report.findings.length} finding(s))`,
76
+ errors,
77
+ warnings,
78
+ items: report.findings.map((f) => ({
79
+ file: f.file,
80
+ line: f.line,
81
+ message: `${f.kind}: ${f.suggestedFix || f.reason || f.snippet}`.trim(),
82
+ })),
83
+ });
84
+ }
85
+ // ── wiring + policy (changed-only, from resolved config) ─────────────
86
+ const loaded = await resolveProjectConfig(cwd);
87
+ let configError;
88
+ if (!loaded.ok) {
89
+ // A MALFORMED config in a real sharkcraft project is a fail (the wiring/
90
+ // policy gates can't be trusted). The mere ABSENCE of a sharkcraft/ folder
91
+ // is not — those gates simply don't apply, so skip them without failing.
92
+ const isSharkcraftProject = existsSync(nodePath.join(cwd, 'sharkcraft'));
93
+ if (isSharkcraftProject)
94
+ configError = loaded.error.message;
95
+ const detail = isSharkcraftProject
96
+ ? `config did not load: ${loaded.error.message}`
97
+ : 'no sharkcraft config (gate not applicable)';
98
+ gates.push(skip('wiring', detail));
99
+ gates.push(skip('policy', detail));
100
+ }
101
+ else {
102
+ gates.push(wiringGate(cwd, loaded.value.config.wiringRules ?? [], changedFiles), policyGate(cwd, loaded.value.config.policyRules ?? [], changedFiles));
103
+ }
104
+ // ── deleted-orphans (write-safety) ───────────────────────────────────
105
+ gates.push(await orphansGate(input));
106
+ // ── impact summary (informational, best-effort) ──────────────────────
107
+ const impact = await impactSummary(cwd, changedFiles);
108
+ // ── fold into one verdict ────────────────────────────────────────────
109
+ const failed = gates.filter((g) => g.status === 'fail');
110
+ const warnings = gates.reduce((n, g) => n + g.warnings, 0);
111
+ const verdict = failed.length > 0 || configError ? 'fail' : 'pass';
112
+ const skipped = gates.filter((g) => g.status === 'skipped').map((g) => g.name);
113
+ const summary = verdict === 'fail'
114
+ ? `Not safe to finish: ${configError ? 'config failed to load; ' : ''}${failed.map((g) => `${g.name} (${g.errors} error(s))`).join(', ') || 'see gates'}.`
115
+ : changedFiles.length === 0 && input.mode !== 'files'
116
+ ? 'No files in the changed scope — nothing to gate.'
117
+ : `Safe to finish: every applicable gate passed${warnings > 0 ? ` (${warnings} non-blocking warning(s))` : ''}${skipped.length > 0 ? `; skipped: ${skipped.join(', ')}` : ''}.`;
118
+ const nextAction = verdict === 'fail'
119
+ ? 'Fix every failing gate item (each carries file:line), then re-run `shrk finish`.'
120
+ : 'Safe to declare done.';
121
+ return {
122
+ schema: FINISH_SCHEMA,
123
+ scope: { mode: input.mode, files: changedFiles, fileCount: changedFiles.length },
124
+ gates,
125
+ impact,
126
+ verdict,
127
+ warnings,
128
+ ...(configError ? { configError } : {}),
129
+ summary,
130
+ nextAction,
131
+ };
132
+ }
133
+ function skip(name, detail) {
134
+ return { name, status: 'skipped', detail, errors: 0, warnings: 0, items: [] };
135
+ }
136
+ function wiringGate(cwd, rules, changedFiles) {
137
+ if (rules.length === 0)
138
+ return skip('wiring', 'no wiring rules configured');
139
+ const report = runWiring(cwd, rules, { changedOnly: true, changedFiles });
140
+ if (report.evaluated === 0) {
141
+ return skip('wiring', `${report.rules.length} rule(s) configured but none matched the changed scope`);
142
+ }
143
+ const errors = report.violations.filter((v) => v.severity === 'error');
144
+ const warnings = report.violations.filter((v) => v.severity === 'warning');
145
+ // A misconfigured rule (bad regex / no capture group) yields a diagnostic +
146
+ // rule-level error but NO violation — it must FAIL the gate, never read as a
147
+ // silent green ("loud, never silent green").
148
+ const diag = report.diagnostics;
149
+ return {
150
+ name: 'wiring',
151
+ status: report.verdict === 'errors' || diag.length > 0 ? 'fail' : 'pass',
152
+ detail: `${report.evaluated}/${report.rules.length} rule(s) evaluated — ${errors.length} error(s), ${warnings.length} warning(s)${diag.length > 0 ? `, ${diag.length} misconfigured` : ''}`,
153
+ errors: errors.length + diag.length,
154
+ warnings: warnings.length,
155
+ items: [
156
+ ...report.violations.map((v) => ({
157
+ file: v.file,
158
+ line: v.line,
159
+ message: `[${v.severity}] ${v.ruleId}: "${v.token}" ${v.direction === 'registered-missing' ? 'registered but not declared' : 'declared but not registered'}`,
160
+ })),
161
+ ...diag.map((d) => ({ message: `misconfigured rule: ${d}` })),
162
+ ],
163
+ };
164
+ }
165
+ function policyGate(cwd, rules, changedFiles) {
166
+ if (rules.length === 0)
167
+ return skip('policy', 'no policy rules configured');
168
+ const report = runPolicyLint(cwd, rules, { changedOnly: true, changedFiles });
169
+ if (report.evaluated === 0) {
170
+ return skip('policy', `${report.rules.length} rule(s) configured but none matched the changed scope`);
171
+ }
172
+ const errors = report.findings.filter((f) => f.severity === 'error');
173
+ const warnings = report.findings.filter((f) => f.severity === 'warning');
174
+ const diag = report.diagnostics;
175
+ return {
176
+ name: 'policy',
177
+ status: report.verdict === 'errors' || diag.length > 0 ? 'fail' : 'pass',
178
+ detail: `${report.evaluated}/${report.rules.length} rule(s) evaluated — ${errors.length} error(s), ${warnings.length} warning(s)${diag.length > 0 ? `, ${diag.length} misconfigured` : ''}`,
179
+ errors: errors.length + diag.length,
180
+ warnings: warnings.length,
181
+ items: [
182
+ ...report.findings.map((f) => ({
183
+ file: f.file,
184
+ line: f.line,
185
+ message: `[${f.severity}] ${f.ruleId}: ${f.message ?? ''}`.trim(),
186
+ })),
187
+ ...diag.map((d) => ({ message: `misconfigured rule: ${d}` })),
188
+ ],
189
+ };
190
+ }
191
+ async function orphansGate(input) {
192
+ const opts = orphanOptsFor(input);
193
+ if (!opts)
194
+ return skip('orphans', 'explicit --files scope has no diff to read deletions from');
195
+ const scan = await computeDeletedOrphans(input.cwd, opts);
196
+ if (!scan.ok) {
197
+ return skip('orphans', scan.reason === 'graph-missing'
198
+ ? 'code-graph index missing — run `shrk graph index` to enable the orphan check'
199
+ : `diff unavailable: ${scan.error ?? 'unknown'}`);
200
+ }
201
+ if (scan.deleted.length === 0)
202
+ return skip('orphans', `nothing deleted (vs ${scan.ref})`);
203
+ const orphans = scan.report?.orphans ?? [];
204
+ return {
205
+ name: 'orphans',
206
+ status: orphans.length > 0 ? 'fail' : 'pass',
207
+ detail: `${scan.deleted.length} deleted file(s) (vs ${scan.ref}) — ${orphans.length} surviving importer(s)`,
208
+ errors: orphans.length,
209
+ warnings: 0,
210
+ items: orphans.map((o) => ({
211
+ file: o.path ?? o.id,
212
+ ...(typeof o.line === 'number' ? { line: o.line } : {}),
213
+ message: o.via === 'reference' && o.symbol
214
+ ? `references \`${o.symbol}\` from deleted ${o.deletedFile}`
215
+ : `imports deleted ${o.deletedFile}`,
216
+ })),
217
+ };
218
+ }
219
+ async function impactSummary(cwd, changedFiles) {
220
+ if (changedFiles.length === 0)
221
+ return { ran: false, note: 'no changed files' };
222
+ try {
223
+ const { GraphStore } = await import('@shrkcrft/graph');
224
+ if (!new GraphStore(cwd).exists()) {
225
+ return { ran: false, note: 'code-graph index missing — run `shrk graph index`' };
226
+ }
227
+ const { analyzeGraphImpact } = await import('@shrkcrft/impact-engine');
228
+ const analysis = analyzeGraphImpact({ kind: 'files', files: [...changedFiles] }, { projectRoot: cwd, maxDepth: 5, limit: 200 });
229
+ return {
230
+ ran: true,
231
+ risk: analysis.risk,
232
+ directDependents: analysis.directDependents.length,
233
+ transitiveDependents: analysis.transitiveDependents.length,
234
+ };
235
+ }
236
+ catch (e) {
237
+ return { ran: false, note: `impact summary unavailable: ${e instanceof Error ? e.message : String(e)}` };
238
+ }
239
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":";AAEA,OAAO,EACL,eAAe,EAKhB,MAAM,uBAAuB,CAAC;AA0X/B,wBAAgB,aAAa,IAAI,eAAe,CA8X/C;AAED,wBAAsB,MAAM,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CA2BrE;AAsHD;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAoDxE"}
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":";AAEA,OAAO,EACL,eAAe,EAKhB,MAAM,uBAAuB,CAAC;AA4X/B,wBAAgB,aAAa,IAAI,eAAe,CAgY/C;AAED,wBAAsB,MAAM,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CA2BrE;AAqID;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAoDxE"}
package/dist/main.js CHANGED
@@ -22,6 +22,7 @@ import { presetsListCommand, presetsGetCommand, presetsExplainCommand, presetsRe
22
22
  import { taskCommand } from "./commands/task.command.js";
23
23
  import { preflightCommand } from "./commands/preflight.command.js";
24
24
  import { checkCommand } from "./commands/check.command.js";
25
+ import { finishCommand } from "./commands/finish.command.js";
25
26
  import { diffCheckCommand } from "./commands/diff-check.command.js";
26
27
  import { driftCommand } from "./commands/drift.command.js";
27
28
  import { graphCommand } from "./commands/graph.command.js";
@@ -33,6 +34,7 @@ import { frameworkCommand } from "./commands/framework.command.js";
33
34
  import { apiDiffCommand } from "./commands/api-diff.command.js";
34
35
  import { gateCommand } from "./commands/gate.command.js";
35
36
  import { policyLintCommand } from "./commands/policy-lint.command.js";
37
+ import { wiringCommand } from "./commands/wiring.command.js";
36
38
  import { reuseCommand } from "./commands/reuse.command.js";
37
39
  import { migrateCommand } from "./commands/migrate.command.js";
38
40
  import { coverageCommand } from "./commands/coverage.command.js";
@@ -189,6 +191,7 @@ export function buildRegistry() {
189
191
  registry.register(explainCommand);
190
192
  registry.register(checkCommand);
191
193
  registry.register(diffCheckCommand);
194
+ registry.register(finishCommand);
192
195
  // changed-only preflight orchestrator.
193
196
  registry.register(preflightCommand);
194
197
  registry.register(driftCommand);
@@ -201,6 +204,7 @@ export function buildRegistry() {
201
204
  registry.register(apiDiffCommand);
202
205
  registry.register(gateCommand);
203
206
  registry.register(policyLintCommand);
207
+ registry.register(wiringCommand);
204
208
  registry.register(reuseCommand);
205
209
  registry.register(migrateCommand);
206
210
  registry.register(coverageCommand);
@@ -601,7 +605,21 @@ async function runCliInner(argv) {
601
605
  // (`shrk doctor`, `shrk packs list`, `shrk pack author status`). The
602
606
  // resolver stops when it hits a flag or a token that isn't a child of the
603
607
  // current node, returning the deepest handler + the leftover tokens.
604
- const { handler, matchedPath, rest: leftover, node } = registry.resolve(cleanArgv);
608
+ let { handler, matchedPath, rest: leftover, node } = registry.resolve(cleanArgv);
609
+ // A multi-word command passed as a SINGLE quoted token (`shrk "graph status"`)
610
+ // arrives as one argv element with internal whitespace. The trie has no atomic
611
+ // `graph status` node, so the first descent misses (handler undefined) — yet
612
+ // did-you-mean would still list `graph status` as the #1 closest match, a
613
+ // self-contradiction. Re-split the lone token on whitespace and retry so the
614
+ // quoted form behaves identically to the unquoted `shrk graph status`.
615
+ // Genuinely unknown tokens leave `handler` undefined and fall through to the
616
+ // existing unknown-command path unchanged.
617
+ if (!handler && cleanArgv.length === 1 && /\s/.test(cleanArgv[0])) {
618
+ const retry = registry.resolve(cleanArgv[0].trim().split(/\s+/));
619
+ if (retry.handler) {
620
+ ({ handler, matchedPath, rest: leftover, node } = retry);
621
+ }
622
+ }
605
623
  // `--help` / `-h` immediately after a (sub)group → render help for that
606
624
  // path. This works at any depth.
607
625
  if (leftover[0] === '--help' || leftover[0] === '-h') {
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Source-mtime freshness — a small shared CLI helper.
3
+ *
4
+ * Several `… status` commands (`framework status`, `context status`) need to
5
+ * answer the same question: "is this stored artefact stale relative to the
6
+ * source tree it was built from?" They all walk the workspace for the newest
7
+ * source-file mtime and compare it to a stored build timestamp. This module
8
+ * is the single home for that walk so the commands stay byte-identical.
9
+ *
10
+ * NOTE: this is deliberately CLI-local and distinct from
11
+ * `@shrkcrft/graph`'s `detectGraphFreshness` — that one models the graph
12
+ * store's own freshness and graph sits BELOW cli in the layer order, so it
13
+ * cannot be reused here.
14
+ */
15
+ export declare const FRESHNESS_SKIP_DIRS: Set<string>;
16
+ export declare const FRESHNESS_SOURCE_EXTS: Set<string>;
17
+ export interface IStatusFreshness {
18
+ state: 'fresh' | 'stale' | 'unknown';
19
+ lastBuiltAt: string | null;
20
+ lastChangedAt: string | null;
21
+ behindMs: number;
22
+ }
23
+ /** Newest source-file mtime (ms) under `root`, ignoring build/output dirs. 0
24
+ * when nothing matched. */
25
+ export declare function newestSourceMtimeMs(root: string): number;
26
+ /** Compare a stored build timestamp to the newest source mtime in `scope`.
27
+ * `stale` when a source file is newer than the build; `unknown` when the
28
+ * build timestamp can't be parsed. */
29
+ export declare function computeMtimeFreshness(scope: string, builtAtIso: string | undefined | null): IStatusFreshness;
30
+ //# sourceMappingURL=freshness.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"freshness.d.ts","sourceRoot":"","sources":["../../src/status/freshness.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;GAaG;AAEH,eAAO,MAAM,mBAAmB,aAG9B,CAAC;AACH,eAAO,MAAM,qBAAqB,aAGhC,CAAC;AAEH,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,OAAO,GAAG,OAAO,GAAG,SAAS,CAAC;IACrC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;4BAC4B;AAC5B,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CA8BxD;AAED;;uCAEuC;AACvC,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,gBAAgB,CAc5G"}
@@ -0,0 +1,81 @@
1
+ import { readdirSync, statSync } from 'node:fs';
2
+ import * as nodePath from 'node:path';
3
+ /**
4
+ * Source-mtime freshness — a small shared CLI helper.
5
+ *
6
+ * Several `… status` commands (`framework status`, `context status`) need to
7
+ * answer the same question: "is this stored artefact stale relative to the
8
+ * source tree it was built from?" They all walk the workspace for the newest
9
+ * source-file mtime and compare it to a stored build timestamp. This module
10
+ * is the single home for that walk so the commands stay byte-identical.
11
+ *
12
+ * NOTE: this is deliberately CLI-local and distinct from
13
+ * `@shrkcrft/graph`'s `detectGraphFreshness` — that one models the graph
14
+ * store's own freshness and graph sits BELOW cli in the layer order, so it
15
+ * cannot be reused here.
16
+ */
17
+ export const FRESHNESS_SKIP_DIRS = new Set([
18
+ 'node_modules', 'dist', 'build', 'coverage', '.git', '.sharkcraft',
19
+ '.next', '.cache', '.tmp-pack', 'out', 'target',
20
+ ]);
21
+ export const FRESHNESS_SOURCE_EXTS = new Set([
22
+ '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs',
23
+ '.java', '.cs', '.py', '.go', '.rs', '.kt', '.swift',
24
+ ]);
25
+ /** Newest source-file mtime (ms) under `root`, ignoring build/output dirs. 0
26
+ * when nothing matched. */
27
+ export function newestSourceMtimeMs(root) {
28
+ let newest = 0;
29
+ const stack = [root];
30
+ while (stack.length > 0) {
31
+ const dir = stack.pop();
32
+ let entries;
33
+ try {
34
+ entries = readdirSync(dir);
35
+ }
36
+ catch {
37
+ continue;
38
+ }
39
+ for (const name of entries) {
40
+ if (FRESHNESS_SKIP_DIRS.has(name) || name.startsWith('.'))
41
+ continue;
42
+ const full = nodePath.join(dir, name);
43
+ let st;
44
+ try {
45
+ st = statSync(full);
46
+ }
47
+ catch {
48
+ continue;
49
+ }
50
+ if (st.isDirectory()) {
51
+ stack.push(full);
52
+ continue;
53
+ }
54
+ if (!st.isFile())
55
+ continue;
56
+ if (!FRESHNESS_SOURCE_EXTS.has(nodePath.extname(full).toLowerCase()))
57
+ continue;
58
+ if (st.mtimeMs > newest)
59
+ newest = st.mtimeMs;
60
+ }
61
+ }
62
+ return newest;
63
+ }
64
+ /** Compare a stored build timestamp to the newest source mtime in `scope`.
65
+ * `stale` when a source file is newer than the build; `unknown` when the
66
+ * build timestamp can't be parsed. */
67
+ export function computeMtimeFreshness(scope, builtAtIso) {
68
+ const newestMs = newestSourceMtimeMs(scope);
69
+ const lastChangedAt = newestMs > 0 ? new Date(newestMs).toISOString() : null;
70
+ const builtMs = builtAtIso ? Date.parse(builtAtIso) : NaN;
71
+ if (!Number.isFinite(builtMs)) {
72
+ return { state: 'unknown', lastBuiltAt: builtAtIso ?? null, lastChangedAt, behindMs: 0 };
73
+ }
74
+ const behindMs = newestMs > builtMs ? Math.round(newestMs - builtMs) : 0;
75
+ return {
76
+ state: behindMs > 0 ? 'stale' : 'fresh',
77
+ lastBuiltAt: builtAtIso ?? null,
78
+ lastChangedAt,
79
+ behindMs,
80
+ };
81
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shrkcrft/cli",
3
- "version": "0.1.0-alpha.22",
3
+ "version": "0.1.0-alpha.24",
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.22",
51
- "@shrkcrft/compress": "^0.1.0-alpha.22",
52
- "@shrkcrft/config": "^0.1.0-alpha.22",
53
- "@shrkcrft/workspace": "^0.1.0-alpha.22",
54
- "@shrkcrft/knowledge": "^0.1.0-alpha.22",
55
- "@shrkcrft/context": "^0.1.0-alpha.22",
56
- "@shrkcrft/rules": "^0.1.0-alpha.22",
57
- "@shrkcrft/paths": "^0.1.0-alpha.22",
58
- "@shrkcrft/templates": "^0.1.0-alpha.22",
59
- "@shrkcrft/plugin-api": "^0.1.0-alpha.22",
60
- "@shrkcrft/dashboard": "^0.1.0-alpha.22",
61
- "@shrkcrft/dashboard-api": "^0.1.0-alpha.22",
62
- "@shrkcrft/pipelines": "^0.1.0-alpha.22",
63
- "@shrkcrft/presets": "^0.1.0-alpha.22",
64
- "@shrkcrft/boundaries": "^0.1.0-alpha.22",
65
- "@shrkcrft/graph": "^0.1.0-alpha.22",
66
- "@shrkcrft/rule-graph": "^0.1.0-alpha.22",
67
- "@shrkcrft/structural-search": "^0.1.0-alpha.22",
68
- "@shrkcrft/impact-engine": "^0.1.0-alpha.22",
69
- "@shrkcrft/context-planner": "^0.1.0-alpha.22",
70
- "@shrkcrft/architecture-guard": "^0.1.0-alpha.22",
71
- "@shrkcrft/framework-scanners": "^0.1.0-alpha.22",
72
- "@shrkcrft/api-surface-diff": "^0.1.0-alpha.22",
73
- "@shrkcrft/quality-gates": "^0.1.0-alpha.22",
74
- "@shrkcrft/migrate": "^0.1.0-alpha.22",
75
- "@shrkcrft/generator": "^0.1.0-alpha.22",
76
- "@shrkcrft/importer": "^0.1.0-alpha.22",
77
- "@shrkcrft/inspector": "^0.1.0-alpha.22",
78
- "@shrkcrft/ai": "^0.1.0-alpha.22",
79
- "@shrkcrft/embeddings": "^0.1.0-alpha.22",
80
- "@shrkcrft/shared": "^0.1.0-alpha.22",
81
- "@shrkcrft/mcp-server": "^0.1.0-alpha.22",
50
+ "@shrkcrft/core": "^0.1.0-alpha.24",
51
+ "@shrkcrft/compress": "^0.1.0-alpha.24",
52
+ "@shrkcrft/config": "^0.1.0-alpha.24",
53
+ "@shrkcrft/workspace": "^0.1.0-alpha.24",
54
+ "@shrkcrft/knowledge": "^0.1.0-alpha.24",
55
+ "@shrkcrft/context": "^0.1.0-alpha.24",
56
+ "@shrkcrft/rules": "^0.1.0-alpha.24",
57
+ "@shrkcrft/paths": "^0.1.0-alpha.24",
58
+ "@shrkcrft/templates": "^0.1.0-alpha.24",
59
+ "@shrkcrft/plugin-api": "^0.1.0-alpha.24",
60
+ "@shrkcrft/dashboard": "^0.1.0-alpha.24",
61
+ "@shrkcrft/dashboard-api": "^0.1.0-alpha.24",
62
+ "@shrkcrft/pipelines": "^0.1.0-alpha.24",
63
+ "@shrkcrft/presets": "^0.1.0-alpha.24",
64
+ "@shrkcrft/boundaries": "^0.1.0-alpha.24",
65
+ "@shrkcrft/graph": "^0.1.0-alpha.24",
66
+ "@shrkcrft/rule-graph": "^0.1.0-alpha.24",
67
+ "@shrkcrft/structural-search": "^0.1.0-alpha.24",
68
+ "@shrkcrft/impact-engine": "^0.1.0-alpha.24",
69
+ "@shrkcrft/context-planner": "^0.1.0-alpha.24",
70
+ "@shrkcrft/architecture-guard": "^0.1.0-alpha.24",
71
+ "@shrkcrft/framework-scanners": "^0.1.0-alpha.24",
72
+ "@shrkcrft/api-surface-diff": "^0.1.0-alpha.24",
73
+ "@shrkcrft/quality-gates": "^0.1.0-alpha.24",
74
+ "@shrkcrft/migrate": "^0.1.0-alpha.24",
75
+ "@shrkcrft/generator": "^0.1.0-alpha.24",
76
+ "@shrkcrft/importer": "^0.1.0-alpha.24",
77
+ "@shrkcrft/inspector": "^0.1.0-alpha.24",
78
+ "@shrkcrft/ai": "^0.1.0-alpha.24",
79
+ "@shrkcrft/embeddings": "^0.1.0-alpha.24",
80
+ "@shrkcrft/shared": "^0.1.0-alpha.24",
81
+ "@shrkcrft/mcp-server": "^0.1.0-alpha.24",
82
82
  "@huggingface/transformers": "^3.7.5"
83
83
  },
84
84
  "publishConfig": {