@shrkcrft/cli 0.1.0-alpha.21 → 0.1.0-alpha.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/command-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 +119 -3
- package/dist/commands/command-catalog.d.ts.map +1 -1
- package/dist/commands/command-catalog.js +224 -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/constructs.command.d.ts.map +1 -1
- package/dist/commands/constructs.command.js +40 -9
- 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/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 +99 -2
- package/dist/commands/graph-code-subverbs.d.ts.map +1 -1
- package/dist/commands/graph-code-subverbs.js +183 -68
- package/dist/commands/graph.command.d.ts.map +1 -1
- package/dist/commands/graph.command.js +6 -0
- package/dist/commands/impact.command.d.ts.map +1 -1
- package/dist/commands/impact.command.js +91 -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 +3 -0
- package/dist/commands/policy-lint.command.d.ts.map +1 -0
- package/dist/commands/policy-lint.command.js +135 -0
- 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 +3 -0
- package/dist/commands/reuse.command.d.ts.map +1 -0
- package/dist/commands/reuse.command.js +202 -0
- 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 +87 -4
- 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 +14 -1
- 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,135 @@
|
|
|
1
|
+
import * as nodePath from 'node:path';
|
|
2
|
+
import { runPolicyLint } from '@shrkcrft/boundaries';
|
|
3
|
+
import { resolveChangedFiles, resolveProjectConfig } from '@shrkcrft/inspector';
|
|
4
|
+
import { flagBool, flagString, resolveCwd, } from "../command-registry.js";
|
|
5
|
+
import { asJson, header, kv } from "../output/format-output.js";
|
|
6
|
+
const VALID_SURFACES = new Set(['template', 'style', 'ts']);
|
|
7
|
+
export const policyLintCommand = {
|
|
8
|
+
name: 'policy-lint',
|
|
9
|
+
description: 'Lint template/markup, stylesheet, and AOT-invisible TS surfaces against data-defined policyRules[] (e.g. flag raw markup when a primitive exists). Sees `.html` files AND inline `template:` strings — surfaces tsc/AOT cannot. Deterministic; no AI.',
|
|
10
|
+
usage: 'shrk [--cwd <dir>] policy-lint [--surface template|style|ts] [--changed-only] [--since <ref>] [--only <ids>] [--json]',
|
|
11
|
+
async run(args) {
|
|
12
|
+
const cwd = resolveCwd(args);
|
|
13
|
+
const wantJson = flagBool(args, 'json');
|
|
14
|
+
const changedOnly = flagBool(args, 'changed-only');
|
|
15
|
+
const since = flagString(args, 'since');
|
|
16
|
+
const only = flagString(args, 'only');
|
|
17
|
+
const surfaceRaw = flagString(args, 'surface');
|
|
18
|
+
let surfaces;
|
|
19
|
+
if (surfaceRaw) {
|
|
20
|
+
const parts = surfaceRaw.split(',').map((s) => s.trim()).filter(Boolean);
|
|
21
|
+
const bad = parts.filter((s) => !VALID_SURFACES.has(s));
|
|
22
|
+
if (bad.length > 0) {
|
|
23
|
+
process.stderr.write(`Unknown --surface "${bad.join(', ')}". Use template | style | ts.\n`);
|
|
24
|
+
return 2;
|
|
25
|
+
}
|
|
26
|
+
surfaces = parts;
|
|
27
|
+
}
|
|
28
|
+
// Distinguish invalid config from valid-with-no-rules (no silent fail-open).
|
|
29
|
+
const loaded = await resolveProjectConfig(cwd);
|
|
30
|
+
if (!loaded.ok) {
|
|
31
|
+
const msg = loaded.error.message;
|
|
32
|
+
if (wantJson) {
|
|
33
|
+
process.stdout.write(asJson({ schema: 'sharkcraft.policy-lint/v1', error: msg, rules: [], findings: [], diagnostics: [msg], evaluated: 0, verdict: 'errors' }) + '\n');
|
|
34
|
+
return 1;
|
|
35
|
+
}
|
|
36
|
+
process.stdout.write(header('Policy lint'));
|
|
37
|
+
process.stdout.write(` ✗ Could not load config: ${msg}\n Run \`shrk doctor\` for details.\n`);
|
|
38
|
+
return 1;
|
|
39
|
+
}
|
|
40
|
+
const rules = loaded.value.config.policyRules ?? [];
|
|
41
|
+
const planeDiagnostics = loaded.value.planeDiagnostics;
|
|
42
|
+
if (rules.length === 0) {
|
|
43
|
+
if (wantJson) {
|
|
44
|
+
process.stdout.write(asJson({ schema: 'sharkcraft.policy-lint/v1', rules: [], findings: [], diagnostics: [], evaluated: 0, verdict: 'pass' }) + '\n');
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
process.stdout.write(header('Policy lint'));
|
|
48
|
+
process.stdout.write(' No policy rules configured. Declare `policyRules[]` in sharkcraft.config.ts to lint\n' +
|
|
49
|
+
' templates / styles / AOT-invisible TS shapes (see docs/policy-lint.md).\n');
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
52
|
+
// A typo'd --only id must not silently select nothing and report green.
|
|
53
|
+
if (only) {
|
|
54
|
+
const requested = only.split(',').map((s) => s.trim()).filter(Boolean);
|
|
55
|
+
const known = new Set(rules.map((r) => r.id));
|
|
56
|
+
const unknown = requested.filter((id) => !known.has(id));
|
|
57
|
+
if (unknown.length > 0) {
|
|
58
|
+
process.stderr.write(`Unknown --only rule id(s): ${unknown.join(', ')}. Configured: ${[...known].join(', ') || '(none)'}\n`);
|
|
59
|
+
return 2;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
let changedFiles;
|
|
63
|
+
if (changedOnly || since) {
|
|
64
|
+
changedFiles = resolveChangedFiles({
|
|
65
|
+
projectRoot: cwd,
|
|
66
|
+
...(since ? { since } : {}),
|
|
67
|
+
...(changedOnly && !since ? { includeWorktree: true } : {}),
|
|
68
|
+
}).files;
|
|
69
|
+
}
|
|
70
|
+
// Don't lint SharkCraft's own asset/config dir by default (its .ts files
|
|
71
|
+
// hold the rule definitions themselves, which can self-match).
|
|
72
|
+
const sharkcraftRel = nodePath.relative(cwd, loaded.value.sharkcraftDir).split(nodePath.sep).join('/');
|
|
73
|
+
const excludeDirs = sharkcraftRel && !sharkcraftRel.startsWith('..') ? [sharkcraftRel] : [];
|
|
74
|
+
const reportRaw = runPolicyLint(cwd, rules, {
|
|
75
|
+
...(surfaces ? { surfaces } : {}),
|
|
76
|
+
...(changedOnly || since ? { changedOnly: true, changedFiles: changedFiles ?? [] } : {}),
|
|
77
|
+
...(only ? { only: only.split(',').map((s) => s.trim()).filter(Boolean) } : {}),
|
|
78
|
+
...(excludeDirs.length > 0 ? { excludeDirs } : {}),
|
|
79
|
+
});
|
|
80
|
+
// Surface pack-plane merge notes (missing/invalid pack policy files, dropped
|
|
81
|
+
// collisions) in the same diagnostics array the engine already emits.
|
|
82
|
+
const report = planeDiagnostics.length > 0
|
|
83
|
+
? { ...reportRaw, diagnostics: [...reportRaw.diagnostics, ...planeDiagnostics] }
|
|
84
|
+
: reportRaw;
|
|
85
|
+
if (wantJson) {
|
|
86
|
+
process.stdout.write(asJson(report) + '\n');
|
|
87
|
+
return report.verdict === 'errors' ? 1 : 0;
|
|
88
|
+
}
|
|
89
|
+
process.stdout.write(header('Policy lint'));
|
|
90
|
+
// `evaluated` counts rules that actually scanned ≥1 file. When 0 rules
|
|
91
|
+
// evaluated but rules ARE configured, say so loudly — "scanned nothing" must
|
|
92
|
+
// never read as the green "no policy violations" pass.
|
|
93
|
+
if (report.evaluated === 0) {
|
|
94
|
+
process.stdout.write(` ! Nothing evaluated — ${report.rules.length} rule(s) configured but none matched files in scope` +
|
|
95
|
+
(changedOnly || since ? ' (changed-only).\n' : '.\n'));
|
|
96
|
+
return 0;
|
|
97
|
+
}
|
|
98
|
+
process.stdout.write(kv('rules evaluated', `${report.evaluated} of ${report.rules.length}`) + '\n');
|
|
99
|
+
const errors = report.findings.filter((f) => f.severity === 'error').length;
|
|
100
|
+
const warnings = report.findings.filter((f) => f.severity === 'warning').length;
|
|
101
|
+
process.stdout.write(kv('findings', `${errors} error(s), ${warnings} warning(s)`) + '\n');
|
|
102
|
+
if (report.diagnostics.length > 0) {
|
|
103
|
+
process.stdout.write('\nMisconfigured rules:\n');
|
|
104
|
+
for (const d of report.diagnostics)
|
|
105
|
+
process.stdout.write(` ! ${d}\n`);
|
|
106
|
+
}
|
|
107
|
+
if (report.findings.length === 0 && report.diagnostics.length === 0) {
|
|
108
|
+
process.stdout.write('\nNo policy violations on the scanned surfaces. ✓\n');
|
|
109
|
+
return 0;
|
|
110
|
+
}
|
|
111
|
+
// Group findings by rule.
|
|
112
|
+
const byRule = new Map();
|
|
113
|
+
for (const f of report.findings) {
|
|
114
|
+
const arr = byRule.get(f.ruleId) ?? [];
|
|
115
|
+
arr.push(f);
|
|
116
|
+
byRule.set(f.ruleId, arr);
|
|
117
|
+
}
|
|
118
|
+
for (const r of report.rules) {
|
|
119
|
+
const fs = byRule.get(r.ruleId);
|
|
120
|
+
if (!fs || fs.length === 0)
|
|
121
|
+
continue;
|
|
122
|
+
process.stdout.write(`\n[${r.severity}] ${r.ruleId} (${r.surface}) — ${fs[0].message}\n`);
|
|
123
|
+
for (const f of fs.slice(0, 50)) {
|
|
124
|
+
const tag = f.inlineTemplate ? ' [inline template]' : '';
|
|
125
|
+
process.stdout.write(` • ${f.match} (${f.file}:${f.line})${tag}\n`);
|
|
126
|
+
}
|
|
127
|
+
if (fs.length > 50)
|
|
128
|
+
process.stdout.write(` … (${fs.length - 50} more)\n`);
|
|
129
|
+
const suggest = fs.find((f) => f.suggest)?.suggest;
|
|
130
|
+
if (suggest)
|
|
131
|
+
process.stdout.write(` → ${suggest}\n`);
|
|
132
|
+
}
|
|
133
|
+
return report.verdict === 'errors' ? 1 : 0;
|
|
134
|
+
},
|
|
135
|
+
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"recommend.command.d.ts","sourceRoot":"","sources":["../../src/commands/recommend.command.ts"],"names":[],"mappings":"AAUA,OAAO,EAKL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAShC,eAAO,MAAM,gBAAgB,EAAE,
|
|
1
|
+
{"version":3,"file":"recommend.command.d.ts","sourceRoot":"","sources":["../../src/commands/recommend.command.ts"],"names":[],"mappings":"AAUA,OAAO,EAKL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAShC,eAAO,MAAM,gBAAgB,EAAE,eA4P9B,CAAC;AA0HF;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAY3D;AAcD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAcxD"}
|
|
@@ -114,10 +114,18 @@ export const recommendCommand = {
|
|
|
114
114
|
}));
|
|
115
115
|
const promotedSet = new Set(promoted.map((p) => p.command));
|
|
116
116
|
const isGenericDefault = (c) => /^(bun run )?shrk (review|report|impact)\b/i.test(c);
|
|
117
|
+
// The recommender (inspector) already arbitrates the keyword recipe
|
|
118
|
+
// against the shared ranker and may have promoted a `shrk gen <template>`
|
|
119
|
+
// / `shrk task` headline (its `why` starts with "Ranker matched"). That
|
|
120
|
+
// ranker-confirmed scaffold is the engine's corrected route — keep it
|
|
121
|
+
// ahead of even the routing-hint promotion so the headline survives.
|
|
122
|
+
const isRankerPromoted = (r) => /^Ranker matched /.test(r.why);
|
|
117
123
|
const existing = report.recommendations;
|
|
118
|
-
const
|
|
119
|
-
const
|
|
124
|
+
const keptRankerPromoted = existing.filter((r) => !promotedSet.has(r.command) && isRankerPromoted(r));
|
|
125
|
+
const keptNonGeneric = existing.filter((r) => !promotedSet.has(r.command) && !isRankerPromoted(r) && !isGenericDefault(r.command));
|
|
126
|
+
const keptGeneric = existing.filter((r) => !promotedSet.has(r.command) && !isRankerPromoted(r) && isGenericDefault(r.command));
|
|
120
127
|
report.recommendations = [
|
|
128
|
+
...keptRankerPromoted,
|
|
121
129
|
...promoted,
|
|
122
130
|
...keptNonGeneric,
|
|
123
131
|
...keptGeneric,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"registry.command.d.ts","sourceRoot":"","sources":["../../src/commands/registry.command.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"registry.command.d.ts","sourceRoot":"","sources":["../../src/commands/registry.command.ts"],"names":[],"mappings":"AAwBA,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAGhC,eAAO,MAAM,wBAAwB,EAAE,eAetC,CAAC;AAsGF,eAAO,MAAM,eAAe,EAAE,eAuB7B,CAAC"}
|
|
@@ -1,18 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `shrk registry` commands.
|
|
3
3
|
*
|
|
4
|
-
* shrk registry lifecycle [--json]
|
|
4
|
+
* shrk registry lifecycle [--json] # register/remove symmetry
|
|
5
|
+
* shrk registry <name> list [--json] # every declared id
|
|
6
|
+
* shrk registry <name> exists <id> [--json] # is the id taken? (exit 1 if not)
|
|
7
|
+
* shrk registry <name> where <id> [--json] # declaration (+ consumer) sites
|
|
8
|
+
*
|
|
9
|
+
* `<name>` resolves a `registries[]` declaration in sharkcraft.config.ts — one
|
|
10
|
+
* deterministic multi-root scan that answers "is this id taken / where is it"
|
|
11
|
+
* without an agent re-running a fragile grep.
|
|
5
12
|
*/
|
|
6
|
-
import { buildRegistryLifecycleReport, renderRegistryLifecycleReportText, } from '@shrkcrft/inspector';
|
|
7
|
-
import {
|
|
13
|
+
import { buildRegistryLifecycleReport, renderRegistryLifecycleReportText, resolveProjectConfig, } from '@shrkcrft/inspector';
|
|
14
|
+
import { scanRegistry, registryExists, registryWhere, } from '@shrkcrft/boundaries';
|
|
15
|
+
import { flagBool, flagString, resolveCwd, } from "../command-registry.js";
|
|
8
16
|
import { asJson } from "../output/format-output.js";
|
|
9
17
|
export const registryLifecycleCommand = {
|
|
10
18
|
name: 'lifecycle',
|
|
11
|
-
description: 'Scan the workspace for register
|
|
12
|
-
usage: 'shrk registry lifecycle [--json]',
|
|
19
|
+
description: 'Scan the workspace for register/remove symmetry. Read-only.',
|
|
20
|
+
usage: 'shrk registry lifecycle [--scope <dir>] [--json]',
|
|
13
21
|
async run(args) {
|
|
14
22
|
const cwd = resolveCwd(args);
|
|
15
|
-
const
|
|
23
|
+
const scope = flagString(args, 'scope');
|
|
24
|
+
const report = buildRegistryLifecycleReport({ projectRoot: cwd, ...(scope ? { scope } : {}) });
|
|
16
25
|
if (flagBool(args, 'json')) {
|
|
17
26
|
process.stdout.write(asJson(report) + '\n');
|
|
18
27
|
return report.missingRemovers.length === 0 ? 0 : 1;
|
|
@@ -21,17 +30,116 @@ export const registryLifecycleCommand = {
|
|
|
21
30
|
return report.missingRemovers.length === 0 ? 0 : 1;
|
|
22
31
|
},
|
|
23
32
|
};
|
|
33
|
+
async function loadRegistries(cwd) {
|
|
34
|
+
const loaded = await resolveProjectConfig(cwd);
|
|
35
|
+
if (!loaded.ok)
|
|
36
|
+
return { ok: false, message: loaded.error.message };
|
|
37
|
+
return {
|
|
38
|
+
ok: true,
|
|
39
|
+
registries: loaded.value.config.registries ?? [],
|
|
40
|
+
planeDiagnostics: loaded.value.planeDiagnostics,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function findRegistry(registries, name) {
|
|
44
|
+
return registries.find((r) => r.name === name);
|
|
45
|
+
}
|
|
46
|
+
/** Run a `list | exists | where` query against a declared registry. */
|
|
47
|
+
async function runRegistryInventory(args, name) {
|
|
48
|
+
const cwd = resolveCwd(args);
|
|
49
|
+
const json = flagBool(args, 'json');
|
|
50
|
+
const action = args.positional[1];
|
|
51
|
+
const id = args.positional[2];
|
|
52
|
+
const loaded = await loadRegistries(cwd);
|
|
53
|
+
if (!loaded.ok) {
|
|
54
|
+
if (json)
|
|
55
|
+
process.stdout.write(asJson({ error: loaded.message }) + '\n');
|
|
56
|
+
else
|
|
57
|
+
process.stderr.write(`Could not load config: ${loaded.message}\n Run \`shrk doctor\` for details.\n`);
|
|
58
|
+
return 1;
|
|
59
|
+
}
|
|
60
|
+
const decl = findRegistry(loaded.registries, name);
|
|
61
|
+
if (!decl) {
|
|
62
|
+
const names = loaded.registries.map((r) => r.name);
|
|
63
|
+
const avail = names.length > 0 ? `Declared registries: ${names.join(', ')}.` : 'No registries declared in sharkcraft.config.ts `registries[]`.';
|
|
64
|
+
if (json)
|
|
65
|
+
process.stdout.write(asJson({ error: `unknown registry "${name}"`, available: names }) + '\n');
|
|
66
|
+
else
|
|
67
|
+
process.stderr.write(`No registry named "${name}". ${avail}\n`);
|
|
68
|
+
return 2;
|
|
69
|
+
}
|
|
70
|
+
const inventory = scanRegistry(cwd, decl);
|
|
71
|
+
if (action === 'list' || action === undefined) {
|
|
72
|
+
// Fold pack-plane merge notes (missing/invalid pack registry files, dropped
|
|
73
|
+
// collisions) into the inventory's own scan diagnostics.
|
|
74
|
+
const diagnostics = [...inventory.diagnostics, ...loaded.planeDiagnostics];
|
|
75
|
+
if (json) {
|
|
76
|
+
process.stdout.write(asJson({ name: inventory.name, count: inventory.entries.length, ids: inventory.entries.map((e) => e.id), diagnostics }) + '\n');
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
process.stdout.write(`Registry "${inventory.name}"${inventory.description ? ' — ' + inventory.description : ''}\n`);
|
|
80
|
+
process.stdout.write(` ${inventory.entries.length} id(s)\n`);
|
|
81
|
+
for (const e of inventory.entries) {
|
|
82
|
+
process.stdout.write(` • ${e.id} (${e.sites.length} site${e.sites.length === 1 ? '' : 's'})\n`);
|
|
83
|
+
}
|
|
84
|
+
for (const d of diagnostics)
|
|
85
|
+
process.stdout.write(` ! ${d}\n`);
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
88
|
+
if (action === 'exists') {
|
|
89
|
+
if (!id) {
|
|
90
|
+
process.stderr.write(`Usage: shrk registry ${name} exists <id>\n`);
|
|
91
|
+
return 2;
|
|
92
|
+
}
|
|
93
|
+
const exists = registryExists(inventory, id);
|
|
94
|
+
if (json)
|
|
95
|
+
process.stdout.write(asJson({ name: inventory.name, id, exists }) + '\n');
|
|
96
|
+
else
|
|
97
|
+
process.stdout.write(`${exists ? 'yes' : 'no'} — "${id}" is ${exists ? 'declared' : 'NOT declared'} in registry "${inventory.name}".\n`);
|
|
98
|
+
return exists ? 0 : 1;
|
|
99
|
+
}
|
|
100
|
+
if (action === 'where') {
|
|
101
|
+
if (!id) {
|
|
102
|
+
process.stderr.write(`Usage: shrk registry ${name} where <id>\n`);
|
|
103
|
+
return 2;
|
|
104
|
+
}
|
|
105
|
+
const entry = registryWhere(inventory, id);
|
|
106
|
+
if (json) {
|
|
107
|
+
process.stdout.write(asJson({ name: inventory.name, id, found: entry !== undefined, entry: entry ?? null }) + '\n');
|
|
108
|
+
return entry ? 0 : 1;
|
|
109
|
+
}
|
|
110
|
+
if (!entry) {
|
|
111
|
+
process.stdout.write(`"${id}" is not declared in registry "${inventory.name}".\n`);
|
|
112
|
+
return 1;
|
|
113
|
+
}
|
|
114
|
+
process.stdout.write(`"${id}" in registry "${inventory.name}":\n`);
|
|
115
|
+
for (const s of entry.sites)
|
|
116
|
+
process.stdout.write(` declared ${s.file}:${s.line}\n`);
|
|
117
|
+
for (const s of entry.consumerSites ?? [])
|
|
118
|
+
process.stdout.write(` consumed ${s.file}:${s.line}\n`);
|
|
119
|
+
return 0;
|
|
120
|
+
}
|
|
121
|
+
process.stderr.write(`Unknown action "${action}". Usage: shrk registry ${name} list | exists <id> | where <id>\n`);
|
|
122
|
+
return 2;
|
|
123
|
+
}
|
|
24
124
|
export const registryCommand = {
|
|
25
125
|
name: 'registry',
|
|
26
|
-
description: 'Registry inspections
|
|
27
|
-
usage: 'shrk registry lifecycle',
|
|
126
|
+
description: 'Registry inspections: lifecycle symmetry + declared-registry inventory. Read-only.',
|
|
127
|
+
usage: 'shrk registry lifecycle | <name> list | <name> exists <id> | <name> where <id>',
|
|
28
128
|
async run(args) {
|
|
29
129
|
const sub = args.positional[0];
|
|
30
130
|
if (sub === 'lifecycle') {
|
|
31
131
|
args.positional = args.positional.slice(1);
|
|
32
132
|
return registryLifecycleCommand.run(args);
|
|
33
133
|
}
|
|
34
|
-
|
|
134
|
+
if (sub !== undefined && sub.length > 0) {
|
|
135
|
+
// `<name> list | exists <id> | where <id>` — sub is the registry name.
|
|
136
|
+
return runRegistryInventory(args, sub);
|
|
137
|
+
}
|
|
138
|
+
const cwd = resolveCwd(args);
|
|
139
|
+
const loaded = await loadRegistries(cwd);
|
|
140
|
+
const names = loaded.ok ? loaded.registries.map((r) => r.name) : [];
|
|
141
|
+
process.stderr.write('Usage: shrk registry lifecycle | <name> list | <name> exists <id> | <name> where <id>\n' +
|
|
142
|
+
(names.length > 0 ? `Declared registries: ${names.join(', ')}.\n` : 'No registries declared (sharkcraft.config.ts `registries[]`).\n'));
|
|
35
143
|
return 2;
|
|
36
144
|
},
|
|
37
145
|
};
|
|
@@ -8,7 +8,7 @@ const VALID_FORMATS = new Set(['text', 'markdown', 'html', 'json']);
|
|
|
8
8
|
export const reportCommand = {
|
|
9
9
|
name: 'report',
|
|
10
10
|
description: 'Render runtime reports in text / markdown / html / json. Subcommands: adoption / session / quality / safety / review / coverage / drift / graph.',
|
|
11
|
-
usage: 'shrk report <adoption|session <id>|quality|safety|review <packet.json>|coverage|drift|graph> [--format text|markdown|html|json] [--output <path>] [--collapse-long-sections] [--max-items N] [--
|
|
11
|
+
usage: 'shrk report <adoption|session <id>|quality|safety|review <packet.json>|coverage|drift|graph> [--format text|markdown|html|json] [--output <path>] [--collapse-long-sections] [--max-items N] [--json]',
|
|
12
12
|
async run(args) {
|
|
13
13
|
const sub = args.positional[0];
|
|
14
14
|
const sliced = { ...args, positional: args.positional.slice(1) };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reuse.command.d.ts","sourceRoot":"","sources":["../../src/commands/reuse.command.ts"],"names":[],"mappings":"AAGA,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AA4DhC,eAAO,MAAM,YAAY,EAAE,eAqK1B,CAAC"}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { GraphStore, GraphQueryApi } from '@shrkcrft/graph';
|
|
2
|
+
import { resolveProjectConfig } from '@shrkcrft/inspector';
|
|
3
|
+
import { flagBool, flagNumber, resolveCwd, } from "../command-registry.js";
|
|
4
|
+
import { asJson, header } from "../output/format-output.js";
|
|
5
|
+
const STOP = new Set([
|
|
6
|
+
'the', 'a', 'an', 'to', 'for', 'of', 'and', 'or', 'with', 'in', 'on', 'into', 'my',
|
|
7
|
+
'add', 'use', 'using', 'create', 'make', 'new', 'build', 'want', 'need', 'how', 'do',
|
|
8
|
+
]);
|
|
9
|
+
function tokenize(s) {
|
|
10
|
+
// Min length 3: 2-char tokens (`ui`, `id`) substring-match unrelated text
|
|
11
|
+
// (`guidance`, `valid`) and produce false-positive primitive matches.
|
|
12
|
+
return [
|
|
13
|
+
...new Set(s
|
|
14
|
+
.toLowerCase()
|
|
15
|
+
.replace(/[^a-z0-9]+/g, ' ')
|
|
16
|
+
.split(' ')
|
|
17
|
+
.filter((t) => t.length >= 3 && !STOP.has(t))),
|
|
18
|
+
];
|
|
19
|
+
}
|
|
20
|
+
function scorePrimitive(p, tokens) {
|
|
21
|
+
// Substring match (so intent "debounce" matches symbol `useDebounce`); the
|
|
22
|
+
// 3-char minimum above keeps it from over-matching on tiny fragments.
|
|
23
|
+
const hay = [p.symbol, ...(p.roles ?? []), ...(p.keywords ?? []), p.description ?? '']
|
|
24
|
+
.join(' ')
|
|
25
|
+
.toLowerCase();
|
|
26
|
+
let s = 0;
|
|
27
|
+
for (const t of tokens)
|
|
28
|
+
if (hay.includes(t))
|
|
29
|
+
s += 1;
|
|
30
|
+
// A role the intent mentions is a strong signal; re-weight role hits.
|
|
31
|
+
for (const role of p.roles ?? []) {
|
|
32
|
+
const rl = role.toLowerCase();
|
|
33
|
+
if (rl.length >= 3 && tokens.some((t) => rl.includes(t)))
|
|
34
|
+
s += 0.5;
|
|
35
|
+
}
|
|
36
|
+
return s;
|
|
37
|
+
}
|
|
38
|
+
const INDEX_RE = /(^|\/)index\.[cm]?[jt]sx?$/;
|
|
39
|
+
export const reuseCommand = {
|
|
40
|
+
name: 'reuse',
|
|
41
|
+
description: 'Intent → the canonical primitive to reuse. Matches your intent against configured reusePrimitives[], then resolves the symbol in the code graph to its declaration, public import path, sibling exports, and real consumer files to copy. Deterministic; no AI.',
|
|
42
|
+
usage: 'shrk reuse "<what I want to build>" [--limit N] [--json]',
|
|
43
|
+
async run(args) {
|
|
44
|
+
const cwd = resolveCwd(args);
|
|
45
|
+
const wantJson = flagBool(args, 'json');
|
|
46
|
+
const limit = flagNumber(args, 'limit') ?? 3;
|
|
47
|
+
const intent = args.positional.join(' ').trim();
|
|
48
|
+
if (!intent) {
|
|
49
|
+
process.stderr.write('Usage: shrk reuse "<what I want to build>" [--limit N] [--json]\n');
|
|
50
|
+
return 2;
|
|
51
|
+
}
|
|
52
|
+
const loaded = await resolveProjectConfig(cwd);
|
|
53
|
+
if (!loaded.ok) {
|
|
54
|
+
const msg = loaded.error.message;
|
|
55
|
+
if (wantJson) {
|
|
56
|
+
process.stdout.write(asJson({ schema: 'sharkcraft.reuse/v1', intent, error: msg, results: [] }) + '\n');
|
|
57
|
+
return 1;
|
|
58
|
+
}
|
|
59
|
+
process.stdout.write(header(`Reuse: "${intent}"`));
|
|
60
|
+
process.stdout.write(` ✗ Could not load config: ${msg}\n Run \`shrk doctor\` for details.\n`);
|
|
61
|
+
return 1;
|
|
62
|
+
}
|
|
63
|
+
const primitives = loaded.value.config.reusePrimitives ?? [];
|
|
64
|
+
const planeDiagnostics = loaded.value.planeDiagnostics;
|
|
65
|
+
// reuse has no pre-existing diagnostics surface; expose pack-plane merge
|
|
66
|
+
// notes (missing/invalid pack primitive files, dropped collisions) so a
|
|
67
|
+
// pack contribution that failed to load isn't silently invisible.
|
|
68
|
+
const planeJson = planeDiagnostics.length > 0 ? { planeDiagnostics } : {};
|
|
69
|
+
const writePlaneNotes = () => {
|
|
70
|
+
for (const d of planeDiagnostics)
|
|
71
|
+
process.stdout.write(` ! ${d}\n`);
|
|
72
|
+
};
|
|
73
|
+
if (primitives.length === 0) {
|
|
74
|
+
if (wantJson) {
|
|
75
|
+
process.stdout.write(asJson({ schema: 'sharkcraft.reuse/v1', intent, results: [], ...planeJson }) + '\n');
|
|
76
|
+
return 0;
|
|
77
|
+
}
|
|
78
|
+
process.stdout.write(header(`Reuse: "${intent}"`));
|
|
79
|
+
process.stdout.write(' No reuse primitives configured. Declare `reusePrimitives[]` in sharkcraft.config.ts\n' +
|
|
80
|
+
' to map roles/intents to canonical symbols (see docs/reuse.md).\n');
|
|
81
|
+
writePlaneNotes();
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
84
|
+
const tokens = tokenize(intent);
|
|
85
|
+
const ranked = primitives
|
|
86
|
+
.map((p) => ({ p, score: scorePrimitive(p, tokens) }))
|
|
87
|
+
.filter((x) => x.score > 0)
|
|
88
|
+
.sort((a, b) => b.score - a.score || a.p.symbol.localeCompare(b.p.symbol))
|
|
89
|
+
.slice(0, Math.max(1, limit));
|
|
90
|
+
const store = new GraphStore(cwd);
|
|
91
|
+
const api = store.exists() ? GraphQueryApi.fromStore(cwd) : null;
|
|
92
|
+
if (ranked.length === 0) {
|
|
93
|
+
const roles = [...new Set(primitives.flatMap((p) => p.roles))].sort();
|
|
94
|
+
if (wantJson) {
|
|
95
|
+
process.stdout.write(asJson({ schema: 'sharkcraft.reuse/v1', intent, results: [], availableRoles: roles, ...planeJson }) + '\n');
|
|
96
|
+
return 0;
|
|
97
|
+
}
|
|
98
|
+
process.stdout.write(header(`Reuse: "${intent}"`));
|
|
99
|
+
process.stdout.write(' No primitive matched. Available roles:\n');
|
|
100
|
+
for (const r of roles.slice(0, 40))
|
|
101
|
+
process.stdout.write(` • ${r}\n`);
|
|
102
|
+
writePlaneNotes();
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
105
|
+
const results = ranked.map(({ p, score }) => {
|
|
106
|
+
const r = { symbol: p.symbol, score, roles: p.roles, siblings: [], consumers: [] };
|
|
107
|
+
if (p.description)
|
|
108
|
+
r.description = p.description;
|
|
109
|
+
if (p.importPath)
|
|
110
|
+
r.importPath = p.importPath;
|
|
111
|
+
if (api) {
|
|
112
|
+
// Disambiguate same-named declarations deterministically: prefer
|
|
113
|
+
// exported symbols, then shallowest path; disclose the alternates.
|
|
114
|
+
const candidates = api
|
|
115
|
+
.findSymbol(p.symbol, { exact: true })
|
|
116
|
+
.slice()
|
|
117
|
+
.sort((a, b) => (a.path ?? '').localeCompare(b.path ?? ''));
|
|
118
|
+
const exported = candidates.filter((c) => c.data?.['isExported'] === true);
|
|
119
|
+
const pool = exported.length > 0 ? exported : candidates;
|
|
120
|
+
const sym = pool[0];
|
|
121
|
+
if (sym) {
|
|
122
|
+
if (sym.path)
|
|
123
|
+
r.declaredIn = sym.path;
|
|
124
|
+
if (sym.line)
|
|
125
|
+
r.declaredLine = sym.line;
|
|
126
|
+
if (sym.path) {
|
|
127
|
+
const fileNode = api.findFile(sym.path);
|
|
128
|
+
if (fileNode) {
|
|
129
|
+
r.siblings = api
|
|
130
|
+
.symbolsIn(fileNode.id)
|
|
131
|
+
.filter((s) => s.data?.['isExported'] === true && s.label && s.label !== p.symbol)
|
|
132
|
+
.map((s) => s.label)
|
|
133
|
+
.slice(0, 8);
|
|
134
|
+
// When no public importPath is configured, surface a re-exporting
|
|
135
|
+
// barrel as a hint (we never fabricate a module specifier from a
|
|
136
|
+
// deep file path — that would be a broken/unusable import).
|
|
137
|
+
if (!r.importPath) {
|
|
138
|
+
const barrel = api.importersOf(fileNode.id).find((n) => n.path && INDEX_RE.test(n.path));
|
|
139
|
+
if (barrel?.path)
|
|
140
|
+
r.reExportedVia = barrel.path;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
r.consumers = api
|
|
145
|
+
.referenceSitesOf(sym.id)
|
|
146
|
+
.slice(0, 5)
|
|
147
|
+
.map((s) => ({ path: s.node.path ?? s.node.id, ...(s.line ? { line: s.line } : {}) }));
|
|
148
|
+
const alts = pool.slice(1).map((c) => c.path).filter((x) => !!x);
|
|
149
|
+
if (alts.length > 0)
|
|
150
|
+
r.alternates = alts;
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
r.notFound = true;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// The import line is emitted ONLY from a configured importPath (a clean,
|
|
157
|
+
// copy-pasteable specifier). Without it we show declaration + barrel hint.
|
|
158
|
+
if (r.importPath)
|
|
159
|
+
r.importLine = `import { ${p.symbol} } from '${r.importPath}';`;
|
|
160
|
+
return r;
|
|
161
|
+
});
|
|
162
|
+
if (wantJson) {
|
|
163
|
+
process.stdout.write(asJson({ schema: 'sharkcraft.reuse/v1', intent, graphIndexed: !!api, results, ...planeJson }) + '\n');
|
|
164
|
+
return 0;
|
|
165
|
+
}
|
|
166
|
+
process.stdout.write(header(`Reuse: "${intent}"`));
|
|
167
|
+
writePlaneNotes();
|
|
168
|
+
if (!api) {
|
|
169
|
+
process.stdout.write(' (code graph missing — import path/siblings/consumers limited; run `shrk graph index`)\n');
|
|
170
|
+
}
|
|
171
|
+
let i = 0;
|
|
172
|
+
for (const r of results) {
|
|
173
|
+
i += 1;
|
|
174
|
+
process.stdout.write(`\n${i}. ${r.symbol}\n`);
|
|
175
|
+
if (r.description)
|
|
176
|
+
process.stdout.write(` ${r.description}\n`);
|
|
177
|
+
if (r.notFound) {
|
|
178
|
+
process.stdout.write(' ⚠ symbol not found in the code graph — verify reusePrimitives[].symbol (typo/rename?) or run `shrk graph index`\n');
|
|
179
|
+
}
|
|
180
|
+
if (r.importLine) {
|
|
181
|
+
process.stdout.write(` import: ${r.importLine}\n`);
|
|
182
|
+
}
|
|
183
|
+
else if (r.reExportedVia) {
|
|
184
|
+
process.stdout.write(` re-exported via: ${r.reExportedVia} (set reusePrimitives[].importPath for a copy-paste import)\n`);
|
|
185
|
+
}
|
|
186
|
+
if (r.declaredIn) {
|
|
187
|
+
process.stdout.write(` declared in: ${r.declaredIn}${r.declaredLine ? ':' + r.declaredLine : ''}\n`);
|
|
188
|
+
}
|
|
189
|
+
if (r.alternates && r.alternates.length > 0) {
|
|
190
|
+
process.stdout.write(` ⚠ name also declared in: ${r.alternates.join(', ')}\n`);
|
|
191
|
+
}
|
|
192
|
+
if (r.siblings.length > 0)
|
|
193
|
+
process.stdout.write(` sibling exports: ${r.siblings.join(', ')}\n`);
|
|
194
|
+
if (r.consumers.length > 0) {
|
|
195
|
+
process.stdout.write(' consumers to copy:\n');
|
|
196
|
+
for (const c of r.consumers)
|
|
197
|
+
process.stdout.write(` - ${c.path}${c.line ? ':' + c.line : ''}\n`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return 0;
|
|
201
|
+
},
|
|
202
|
+
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"review.command.d.ts","sourceRoot":"","sources":["../../src/commands/review.command.ts"],"names":[],"mappings":"AAgBA,OAAO,EAML,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AA6GhC,eAAO,MAAM,aAAa,EAAE,
|
|
1
|
+
{"version":3,"file":"review.command.d.ts","sourceRoot":"","sources":["../../src/commands/review.command.ts"],"names":[],"mappings":"AAgBA,OAAO,EAML,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AA6GhC,eAAO,MAAM,aAAa,EAAE,eAiG3B,CAAC"}
|
|
@@ -77,7 +77,20 @@ function scaffoldOptionsFromArgs(args) {
|
|
|
77
77
|
export const reviewCommand = {
|
|
78
78
|
name: 'review',
|
|
79
79
|
description: 'Build a PR-review packet: changed files (via git), affected path conventions, relevant rules/templates/pipelines, boundary violations on those files, missing-test heuristic, verification commands. Also: `shrk review scaffold github-action` prints a workflow template with optional `--with-boundaries / --with-coverage / --with-drift / --artifact-only / --comment-placeholder` flags. `shrk review render-comment <packet.json>` renders a Markdown PR comment from a saved packet.',
|
|
80
|
-
usage: 'shrk [--cwd <dir>] review [--since <ref>] [--staged] [--
|
|
80
|
+
usage: 'shrk [--cwd <dir>] review [files... | --files a,b] [--since <ref>] [--staged] [--no-untracked] [--json] | shrk review scaffold github-action [...] | shrk review render-comment <packet.json> [--output <file>]',
|
|
81
|
+
booleanFlags: new Set([
|
|
82
|
+
'json',
|
|
83
|
+
'staged',
|
|
84
|
+
'untracked',
|
|
85
|
+
'no-untracked',
|
|
86
|
+
'v2',
|
|
87
|
+
'v3',
|
|
88
|
+
'with-boundaries',
|
|
89
|
+
'with-coverage',
|
|
90
|
+
'with-drift',
|
|
91
|
+
'artifact-only',
|
|
92
|
+
'comment-placeholder',
|
|
93
|
+
]),
|
|
81
94
|
async run(args) {
|
|
82
95
|
if (args.positional[0] === 'scaffold') {
|
|
83
96
|
const which = args.positional[1] ?? 'github-action';
|
|
@@ -97,11 +110,16 @@ export const reviewCommand = {
|
|
|
97
110
|
}
|
|
98
111
|
const inspection = await inspectSharkcraft({ cwd: resolveCwd(args) });
|
|
99
112
|
const since = flagString(args, 'since');
|
|
100
|
-
|
|
113
|
+
// Accept files via `--files a,b` OR as bare positional args
|
|
114
|
+
// (`shrk review a.ts b.ts`). Positionals were previously dropped silently,
|
|
115
|
+
// yielding a confident "0 changed files" with exit 0.
|
|
116
|
+
const filesFlag = flagList(args, 'files');
|
|
117
|
+
const files = filesFlag.length ? filesFlag : args.positional;
|
|
101
118
|
const packet = buildReviewPacket(inspection, {
|
|
102
119
|
...(since ? { since } : {}),
|
|
103
120
|
...(flagBool(args, 'staged') ? { staged: true } : {}),
|
|
104
121
|
...(files.length ? { files } : {}),
|
|
122
|
+
...(flagBool(args, 'no-untracked') ? { untracked: false } : {}),
|
|
105
123
|
});
|
|
106
124
|
if (flagBool(args, 'json')) {
|
|
107
125
|
process.stdout.write(asJson(packet) + '\n');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rule-graph-subverbs.d.ts","sourceRoot":"","sources":["../../src/commands/rule-graph-subverbs.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"rule-graph-subverbs.d.ts","sourceRoot":"","sources":["../../src/commands/rule-graph-subverbs.ts"],"names":[],"mappings":"AASA,OAAO,EAAwB,KAAK,eAAe,EAAmB,MAAM,wBAAwB,CAAC;AAGrG,eAAO,MAAM,gBAAgB,EAAE,eAc9B,CAAC"}
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* because the verbs operate on a separate `.sharkcraft/bridge/` store
|
|
6
6
|
* and don't conceptually belong under `shrk graph`.
|
|
7
7
|
*/
|
|
8
|
-
import { buildBridge, RuleGraphQueryApi } from '@shrkcrft/rule-graph';
|
|
8
|
+
import { buildBridge, BridgeStore, RuleGraphQueryApi } from '@shrkcrft/rule-graph';
|
|
9
|
+
import { detectGraphFreshness } from '@shrkcrft/graph';
|
|
9
10
|
import { flagBool, resolveCwd } from "../command-registry.js";
|
|
10
11
|
import { asJson, header, kv } from "../output/format-output.js";
|
|
11
12
|
export const ruleGraphCommand = {
|
|
@@ -54,13 +55,39 @@ async function runStatus(args) {
|
|
|
54
55
|
return 1;
|
|
55
56
|
}
|
|
56
57
|
const api = RuleGraphQueryApi.fromStores(cwd);
|
|
57
|
-
//
|
|
58
|
+
// The bridge maps code-graph files → asset registries, so it is stale once
|
|
59
|
+
// the underlying code graph has drifted. Reuse the already-available graph
|
|
60
|
+
// freshness signal (the same one `graph status` reports) rather than a
|
|
61
|
+
// hardcoded 'fresh'. `lastBuiltAt` comes from the bridge manifest.
|
|
62
|
+
let lastBuiltAt = null;
|
|
63
|
+
try {
|
|
64
|
+
lastBuiltAt = new BridgeStore(cwd).loadSnapshot().manifest.lastBuiltAt;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
lastBuiltAt = null;
|
|
68
|
+
}
|
|
69
|
+
const fresh = detectGraphFreshness(cwd);
|
|
70
|
+
const behind = fresh.modified.length + fresh.added.length + fresh.deleted.length;
|
|
71
|
+
const state = behind > 0 ? 'stale' : 'fresh';
|
|
58
72
|
if (wantJson) {
|
|
59
|
-
process.stdout.write(asJson({
|
|
73
|
+
process.stdout.write(asJson({
|
|
74
|
+
ok: true,
|
|
75
|
+
state,
|
|
76
|
+
lastBuiltAt,
|
|
77
|
+
modifiedSinceIndex: fresh.modified.length,
|
|
78
|
+
newSinceIndex: fresh.added.length,
|
|
79
|
+
deletedSinceIndex: fresh.deleted.length,
|
|
80
|
+
...(state === 'stale' ? { nextCommand: 'shrk rule-graph index' } : {}),
|
|
81
|
+
}) + '\n');
|
|
60
82
|
return 0;
|
|
61
83
|
}
|
|
62
84
|
process.stdout.write(header('Rule-graph status'));
|
|
63
|
-
|
|
85
|
+
if (lastBuiltAt)
|
|
86
|
+
process.stdout.write(kv('last built', lastBuiltAt) + '\n');
|
|
87
|
+
process.stdout.write(kv('state', state) + '\n');
|
|
88
|
+
if (state === 'stale') {
|
|
89
|
+
process.stdout.write(`! stale — ${fresh.modified.length} modified, ${fresh.added.length} new, ${fresh.deleted.length} deleted in the code graph since the bridge was built; re-run \`shrk rule-graph index\`\n`);
|
|
90
|
+
}
|
|
64
91
|
void api;
|
|
65
92
|
return 0;
|
|
66
93
|
}
|