@shrkcrft/cli 0.1.0-alpha.23 → 0.1.0-alpha.25
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/commands/changelog-data.d.ts +25 -0
- package/dist/commands/changelog-data.d.ts.map +1 -0
- package/dist/commands/changelog-data.js +70 -0
- package/dist/commands/changelog.command.d.ts +3 -0
- package/dist/commands/changelog.command.d.ts.map +1 -0
- package/dist/commands/changelog.command.js +100 -0
- package/dist/commands/changes.command.d.ts.map +1 -1
- package/dist/commands/changes.command.js +4 -0
- package/dist/commands/check.command.d.ts.map +1 -1
- package/dist/commands/check.command.js +182 -17
- package/dist/commands/command-catalog.d.ts +11 -0
- package/dist/commands/command-catalog.d.ts.map +1 -1
- package/dist/commands/command-catalog.js +99 -0
- package/dist/commands/compress.command.d.ts.map +1 -1
- package/dist/commands/compress.command.js +15 -1
- package/dist/commands/constructs.command.d.ts.map +1 -1
- package/dist/commands/constructs.command.js +49 -14
- package/dist/commands/context.command.d.ts.map +1 -1
- package/dist/commands/context.command.js +31 -19
- package/dist/commands/finish.command.d.ts +3 -0
- package/dist/commands/finish.command.d.ts.map +1 -0
- package/dist/commands/finish.command.js +70 -0
- package/dist/commands/gate.command.d.ts.map +1 -1
- package/dist/commands/gate.command.js +6 -1
- package/dist/commands/gen.command.d.ts.map +1 -1
- package/dist/commands/gen.command.js +65 -9
- package/dist/commands/graph-code-subverbs.d.ts.map +1 -1
- package/dist/commands/graph-code-subverbs.js +56 -25
- package/dist/commands/help.command.d.ts.map +1 -1
- package/dist/commands/help.command.js +20 -2
- package/dist/commands/impact.command.d.ts.map +1 -1
- package/dist/commands/impact.command.js +17 -22
- package/dist/commands/policy-lint.command.d.ts.map +1 -1
- package/dist/commands/policy-lint.command.js +46 -8
- package/dist/commands/registry.command.d.ts.map +1 -1
- package/dist/commands/registry.command.js +71 -13
- package/dist/commands/reuse.command.d.ts.map +1 -1
- package/dist/commands/reuse.command.js +80 -14
- package/dist/commands/smart-context.command.d.ts.map +1 -1
- package/dist/commands/smart-context.command.js +34 -42
- package/dist/commands/task.command.d.ts.map +1 -1
- package/dist/commands/task.command.js +33 -16
- package/dist/commands/trace.command.d.ts.map +1 -1
- package/dist/commands/trace.command.js +73 -3
- package/dist/commands/wiring.command.d.ts +12 -0
- package/dist/commands/wiring.command.d.ts.map +1 -0
- package/dist/commands/wiring.command.js +384 -0
- package/dist/diff/collect-changed-paths.d.ts +5 -3
- package/dist/diff/collect-changed-paths.d.ts.map +1 -1
- package/dist/diff/collect-changed-paths.js +73 -36
- package/dist/diff/deleted-orphans.d.ts +42 -0
- package/dist/diff/deleted-orphans.d.ts.map +1 -0
- package/dist/diff/deleted-orphans.js +46 -0
- package/dist/finish/run-finish.d.ts +62 -0
- package/dist/finish/run-finish.d.ts.map +1 -0
- package/dist/finish/run-finish.js +239 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +6 -0
- package/dist/validation/typecheck-emitted.d.ts +36 -0
- package/dist/validation/typecheck-emitted.d.ts.map +1 -0
- package/dist/validation/typecheck-emitted.js +109 -0
- package/package.json +33 -33
|
@@ -20,20 +20,44 @@ function tokenize(s) {
|
|
|
20
20
|
function scorePrimitive(p, tokens) {
|
|
21
21
|
// Substring match (so intent "debounce" matches symbol `useDebounce`); the
|
|
22
22
|
// 3-char minimum above keeps it from over-matching on tiny fragments.
|
|
23
|
+
const symbolLower = p.symbol.toLowerCase();
|
|
23
24
|
const hay = [p.symbol, ...(p.roles ?? []), ...(p.keywords ?? []), p.description ?? '']
|
|
24
25
|
.join(' ')
|
|
25
26
|
.toLowerCase();
|
|
26
27
|
let s = 0;
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
const matched = [];
|
|
29
|
+
let symbolHit = false;
|
|
30
|
+
for (const t of tokens) {
|
|
31
|
+
if (hay.includes(t)) {
|
|
29
32
|
s += 1;
|
|
33
|
+
matched.push(t);
|
|
34
|
+
if (symbolLower.includes(t))
|
|
35
|
+
symbolHit = true;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
30
38
|
// A role the intent mentions is a strong signal; re-weight role hits.
|
|
31
39
|
for (const role of p.roles ?? []) {
|
|
32
40
|
const rl = role.toLowerCase();
|
|
33
41
|
if (rl.length >= 3 && tokens.some((t) => rl.includes(t)))
|
|
34
42
|
s += 0.5;
|
|
35
43
|
}
|
|
36
|
-
return s;
|
|
44
|
+
return { score: s, matched, symbolHit };
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Confidence floor: a keyword collision is not a match. A hit is only "confident"
|
|
48
|
+
* when it matched the symbol name itself, OR matched ≥2 distinct query tokens, OR
|
|
49
|
+
* the query was a single token and that token hit. A single generic keyword hit on
|
|
50
|
+
* a multi-token intent (the "nearest collision on an unrelated entry" failure mode)
|
|
51
|
+
* is a weak match — surfaced as a did-you-mean, never as a confident answer.
|
|
52
|
+
*/
|
|
53
|
+
function isConfidentMatch(detail, queryTokenCount) {
|
|
54
|
+
if (detail.matched.length === 0)
|
|
55
|
+
return false;
|
|
56
|
+
if (detail.symbolHit)
|
|
57
|
+
return true;
|
|
58
|
+
if (detail.matched.length >= 2)
|
|
59
|
+
return true;
|
|
60
|
+
return queryTokenCount <= 1;
|
|
37
61
|
}
|
|
38
62
|
const INDEX_RE = /(^|\/)index\.[cm]?[jt]sx?$/;
|
|
39
63
|
export const reuseCommand = {
|
|
@@ -82,14 +106,17 @@ export const reuseCommand = {
|
|
|
82
106
|
return 0;
|
|
83
107
|
}
|
|
84
108
|
const tokens = tokenize(intent);
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
.
|
|
88
|
-
.
|
|
89
|
-
.
|
|
109
|
+
const confidenceOf = (matched) => tokens.length === 0 ? 0 : matched / tokens.length;
|
|
110
|
+
const scored = primitives
|
|
111
|
+
.map((p) => ({ p, detail: scorePrimitive(p, tokens) }))
|
|
112
|
+
.filter((x) => x.detail.score > 0)
|
|
113
|
+
.sort((a, b) => b.detail.score - a.detail.score || a.p.symbol.localeCompare(b.p.symbol));
|
|
114
|
+
const confident = scored.filter((x) => isConfidentMatch(x.detail, tokens.length));
|
|
115
|
+
const ranked = confident.slice(0, Math.max(1, limit));
|
|
90
116
|
const store = new GraphStore(cwd);
|
|
91
117
|
const api = store.exists() ? GraphQueryApi.fromStore(cwd) : null;
|
|
92
|
-
|
|
118
|
+
// Zero keyword overlap: nothing matched at all → surface available roles.
|
|
119
|
+
if (scored.length === 0) {
|
|
93
120
|
const roles = [...new Set(primitives.flatMap((p) => p.roles))].sort();
|
|
94
121
|
if (wantJson) {
|
|
95
122
|
process.stdout.write(asJson({ schema: 'sharkcraft.reuse/v1', intent, results: [], availableRoles: roles, ...planeJson }) + '\n');
|
|
@@ -102,8 +129,41 @@ export const reuseCommand = {
|
|
|
102
129
|
writePlaneNotes();
|
|
103
130
|
return 0;
|
|
104
131
|
}
|
|
105
|
-
|
|
106
|
-
|
|
132
|
+
// Weak overlap only (a single generic keyword collision on an unrelated
|
|
133
|
+
// entry): below the confidence floor. A miss must look like a miss — never
|
|
134
|
+
// return the nearest collision as a confident answer. Offer did-you-mean.
|
|
135
|
+
if (ranked.length === 0) {
|
|
136
|
+
const didYouMean = scored.slice(0, 5).map((x) => ({
|
|
137
|
+
symbol: x.p.symbol,
|
|
138
|
+
score: x.detail.score,
|
|
139
|
+
confidence: confidenceOf(x.detail.matched.length),
|
|
140
|
+
matched: x.detail.matched,
|
|
141
|
+
roles: x.p.roles,
|
|
142
|
+
}));
|
|
143
|
+
if (wantJson) {
|
|
144
|
+
process.stdout.write(asJson({ schema: 'sharkcraft.reuse/v1', intent, confident: false, results: [], didYouMean, ...planeJson }) + '\n');
|
|
145
|
+
return 0;
|
|
146
|
+
}
|
|
147
|
+
process.stdout.write(header(`Reuse: "${intent}"`));
|
|
148
|
+
process.stdout.write(' No confident match — the intent only weakly overlaps existing primitives.\n' +
|
|
149
|
+
' Did you mean (weak, verify before reusing):\n');
|
|
150
|
+
for (const s of didYouMean) {
|
|
151
|
+
process.stdout.write(` • ${s.symbol} (score ${s.score}, ${Math.round(s.confidence * 100)}% of intent; matched: ${s.matched.join(', ') || '—'})\n`);
|
|
152
|
+
}
|
|
153
|
+
writePlaneNotes();
|
|
154
|
+
return 0;
|
|
155
|
+
}
|
|
156
|
+
const results = ranked.map(({ p, detail }) => {
|
|
157
|
+
const score = detail.score;
|
|
158
|
+
const r = {
|
|
159
|
+
symbol: p.symbol,
|
|
160
|
+
score,
|
|
161
|
+
confidence: confidenceOf(detail.matched.length),
|
|
162
|
+
matched: detail.matched,
|
|
163
|
+
roles: p.roles,
|
|
164
|
+
siblings: [],
|
|
165
|
+
consumers: [],
|
|
166
|
+
};
|
|
107
167
|
if (p.description)
|
|
108
168
|
r.description = p.description;
|
|
109
169
|
if (p.importPath)
|
|
@@ -141,8 +201,9 @@ export const reuseCommand = {
|
|
|
141
201
|
}
|
|
142
202
|
}
|
|
143
203
|
}
|
|
144
|
-
|
|
145
|
-
|
|
204
|
+
const sites = api.referenceSitesOf(sym.id);
|
|
205
|
+
r.consumerTotal = sites.length;
|
|
206
|
+
r.consumers = sites
|
|
146
207
|
.slice(0, 5)
|
|
147
208
|
.map((s) => ({ path: s.node.path ?? s.node.id, ...(s.line ? { line: s.line } : {}) }));
|
|
148
209
|
const alts = pool.slice(1).map((c) => c.path).filter((x) => !!x);
|
|
@@ -174,6 +235,7 @@ export const reuseCommand = {
|
|
|
174
235
|
process.stdout.write(`\n${i}. ${r.symbol}\n`);
|
|
175
236
|
if (r.description)
|
|
176
237
|
process.stdout.write(` ${r.description}\n`);
|
|
238
|
+
process.stdout.write(` match: score ${r.score} (${Math.round(r.confidence * 100)}% of intent; matched: ${r.matched.join(', ') || '—'})\n`);
|
|
177
239
|
if (r.notFound) {
|
|
178
240
|
process.stdout.write(' ⚠ symbol not found in the code graph — verify reusePrimitives[].symbol (typo/rename?) or run `shrk graph index`\n');
|
|
179
241
|
}
|
|
@@ -192,7 +254,11 @@ export const reuseCommand = {
|
|
|
192
254
|
if (r.siblings.length > 0)
|
|
193
255
|
process.stdout.write(` sibling exports: ${r.siblings.join(', ')}\n`);
|
|
194
256
|
if (r.consumers.length > 0) {
|
|
195
|
-
|
|
257
|
+
const total = r.consumerTotal ?? r.consumers.length;
|
|
258
|
+
const label = total > r.consumers.length
|
|
259
|
+
? ` consumers to copy (${total} total, showing ${r.consumers.length}):\n`
|
|
260
|
+
: ` consumers to copy (${total} total):\n`;
|
|
261
|
+
process.stdout.write(label);
|
|
196
262
|
for (const c of r.consumers)
|
|
197
263
|
process.stdout.write(` - ${c.path}${c.line ? ':' + c.line : ''}\n`);
|
|
198
264
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"smart-context.command.d.ts","sourceRoot":"","sources":["../../src/commands/smart-context.command.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"smart-context.command.d.ts","sourceRoot":"","sources":["../../src/commands/smart-context.command.ts"],"names":[],"mappings":"AA2BA,OAAO,EAML,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAmFhC,eAAO,MAAM,mBAAmB,EAAE,eAiQjC,CAAC;AAEF,+EAA+E;AAC/E,eAAO,MAAM,4BAA4B,EAAE,eAsF1C,CAAC;AAEF,sDAAsD;AACtD,eAAO,MAAM,uBAAuB,EAAE,eAwBrC,CAAC;AAEF,8DAA8D;AAC9D,eAAO,MAAM,uBAAuB,EAAE,eAkCrC,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,iCAAiC,EAAE,eAmH/C,CAAC;AA2JF;;;;;;;GAOG;AACH,eAAO,MAAM,iCAAiC,EAAE,eAoH/C,CAAC;AA2JF;;;;;;GAMG;AACH,eAAO,MAAM,iCAAiC,EAAE,eAgG/C,CAAC;AAyPF,4EAA4E;AAC5E,eAAO,MAAM,kCAAkC,EAAE,eAuHhD,CAAC;AAMF,iFAAiF;AACjF,eAAO,MAAM,mCAAmC,EAAE,eAsCjD,CAAC;AAqJF,UAAU,eAAe;IACvB,6CAA6C;IAC7C,OAAO,EAAE,MAAM,CAAC;IAChB,kFAAkF;IAClF,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,uFAAuF;IACvF,SAAS,EAAE,OAAO,CAAC;IACnB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,mEAAmE;IACnE,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;GAIG;AACH,wBAAgB,2BAA2B,CAAC,CAAC,EAAE,eAAe,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAWzF"}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { spawn
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import * as nodePath from 'node:path';
|
|
4
4
|
import * as os from 'node:os';
|
|
5
5
|
import { AiMessageRole, buildPromptMessages, EnhancementPipeline, EnhancementStageKind, OllamaProvider, buildDefaultEnhancementStages, buildFastEnhancementStages, selectAiProvider, } from '@shrkcrft/ai';
|
|
6
6
|
import { buildContext } from '@shrkcrft/context';
|
|
7
|
+
import { runGitLines } from '@shrkcrft/shared';
|
|
7
8
|
import { EdgeKind, GraphQueryApi, GraphStore, NodeKind } from '@shrkcrft/graph';
|
|
8
9
|
import { buildProjectOverview, buildTaskPacket, contextTuningBoostFor, inspectSharkcraft, renderOverviewText, } from '@shrkcrft/inspector';
|
|
9
10
|
import { flagBool, flagList, flagNumber, flagString, resolveCwd, } from "../command-registry.js";
|
|
@@ -51,11 +52,13 @@ export const smartContextCommand = {
|
|
|
51
52
|
name: 'smart-context',
|
|
52
53
|
booleanFlags: SMART_CONTEXT_BOOLEAN_FLAGS,
|
|
53
54
|
description: 'Build deterministic context and ask an AI provider to synthesise an enriched brief (default), structured plan (--plan), or two-stage development plan (--ai-plan).',
|
|
54
|
-
usage: 'shrk smart-context "<task>" [--plus] [--budget <seconds>] [--plan] [--ai-plan] [--save] [--provider auto|ollama|llamacpp] [--enhance|--no-enhance] [--enhance-passes N] [--instructions <path>] [--no-instructions] [--model <id>] [--max-tokens N] [--stage1-max-tokens N] [--seed-tokens N] [--expansion-tokens N] [--expansion-limit N] [--log-prompt] [--save-conversation[=<path>]] [--dry-run] [--debug] [--json]',
|
|
55
|
+
usage: 'shrk smart-context "<task>" | --task "<task>" [--plus] [--budget <seconds>] [--plan] [--ai-plan] [--save] [--provider auto|ollama|llamacpp] [--enhance|--no-enhance] [--enhance-passes N] [--instructions <path>] [--no-instructions] [--model <id>] [--max-tokens N] [--stage1-max-tokens N] [--seed-tokens N] [--expansion-tokens N] [--expansion-limit N] [--log-prompt] [--save-conversation[=<path>]] [--dry-run] [--debug] [--json]',
|
|
55
56
|
async run(args) {
|
|
56
|
-
|
|
57
|
+
// Accept the documented `--task` form as well as the positional form, so the
|
|
58
|
+
// CLI surface matches the docs (which advertise `--task` broadly).
|
|
59
|
+
const task = (args.positional.join(' ').trim() || flagString(args, 'task') || '').trim();
|
|
57
60
|
if (!task) {
|
|
58
|
-
process.stderr.write('Usage: shrk smart-context "<task>" [--plan] [--ai-plan] [--save]\n');
|
|
61
|
+
process.stderr.write('Usage: shrk smart-context "<task>" [--task "<task>"] [--plan] [--ai-plan] [--save]\n');
|
|
59
62
|
return 2;
|
|
60
63
|
}
|
|
61
64
|
// Isolate the LLM / native-runtime work in a child process. On macOS the
|
|
@@ -164,7 +167,7 @@ export const smartContextCommand = {
|
|
|
164
167
|
(originalContext.length > 0 && enhanced.value.content.trim() === originalContext);
|
|
165
168
|
if (fullyDegraded) {
|
|
166
169
|
if (!opts.json) {
|
|
167
|
-
process.stderr.write('
|
|
170
|
+
process.stderr.write(degradedRetrievalBanner(seed, 'enhancement fully degraded') + '\n');
|
|
168
171
|
}
|
|
169
172
|
const fallbackEnvelope = buildEnvelope({
|
|
170
173
|
task,
|
|
@@ -225,7 +228,7 @@ export const smartContextCommand = {
|
|
|
225
228
|
// exit 1. The agent that asked for fast grounding still gets usable
|
|
226
229
|
// rules / paths / templates / candidate files. The error is advisory on
|
|
227
230
|
// stderr (never stdout, so --json stays valid).
|
|
228
|
-
process.stderr.write('
|
|
231
|
+
process.stderr.write(degradedRetrievalBanner(seed, 'provider unavailable') + '\n');
|
|
229
232
|
const fallbackEnvelope = buildEnvelope({
|
|
230
233
|
task,
|
|
231
234
|
seed,
|
|
@@ -1451,6 +1454,19 @@ export function renderIndexFreshnessWarning(f) {
|
|
|
1451
1454
|
`(${f.stale} changed, ${f.missing} deleted, ${f.untracked} new).${pruned} ` +
|
|
1452
1455
|
'Verify the file suggestions above before editing — run `shrk smart-context --refresh` to rebuild.');
|
|
1453
1456
|
}
|
|
1457
|
+
/**
|
|
1458
|
+
* Loud one-liner for the degraded / provider-unavailable path. The returned
|
|
1459
|
+
* content is the deterministic seed (rules/paths/templates/candidate files), NOT
|
|
1460
|
+
* task-scoped semantic retrieval — so say so, and how far the index is behind,
|
|
1461
|
+
* so a caller never trusts a stale generic dump as if it were curated retrieval.
|
|
1462
|
+
*/
|
|
1463
|
+
function degradedRetrievalBanner(seed, cause) {
|
|
1464
|
+
const behind = seed.indexFreshness?.behind ?? 0;
|
|
1465
|
+
const behindClause = behind > 0 ? `, semantic index ${behind} file(s) behind the working tree` : '';
|
|
1466
|
+
return (`[smart-context] semantic retrieval unavailable (${cause})${behindClause} — ` +
|
|
1467
|
+
'falling back to the deterministic seed (rules/paths/templates/candidate files), NOT task-scoped retrieval. ' +
|
|
1468
|
+
'Run `shrk smart-context --refresh` to rebuild the index.');
|
|
1469
|
+
}
|
|
1454
1470
|
async function buildSmartContextSeed(input) {
|
|
1455
1471
|
const { cwd, task, inspection, options } = input;
|
|
1456
1472
|
const overview = buildProjectOverview(inspection.workspace, inspection.config?.projectName);
|
|
@@ -2182,45 +2198,21 @@ function isSemanticAutomationDisabled() {
|
|
|
2182
2198
|
* ripples through. Empty array on git failure or no-graph.
|
|
2183
2199
|
*/
|
|
2184
2200
|
function collectChangedPathsWithNeighbors(cwd, gitRef) {
|
|
2185
|
-
//
|
|
2186
|
-
//
|
|
2187
|
-
//
|
|
2188
|
-
// gate flags
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
const out = spawnSync('git', ['-C', cwd, 'diff', '--name-only', `${gitRef}...HEAD`], {
|
|
2192
|
-
encoding: 'utf8',
|
|
2193
|
-
});
|
|
2194
|
-
if (out.status !== 0)
|
|
2195
|
-
return [];
|
|
2196
|
-
changed = (out.stdout ?? '')
|
|
2197
|
-
.split('\n')
|
|
2198
|
-
.map((s) => s.trim())
|
|
2199
|
-
.filter((s) => s.length > 0);
|
|
2200
|
-
}
|
|
2201
|
-
catch {
|
|
2202
|
-
return [];
|
|
2203
|
-
}
|
|
2204
|
-
if (changed.length === 0)
|
|
2201
|
+
// `runGitLines` is shell-free + high-maxBuffer, so a large changeset can't
|
|
2202
|
+
// ENOBUFS-crash the feedback bundle. (It also keeps the CLI on
|
|
2203
|
+
// `node:child_process` rather than `Bun.*`, which the compat-node preflight
|
|
2204
|
+
// gate flags as a publish blocker.)
|
|
2205
|
+
const committed = runGitLines(cwd, ['diff', '--name-only', `${gitRef}...HEAD`]);
|
|
2206
|
+
if (!committed.ok || committed.lines.length === 0)
|
|
2205
2207
|
return [];
|
|
2208
|
+
const changed = [...committed.lines];
|
|
2206
2209
|
// Also include uncommitted changes — agent feedback should reflect
|
|
2207
2210
|
// the *current* working tree, not just committed deltas.
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
const uncommitted = (out.stdout ?? '')
|
|
2214
|
-
.split('\n')
|
|
2215
|
-
.map((s) => s.trim())
|
|
2216
|
-
.filter((s) => s.length > 0);
|
|
2217
|
-
for (const p of uncommitted)
|
|
2218
|
-
if (!changed.includes(p))
|
|
2219
|
-
changed.push(p);
|
|
2220
|
-
}
|
|
2221
|
-
}
|
|
2222
|
-
catch {
|
|
2223
|
-
// ignore
|
|
2211
|
+
const uncommitted = runGitLines(cwd, ['diff', '--name-only', 'HEAD']);
|
|
2212
|
+
if (uncommitted.ok) {
|
|
2213
|
+
for (const p of uncommitted.lines)
|
|
2214
|
+
if (!changed.includes(p))
|
|
2215
|
+
changed.push(p);
|
|
2224
2216
|
}
|
|
2225
2217
|
const set = new Set(changed);
|
|
2226
2218
|
// One-hop graph expansion if the graph is fresh.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"task.command.d.ts","sourceRoot":"","sources":["../../src/commands/task.command.ts"],"names":[],"mappings":"AAiBA,OAAO,EAKL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"task.command.d.ts","sourceRoot":"","sources":["../../src/commands/task.command.ts"],"names":[],"mappings":"AAiBA,OAAO,EAKL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAkEhC,eAAO,MAAM,WAAW,EAAE,eAgRzB,CAAC"}
|
|
@@ -3,6 +3,12 @@ import { SpecStatus } from '@shrkcrft/generator';
|
|
|
3
3
|
import { flagBool, flagNumber, flagList, resolveCwd, } from "../command-registry.js";
|
|
4
4
|
import { asJson, header, kv } from "../output/format-output.js";
|
|
5
5
|
import { buildTaskNextReport } from "../task-next/task-next-ranker.js";
|
|
6
|
+
/**
|
|
7
|
+
* Budget for the DEFAULT full human-text packet when no explicit `--max-tokens`
|
|
8
|
+
* is set — large enough to hold every planned context section so orientation
|
|
9
|
+
* isn't silently thinner than the sibling verbs. JSON/terse callers keep 3500.
|
|
10
|
+
*/
|
|
11
|
+
const WIDE_TASK_BUDGET = 100_000;
|
|
6
12
|
function compactTaskPacket(p) {
|
|
7
13
|
return {
|
|
8
14
|
task: p.task,
|
|
@@ -139,14 +145,26 @@ export const taskCommand = {
|
|
|
139
145
|
return 2;
|
|
140
146
|
}
|
|
141
147
|
const inspection = await inspectSharkcraft({ cwd: resolveCwd(args) });
|
|
142
|
-
const maxTokens = flagNumber(args, 'max-tokens') ?? 3500;
|
|
143
148
|
const scope = flagList(args, 'scope');
|
|
144
149
|
const explainRanking = flagBool(args, 'explain-ranking') || flagBool(args, 'json');
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
const
|
|
150
|
+
// Orientation renders the full packet by default now (parity with why /
|
|
151
|
+
// reuse / knowledge get / `shrk context`) so the agent's first read isn't
|
|
152
|
+
// thinner than every adjacent command. `--summary`/`--brief` (or
|
|
153
|
+
// `--commands-first`/`--actions-only`) opts back into the terse view.
|
|
154
|
+
const wantsJson = flagBool(args, 'json');
|
|
155
|
+
const terse = flagBool(args, 'summary') ||
|
|
156
|
+
flagBool(args, 'brief') ||
|
|
157
|
+
flagBool(args, 'commands-first') ||
|
|
158
|
+
flagBool(args, 'actions-only');
|
|
159
|
+
// Compact packet (5 rules / 3 templates / 5 hints per field) still backs the
|
|
160
|
+
// default JSON shape and the terse text view; the full text view is uncapped.
|
|
161
|
+
const compact = wantsJson
|
|
162
|
+
? !flagBool(args, 'full') && !flagBool(args, 'verbose')
|
|
163
|
+
: terse;
|
|
164
|
+
// Auto-widen the context budget for the default full human view so no rich
|
|
165
|
+
// section is dropped purely to fit a tight budget; JSON/terse keep 3500.
|
|
166
|
+
const wideView = !wantsJson && !terse;
|
|
167
|
+
const maxTokens = flagNumber(args, 'max-tokens') ?? (wideView ? WIDE_TASK_BUDGET : 3500);
|
|
150
168
|
const packet = buildTaskPacket(inspection, task, {
|
|
151
169
|
maxTokens,
|
|
152
170
|
...(scope.length ? { scope } : {}),
|
|
@@ -185,14 +203,13 @@ export const taskCommand = {
|
|
|
185
203
|
process.stdout.write(kv('detected profiles', packet.detectedProfiles.join(', ') || '(none)') + '\n');
|
|
186
204
|
process.stdout.write(kv('context tokens', `${packet.context.totalTokens} / ${packet.context.maxTokens}`) + '\n');
|
|
187
205
|
process.stdout.write(kv('total token est.', String(packet.tokenEstimate)) + '\n');
|
|
188
|
-
// Commands-first summary at the top so the agent sees the action
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
// `--full` to print the full packet. JSON output is unchanged.
|
|
206
|
+
// Commands-first summary at the top so the agent sees the action path
|
|
207
|
+
// before the long context body. The full packet then prints by default;
|
|
208
|
+
// `--commands-first`/`--actions-only`/`--summary` collapse to just the
|
|
209
|
+
// commands + uncertainty footer.
|
|
193
210
|
const commandsFirst = flagBool(args, 'commands-first');
|
|
194
211
|
const actionsOnly = flagBool(args, 'actions-only');
|
|
195
|
-
const
|
|
212
|
+
const summaryOnly = flagBool(args, 'summary') || flagBool(args, 'brief');
|
|
196
213
|
if (packet.recommendedCliCommands.length > 0) {
|
|
197
214
|
process.stdout.write('\nTop commands (command-first):\n');
|
|
198
215
|
for (const c of packet.recommendedCliCommands.slice(0, 5)) {
|
|
@@ -203,11 +220,11 @@ export const taskCommand = {
|
|
|
203
220
|
process.stdout.write('\nSuggested generation:\n');
|
|
204
221
|
process.stdout.write(` $ ${packet.suggestedGen.dryRunCommand}\n`);
|
|
205
222
|
}
|
|
206
|
-
if (commandsFirst || actionsOnly ||
|
|
207
|
-
if (
|
|
208
|
-
process.stdout.write('\n(
|
|
223
|
+
if (commandsFirst || actionsOnly || summaryOnly) {
|
|
224
|
+
if (summaryOnly && !commandsFirst && !actionsOnly) {
|
|
225
|
+
process.stdout.write('\n(summary mode — omit --summary for the full packet, --json for machine output.)\n');
|
|
209
226
|
}
|
|
210
|
-
// Render uncertainty and stop — caller asked for
|
|
227
|
+
// Render uncertainty and stop — caller asked for terse output.
|
|
211
228
|
process.stdout.write('\n' + renderUncertaintyText(uncertainty) + '\n');
|
|
212
229
|
return 0;
|
|
213
230
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"trace.command.d.ts","sourceRoot":"","sources":["../../src/commands/trace.command.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"trace.command.d.ts","sourceRoot":"","sources":["../../src/commands/trace.command.ts"],"names":[],"mappings":"AAmBA,OAAO,EAML,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AA6EhC,eAAO,MAAM,YAAY,EAAE,eAwH1B,CAAC"}
|
|
@@ -6,16 +6,86 @@
|
|
|
6
6
|
* shared query resolver and prints structured trace output.
|
|
7
7
|
*/
|
|
8
8
|
import { findSymbolInProject, inspectSharkcraft, QueryMatchKind, resolveQuery, } from '@shrkcrft/inspector';
|
|
9
|
+
import { traceLiteral, TraceRole, } from '@shrkcrft/boundaries';
|
|
9
10
|
import { flagBool, flagList, flagNumber, flagString, resolveCwd, } from "../command-registry.js";
|
|
10
|
-
import { asJson, header } from "../output/format-output.js";
|
|
11
|
+
import { asJson, header, kv } from "../output/format-output.js";
|
|
11
12
|
function describeMatch(m) {
|
|
12
13
|
return `${m.kind.padEnd(12)} ${m.id}${m.label && m.label !== m.id ? ` — ${m.label}` : ''} [${m.score.toFixed(0)}]`;
|
|
13
14
|
}
|
|
15
|
+
const LITERAL_SITE_CAP = 30;
|
|
16
|
+
const TRACE_ROLE_ORDER = [
|
|
17
|
+
TraceRole.Declare,
|
|
18
|
+
TraceRole.Register,
|
|
19
|
+
TraceRole.Consume,
|
|
20
|
+
TraceRole.Reference,
|
|
21
|
+
];
|
|
22
|
+
function renderTraceLiteral(report, limit) {
|
|
23
|
+
process.stdout.write(header(`Trace literal: "${report.literal}"`));
|
|
24
|
+
process.stdout.write(kv('found', `${report.total} site(s) across ${report.files} file(s)`) + '\n');
|
|
25
|
+
if (report.aliases.length > 0) {
|
|
26
|
+
process.stdout.write(kv('const aliases', report.aliases.join(', ')) + '\n');
|
|
27
|
+
}
|
|
28
|
+
if (report.total === 0) {
|
|
29
|
+
process.stdout.write('\nNo occurrences of that exact string literal in scope.\n');
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
// Mirror `shrk registry <name> where`'s `<role> file:line` line idiom so the
|
|
33
|
+
// two surfaces read the same — `trace literal` is the same scanner + classifier
|
|
34
|
+
// without a pre-declared registry, just with two extra roles (`registered`,
|
|
35
|
+
// `reference`). The raw source line stays in `--json` (`text`); the human view
|
|
36
|
+
// is classification-first (direction), not a grep-style text dump.
|
|
37
|
+
process.stdout.write('\n');
|
|
38
|
+
for (const role of TRACE_ROLE_ORDER) {
|
|
39
|
+
const sites = report.byRole[role];
|
|
40
|
+
for (const s of sites.slice(0, limit)) {
|
|
41
|
+
const alias = s.viaAlias ? ` [via ${s.viaAlias}]` : '';
|
|
42
|
+
process.stdout.write(` ${role.padEnd(10)} ${s.file}:${s.line}${alias}\n`);
|
|
43
|
+
}
|
|
44
|
+
if (sites.length > limit) {
|
|
45
|
+
process.stdout.write(` ${role.padEnd(10)} … (${sites.length - limit} more — pass --json)\n`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* `shrk trace literal "<string>"`: trace an EXACT string literal across the
|
|
51
|
+
* codebase, classified by direction (declare → register → consume), resolving
|
|
52
|
+
* `const X = "lit"` aliases too. Generalizes `registry … where` to any
|
|
53
|
+
* cross-fence string contract (a kind slug, permission id, route key) with no
|
|
54
|
+
* pre-declared registry — the chain grep can't give (direction + role + layer).
|
|
55
|
+
*/
|
|
56
|
+
function runTraceLiteral(args) {
|
|
57
|
+
const cwd = resolveCwd(args);
|
|
58
|
+
const wantJson = flagBool(args, 'json');
|
|
59
|
+
// `trace literal "<lit>"` → the literal is positional[1] (quoted by the user
|
|
60
|
+
// so an internal space stays one token); join the rest defensively.
|
|
61
|
+
const literal = args.positional.slice(1).join(' ');
|
|
62
|
+
if (literal === '') {
|
|
63
|
+
process.stderr.write('Usage: shrk trace literal "<string>" [--glob <g1,g2>] [--no-aliases] [--limit N] [--json]\n');
|
|
64
|
+
return 2;
|
|
65
|
+
}
|
|
66
|
+
const globs = flagList(args, 'glob');
|
|
67
|
+
const report = traceLiteral(cwd, literal, {
|
|
68
|
+
...(globs.length > 0 ? { globs } : {}),
|
|
69
|
+
resolveAliases: !flagBool(args, 'no-aliases'),
|
|
70
|
+
});
|
|
71
|
+
if (wantJson) {
|
|
72
|
+
process.stdout.write(asJson(report) + '\n');
|
|
73
|
+
return 0;
|
|
74
|
+
}
|
|
75
|
+
const limitRaw = flagNumber(args, 'limit');
|
|
76
|
+
const limit = limitRaw !== undefined && limitRaw > 0 ? Math.floor(limitRaw) : LITERAL_SITE_CAP;
|
|
77
|
+
renderTraceLiteral(report, limit);
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
14
80
|
export const traceCommand = {
|
|
15
81
|
name: 'trace',
|
|
16
|
-
description: 'Fuzzy trace — accept any free-form query (file path, construct id, symbol, plugin key, helper id, template id, knowledge id, command). Read-only.',
|
|
17
|
-
usage: 'shrk trace <query> [--limit <n>] [--kind
|
|
82
|
+
description: 'Fuzzy trace — accept any free-form query (file path, construct id, symbol, plugin key, helper id, template id, knowledge id, command). `trace literal "<string>"` traces an exact string literal\'s declare→register→consume chain across layers (alias-resolved). Read-only.',
|
|
83
|
+
usage: 'shrk trace <query> | shrk trace literal "<string>" [--glob <g>] [--no-aliases] [--limit <n>] [--kind ...] [--deep] [--json]',
|
|
84
|
+
booleanFlags: new Set(['json', 'deep', 'no-aliases']),
|
|
18
85
|
async run(args) {
|
|
86
|
+
// `trace literal "<lit>"` — cross-fence string-contract tracer (3.3).
|
|
87
|
+
if (args.positional[0] === 'literal')
|
|
88
|
+
return runTraceLiteral(args);
|
|
19
89
|
// Direct symbol trace via --symbol <Name>
|
|
20
90
|
const symbol = flagString(args, 'symbol');
|
|
21
91
|
if (symbol) {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type IWiringExplain } from '@shrkcrft/boundaries';
|
|
2
|
+
import { type ICommandHandler } from '../command-registry.js';
|
|
3
|
+
/**
|
|
4
|
+
* Render an {@link IWiringExplain} to stdout. Shared by `wiring explain`,
|
|
5
|
+
* `wiring test`, and `check wiring --explain` so all three speak the same
|
|
6
|
+
* dialect. JSON emits the full payload; text mirrors `search tuning explain`
|
|
7
|
+
* (header → loaded sets → per-site detail → the set-difference → verdict).
|
|
8
|
+
* Always returns 0 — explain is informational, the verdict is in the output.
|
|
9
|
+
*/
|
|
10
|
+
export declare function renderWiringExplain(report: IWiringExplain, wantJson: boolean): number;
|
|
11
|
+
export declare const wiringCommand: ICommandHandler;
|
|
12
|
+
//# sourceMappingURL=wiring.command.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wiring.command.d.ts","sourceRoot":"","sources":["../../src/commands/wiring.command.ts"],"names":[],"mappings":"AAAA,OAAO,EASL,KAAK,cAAc,EACpB,MAAM,sBAAsB,CAAC;AAM9B,OAAO,EAAwB,KAAK,eAAe,EAAmB,MAAM,wBAAwB,CAAC;AAKrG;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,cAAc,EAAE,QAAQ,EAAE,OAAO,GAAG,MAAM,CAsDrF;AA+TD,eAAO,MAAM,aAAa,EAAE,eAgB3B,CAAC"}
|