@shrkcrft/cli 0.1.0-alpha.24 → 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 +73 -15
- package/dist/commands/command-catalog.d.ts.map +1 -1
- package/dist/commands/command-catalog.js +8 -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/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 +14 -2
- 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 +20 -5
- package/dist/commands/task.command.d.ts.map +1 -1
- package/dist/commands/task.command.js +33 -16
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +2 -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
|
@@ -6,6 +6,15 @@ import * as nodePath from 'node:path';
|
|
|
6
6
|
import { buildUniversalSearch, explainTaskRouting, recommendCommands, renderOverviewText, buildProjectOverview, } from '@shrkcrft/inspector';
|
|
7
7
|
import { flagBool, flagNumber, flagString, flagList, resolveCwd, } from "../command-registry.js";
|
|
8
8
|
import { asJson, header } from "../output/format-output.js";
|
|
9
|
+
/**
|
|
10
|
+
* Budget used for the DEFAULT full human-text orientation view when the caller
|
|
11
|
+
* set no explicit `--max-tokens`. Orientation is the cheap-first step an agent
|
|
12
|
+
* uses to form its initial model of the project — it must not silently drop the
|
|
13
|
+
* body of the richest sections (architecture, conventions, paths, workflows) the
|
|
14
|
+
* way a tight budget would. Large enough to hold every planned section; explicit
|
|
15
|
+
* `--max-tokens` and JSON/`--summary` callers still get the configured budget.
|
|
16
|
+
*/
|
|
17
|
+
const WIDE_CONTEXT_BUDGET = 100_000;
|
|
9
18
|
/**
|
|
10
19
|
* Minimal JSON shape for agent / skill consumption — the context-side mirror
|
|
11
20
|
* of `shrk task --compact`. Drops the heavy `body` and `request` echo and
|
|
@@ -65,8 +74,19 @@ export const contextCommand = {
|
|
|
65
74
|
const noPaths = flagBool(args, 'no-paths');
|
|
66
75
|
const includeDocs = flagBool(args, 'include-docs');
|
|
67
76
|
const includeCommands = flagBool(args, 'include-commands');
|
|
77
|
+
// Orientation renders the FULL body by default now (parity with why / reuse
|
|
78
|
+
// / knowledge get). `--summary`/`--brief` opts back into the terse view.
|
|
79
|
+
const wantsSummary = flagBool(args, 'summary') || flagBool(args, 'brief');
|
|
80
|
+
const wantsJsonOut = flagBool(args, 'json') || flagBool(args, 'machine-json');
|
|
68
81
|
const inspection = await inspectSharkcraft({ cwd: resolveCwd(args) });
|
|
69
82
|
const overview = buildProjectOverview(inspection.workspace, inspection.config?.projectName);
|
|
83
|
+
// Auto-widen the budget for the default full human view so no requested
|
|
84
|
+
// section is dropped purely to fit a tight budget. Explicit `--max-tokens`,
|
|
85
|
+
// JSON, and summary callers keep the configured budget.
|
|
86
|
+
const effectiveMaxTokens = maxTokens ??
|
|
87
|
+
(!wantsJsonOut && !wantsSummary
|
|
88
|
+
? WIDE_CONTEXT_BUDGET
|
|
89
|
+
: (inspection.config?.defaultMaxTokens ?? 3000));
|
|
70
90
|
const contextBoost = contextTuningBoostFor(inspection, task);
|
|
71
91
|
const result = buildContext(inspection.knowledgeEntries, {
|
|
72
92
|
task,
|
|
@@ -74,7 +94,7 @@ export const contextCommand = {
|
|
|
74
94
|
area,
|
|
75
95
|
tags,
|
|
76
96
|
scope,
|
|
77
|
-
maxTokens:
|
|
97
|
+
maxTokens: effectiveMaxTokens,
|
|
78
98
|
includeExamples: !noExamples,
|
|
79
99
|
includeTemplates: !noTemplates,
|
|
80
100
|
includeRules: !noRules,
|
|
@@ -84,14 +104,10 @@ export const contextCommand = {
|
|
|
84
104
|
projectOverview: renderOverviewText(overview),
|
|
85
105
|
...(contextBoost ? { boostFor: contextBoost } : {}),
|
|
86
106
|
});
|
|
87
|
-
// Surface top commands prominently before the long context body.
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
const actionVerbRe = /^(rename|add|fix|refactor|remove|delete|migrate|wire|explore|create|implement|update|introduce|build|extract|move|inline|generate|scaffold)\b/i;
|
|
92
|
-
const isActionLike = actionVerbRe.test(task.trim());
|
|
93
|
-
const wantsFull = flagBool(args, 'full');
|
|
94
|
-
const commandsFirst = flagBool(args, 'commands-first') || (isActionLike && !wantsFull);
|
|
107
|
+
// Surface top commands prominently before the long context body. The body
|
|
108
|
+
// itself now prints by default (parity with sibling orientation verbs);
|
|
109
|
+
// `--commands-first` keeps the terse commands-only view for action tasks.
|
|
110
|
+
const commandsOnly = flagBool(args, 'commands-first');
|
|
95
111
|
let commandRecommendations = null;
|
|
96
112
|
let routingMatches = [];
|
|
97
113
|
let searchReport = null;
|
|
@@ -141,16 +157,12 @@ export const contextCommand = {
|
|
|
141
157
|
process.stdout.write(` • ${m.hint.id} ${m.hint.title}\n`);
|
|
142
158
|
}
|
|
143
159
|
}
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
else if (isActionLike && !flagBool(args, 'commands-first')) {
|
|
152
|
-
process.stdout.write('\n(action-like task → commands-first; pass --full to see the long context body.)\n');
|
|
153
|
-
}
|
|
160
|
+
// The full context body prints by default now — parity with why / reuse /
|
|
161
|
+
// knowledge get, so the agent's first read of the project isn't thinner than
|
|
162
|
+
// every adjacent command. `--summary`/`--brief` (or `--commands-first`) opts
|
|
163
|
+
// back into the terse view.
|
|
164
|
+
if (wantsSummary || commandsOnly) {
|
|
165
|
+
process.stdout.write('\n(summary mode — omit --summary for the full context body, --json for machine output.)\n');
|
|
154
166
|
return 0;
|
|
155
167
|
}
|
|
156
168
|
process.stdout.write('\n');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gate.command.d.ts","sourceRoot":"","sources":["../../src/commands/gate.command.ts"],"names":[],"mappings":"AAaA,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAGhC;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,WAAW,EAAE,
|
|
1
|
+
{"version":3,"file":"gate.command.d.ts","sourceRoot":"","sources":["../../src/commands/gate.command.ts"],"names":[],"mappings":"AAaA,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAGhC;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,WAAW,EAAE,eAsMzB,CAAC"}
|
|
@@ -143,7 +143,12 @@ export const gateCommand = {
|
|
|
143
143
|
: {}),
|
|
144
144
|
impact: {
|
|
145
145
|
...(sinceRef ? { sinceRef } : {}),
|
|
146
|
-
|
|
146
|
+
// Blast-radius risk is inherently PRE-EXISTING structure (touching a hub
|
|
147
|
+
// is risky but not a new failure this change introduced), so the composite
|
|
148
|
+
// gate treats it as ADVISORY by default — `failOn: []` warns instead of
|
|
149
|
+
// redding, keeping the verdict change-attributable. `--fail-on critical`
|
|
150
|
+
// opts into a hard fail; `--strict` escalates the advisory warn.
|
|
151
|
+
failOn: failOn ?? [],
|
|
147
152
|
// Scope the impact gate to the changeset too: with `--since` we keep the
|
|
148
153
|
// gitref diff; with `--changed-only` / `--staged` / `--files` (and no
|
|
149
154
|
// `--since`) we analyze the resolved changed-file set directly.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gen.command.d.ts","sourceRoot":"","sources":["../../src/commands/gen.command.ts"],"names":[],"mappings":"AASA,OAAO,EAKL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"gen.command.d.ts","sourceRoot":"","sources":["../../src/commands/gen.command.ts"],"names":[],"mappings":"AASA,OAAO,EAKL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAuBhC,eAAO,MAAM,UAAU,EAAE,eAsNxB,CAAC"}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { inspectSharkcraft } from '@shrkcrft/inspector';
|
|
2
2
|
import { buildSavedPlan, FileChangeType, generate, OverwriteStrategy, savePlanToFile, signPlan, } from '@shrkcrft/generator';
|
|
3
3
|
import { flagBool, flagString, flagVars, resolveCwd, } from "../command-registry.js";
|
|
4
|
+
import * as nodePath from 'node:path';
|
|
4
5
|
import { asJson, header } from "../output/format-output.js";
|
|
5
6
|
import { printError } from "../output/print-error.js";
|
|
7
|
+
import { typecheckEmittedFiles, } from "../validation/typecheck-emitted.js";
|
|
6
8
|
const CHANGE_LABEL = {
|
|
7
9
|
[FileChangeType.Create]: 'CREATE',
|
|
8
10
|
[FileChangeType.Update]: 'UPDATE',
|
|
@@ -19,7 +21,7 @@ const CHANGE_LABEL = {
|
|
|
19
21
|
export const genCommand = {
|
|
20
22
|
name: 'gen',
|
|
21
23
|
description: 'Generate code from a template. Defaults to dry-run.',
|
|
22
|
-
usage: 'shrk gen <templateId> [<name>] [--var key=value ...] [--dry-run] [--write] [--force] [--save-plan <file>] [--show-content] [--json]',
|
|
24
|
+
usage: 'shrk gen <templateId> [<name>] [--var key=value ...] [--dry-run] [--write] [--force] [--save-plan <file>] [--print|--show-content] [--typecheck] [--json]\n (--typecheck compiles the emitted files against the detected tsconfig BEFORE apply — a template bug fails here, not at the human next build; --print shows the rendered bodies)',
|
|
23
25
|
async run(args) {
|
|
24
26
|
const templateId = args.positional[0];
|
|
25
27
|
const name = args.positional[1];
|
|
@@ -40,19 +42,50 @@ export const genCommand = {
|
|
|
40
42
|
: flagString(args, 'overwrite') ?? OverwriteStrategy.Never;
|
|
41
43
|
const variables = flagVars(args);
|
|
42
44
|
const savePlanPath = flagString(args, 'save-plan');
|
|
43
|
-
const
|
|
45
|
+
const wantTypecheck = flagBool(args, 'typecheck');
|
|
46
|
+
const genOpts = {
|
|
44
47
|
templateId: template.id,
|
|
45
48
|
name,
|
|
46
49
|
variables,
|
|
47
50
|
projectRoot: inspection.projectRoot,
|
|
48
51
|
overwriteStrategy: overwrite,
|
|
49
|
-
|
|
50
|
-
|
|
52
|
+
};
|
|
53
|
+
// --typecheck is a PRE-WRITE gate. Render a dry-run FIRST, compile the emitted
|
|
54
|
+
// (full-file create) TS/TSX against the detected tsconfig in memory, and only
|
|
55
|
+
// proceed to write if they compile — so an agent driving gen→apply never lands
|
|
56
|
+
// non-compiling code. On failure the write is refused (nothing touches disk).
|
|
57
|
+
// Only whole-file creates are checkable standalone (update ops are fragments).
|
|
58
|
+
let typecheckResult;
|
|
59
|
+
let effectiveWrite = write;
|
|
60
|
+
let result;
|
|
61
|
+
if (wantTypecheck) {
|
|
62
|
+
const dry = generate(template, { ...genOpts, write: false });
|
|
63
|
+
if (!dry.ok) {
|
|
64
|
+
printError(dry.error);
|
|
65
|
+
return 1;
|
|
66
|
+
}
|
|
67
|
+
const emitted = dry.value.plan.changes
|
|
68
|
+
.filter((c) => c.type === FileChangeType.Create)
|
|
69
|
+
.map((c) => ({
|
|
70
|
+
absPath: nodePath.resolve(inspection.projectRoot, c.relativePath),
|
|
71
|
+
contents: c.contents ?? '',
|
|
72
|
+
}))
|
|
73
|
+
.filter((f) => f.contents.length > 0 && /\.tsx?$/.test(f.absPath));
|
|
74
|
+
typecheckResult = typecheckEmittedFiles(inspection.projectRoot, emitted);
|
|
75
|
+
// Refuse to write non-compiling output — the whole point of the gate.
|
|
76
|
+
effectiveWrite = write && typecheckResult.errors.length === 0;
|
|
77
|
+
result = effectiveWrite ? generate(template, { ...genOpts, write: true }) : dry;
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
result = generate(template, { ...genOpts, write });
|
|
81
|
+
}
|
|
51
82
|
if (!result.ok) {
|
|
52
83
|
printError(result.error);
|
|
53
84
|
return 1;
|
|
54
85
|
}
|
|
55
86
|
const { plan, summary, written } = result.value;
|
|
87
|
+
const typecheckFailed = (typecheckResult?.errors.length ?? 0) > 0;
|
|
88
|
+
const writeRefused = write && typecheckFailed;
|
|
56
89
|
// --save-plan is allowed regardless of write/dry-run, but we refuse to
|
|
57
90
|
// save plans that have conflicts (since `shrk apply` would refuse them too).
|
|
58
91
|
if (savePlanPath) {
|
|
@@ -89,10 +122,12 @@ export const genCommand = {
|
|
|
89
122
|
summary,
|
|
90
123
|
written: written.map((w) => w.relativePath),
|
|
91
124
|
savedPlanPath: savePlanPath ?? null,
|
|
125
|
+
...(typecheckResult ? { typecheck: typecheckResult } : {}),
|
|
126
|
+
...(writeRefused ? { writeRefused: true } : {}),
|
|
92
127
|
}) + '\n');
|
|
93
|
-
return plan.hasConflicts ? 1 : 0;
|
|
128
|
+
return plan.hasConflicts || typecheckFailed ? 1 : 0;
|
|
94
129
|
}
|
|
95
|
-
process.stdout.write(header(
|
|
130
|
+
process.stdout.write(header(effectiveWrite ? `Generation: ${template.id}` : `Dry-run: ${template.id}`));
|
|
96
131
|
if (plan.warnings.length) {
|
|
97
132
|
process.stdout.write('Warnings:\n');
|
|
98
133
|
for (const w of plan.warnings)
|
|
@@ -118,7 +153,7 @@ export const genCommand = {
|
|
|
118
153
|
// agent can review what a template would generate WITHOUT writing to disk
|
|
119
154
|
// (the saved plan stays content-free). The bytes are identical in dry-run
|
|
120
155
|
// and --write; this is purely additive.
|
|
121
|
-
if (flagBool(args, 'show-content')) {
|
|
156
|
+
if (flagBool(args, 'show-content') || flagBool(args, 'print')) {
|
|
122
157
|
process.stdout.write('\nVirtual content (not written to disk):\n');
|
|
123
158
|
for (const change of plan.changes) {
|
|
124
159
|
const body = change.contents ?? '';
|
|
@@ -139,10 +174,31 @@ export const genCommand = {
|
|
|
139
174
|
if (updateLike.length > 0) {
|
|
140
175
|
process.stdout.write(`\nHUMAN REVIEW REQUIRED — ${updateLike.length} update entry/entries modify existing files.\n`);
|
|
141
176
|
}
|
|
177
|
+
if (typecheckResult) {
|
|
178
|
+
if (!typecheckResult.ran) {
|
|
179
|
+
process.stdout.write(`\nTypecheck: skipped — ${typecheckResult.note ?? 'nothing to check'}.\n`);
|
|
180
|
+
}
|
|
181
|
+
else if (typecheckResult.errors.length === 0) {
|
|
182
|
+
process.stdout.write('\nTypecheck: ✓ emitted files compile against the detected tsconfig.\n');
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
process.stdout.write(`\nTypecheck: ✗ ${typecheckResult.errors.length} error(s) in the emitted files — a template bug (NOT applied):\n`);
|
|
186
|
+
for (const e of typecheckResult.errors.slice(0, 30)) {
|
|
187
|
+
const rel = nodePath.relative(inspection.projectRoot, e.file) || e.file;
|
|
188
|
+
process.stdout.write(` ${rel}:${e.line}:${e.column} ${e.message}\n`);
|
|
189
|
+
}
|
|
190
|
+
if (typecheckResult.errors.length > 30) {
|
|
191
|
+
process.stdout.write(` … (${typecheckResult.errors.length - 30} more)\n`);
|
|
192
|
+
}
|
|
193
|
+
if (writeRefused) {
|
|
194
|
+
process.stdout.write('\nWrite REFUSED — the emitted files do not typecheck (nothing written). Fix the template, then re-run.\n');
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
142
198
|
process.stdout.write(`\nSummary: written=${summary.written}, skipped=${summary.skipped}, conflicts=${summary.conflicts}\n`);
|
|
143
|
-
if (!
|
|
199
|
+
if (!effectiveWrite && !savePlanPath && !writeRefused) {
|
|
144
200
|
process.stdout.write('\nRe-run with --write to apply, or --save-plan <file> + `shrk apply`.\n');
|
|
145
201
|
}
|
|
146
|
-
return plan.hasConflicts ? 1 : 0;
|
|
202
|
+
return plan.hasConflicts || typecheckFailed ? 1 : 0;
|
|
147
203
|
},
|
|
148
204
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph-code-subverbs.d.ts","sourceRoot":"","sources":["../../src/commands/graph-code-subverbs.ts"],"names":[],"mappings":"AA2BA,OAAO,EAAiE,KAAK,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAgLxH,wBAAsB,aAAa,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAkBrE;AA4FD,wBAAsB,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,
|
|
1
|
+
{"version":3,"file":"graph-code-subverbs.d.ts","sourceRoot":"","sources":["../../src/commands/graph-code-subverbs.ts"],"names":[],"mappings":"AA2BA,OAAO,EAAiE,KAAK,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAgLxH,wBAAsB,aAAa,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAkBrE;AA4FD,wBAAsB,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAkFtE;AAiBD,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CA2F1E;AAID,wBAAsB,YAAY,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CA+EpE;AAID,wBAAsB,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CA+FtE;AAID,wBAAsB,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAwEtE;AAID,wBAAsB,eAAe,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CA2MvE;AAID,wBAAsB,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CA+HtE;AAID;;;;;;GAMG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAgEpE;AAID,wBAAsB,eAAe,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAiGvE;AAyBD;;;;;;;;;;;;GAYG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAgHpE"}
|
|
@@ -285,7 +285,14 @@ export async function runGraphCycles(args) {
|
|
|
285
285
|
const api = loadGuarded(() => GraphQueryApi.fromStore(cwd), wantJson);
|
|
286
286
|
if (!api)
|
|
287
287
|
return 1;
|
|
288
|
-
|
|
288
|
+
// Type-only import edges (`import type`, `export type … from`) are erased at
|
|
289
|
+
// emit time, so they cannot cause a runtime cycle — excluded by default.
|
|
290
|
+
// `--include-type-edges` opts back in for auditing. The type-only-loop delta
|
|
291
|
+
// is reported as a non-blocking bucket.
|
|
292
|
+
const includeTypeEdges = flagBool(args, 'include-type-edges');
|
|
293
|
+
const allCycles = api.cycles({ includeTypeEdges });
|
|
294
|
+
const runtimeCycles = includeTypeEdges ? api.cycles() : allCycles;
|
|
295
|
+
const typeOnlyLoopCount = Math.max(0, api.cycles({ includeTypeEdges: true }).length - runtimeCycles.length);
|
|
289
296
|
const filtered = allCycles.filter((c) => c.size >= minSize);
|
|
290
297
|
const limited = filtered.slice(0, limit);
|
|
291
298
|
if (wantJson) {
|
|
@@ -293,6 +300,8 @@ export async function runGraphCycles(args) {
|
|
|
293
300
|
ok: true,
|
|
294
301
|
total: filtered.length,
|
|
295
302
|
truncated: filtered.length > limit,
|
|
303
|
+
includeTypeEdges,
|
|
304
|
+
typeOnlyLoopCount,
|
|
296
305
|
cycles: limited.map((c) => ({
|
|
297
306
|
size: c.size,
|
|
298
307
|
paths: c.paths ?? c.nodeIds.map((id) => id.replace(/^file:/, '')),
|
|
@@ -302,8 +311,11 @@ export async function runGraphCycles(args) {
|
|
|
302
311
|
}
|
|
303
312
|
process.stdout.write(header('Graph cycles'));
|
|
304
313
|
process.stdout.write(kv('total', String(filtered.length)) + '\n');
|
|
314
|
+
if (!includeTypeEdges && typeOnlyLoopCount > 0) {
|
|
315
|
+
process.stdout.write(kv('type-only loops', `${typeOnlyLoopCount} (excluded — compile-time only; --include-type-edges to audit)`) + '\n');
|
|
316
|
+
}
|
|
305
317
|
if (filtered.length === 0) {
|
|
306
|
-
process.stdout.write(
|
|
318
|
+
process.stdout.write(`\nNo ${includeTypeEdges ? '' : 'runtime '}cycles in the file-import graph. ✓\n`);
|
|
307
319
|
return 0;
|
|
308
320
|
}
|
|
309
321
|
process.stdout.write(kv('shown', `${limited.length}/${filtered.length}`) + '\n');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"policy-lint.command.d.ts","sourceRoot":"","sources":["../../src/commands/policy-lint.command.ts"],"names":[],"mappings":"AAIA,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAKhC,eAAO,MAAM,iBAAiB,EAAE,
|
|
1
|
+
{"version":3,"file":"policy-lint.command.d.ts","sourceRoot":"","sources":["../../src/commands/policy-lint.command.ts"],"names":[],"mappings":"AAIA,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAKhC,eAAO,MAAM,iBAAiB,EAAE,eA6L/B,CAAC"}
|
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
import * as nodePath from 'node:path';
|
|
2
2
|
import { runPolicyLint } from '@shrkcrft/boundaries';
|
|
3
|
-
import { resolveChangedFiles, resolveProjectConfig } from '@shrkcrft/inspector';
|
|
3
|
+
import { classifyChangedScope, resolveChangedFiles, resolveProjectConfig } from '@shrkcrft/inspector';
|
|
4
4
|
import { flagBool, flagString, resolveCwd, } from "../command-registry.js";
|
|
5
5
|
import { asJson, header, kv } from "../output/format-output.js";
|
|
6
6
|
const VALID_SURFACES = new Set(['template', 'style', 'ts']);
|
|
7
7
|
export const policyLintCommand = {
|
|
8
8
|
name: 'policy-lint',
|
|
9
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]',
|
|
10
|
+
usage: 'shrk [--cwd <dir>] policy-lint [--surface template|style|ts] [--changed-only] [--new-only] [--since <ref>] [--only <ids>] [--json]\n (--changed-only SCANS just the changed files; --new-only scans the whole tree but shows only findings the change introduced, hiding pre-existing baseline debt)',
|
|
11
11
|
async run(args) {
|
|
12
12
|
const cwd = resolveCwd(args);
|
|
13
13
|
const wantJson = flagBool(args, 'json');
|
|
14
14
|
const changedOnly = flagBool(args, 'changed-only');
|
|
15
|
+
// --new-only: scan the WHOLE tree, then show only findings the current change
|
|
16
|
+
// introduced (baseline debt is bucketed as hidden, not printed) — the
|
|
17
|
+
// finding-level complement to --changed-only's file-level scoping.
|
|
18
|
+
const newOnly = flagBool(args, 'new-only');
|
|
15
19
|
const since = flagString(args, 'since');
|
|
16
20
|
const only = flagString(args, 'only');
|
|
17
21
|
const surfaceRaw = flagString(args, 'surface');
|
|
@@ -60,11 +64,11 @@ export const policyLintCommand = {
|
|
|
60
64
|
}
|
|
61
65
|
}
|
|
62
66
|
let changedFiles;
|
|
63
|
-
if (changedOnly || since) {
|
|
67
|
+
if (changedOnly || newOnly || since) {
|
|
64
68
|
changedFiles = resolveChangedFiles({
|
|
65
69
|
projectRoot: cwd,
|
|
66
70
|
...(since ? { since } : {}),
|
|
67
|
-
...(changedOnly && !since ? { includeWorktree: true } : {}),
|
|
71
|
+
...((changedOnly || newOnly) && !since ? { includeWorktree: true } : {}),
|
|
68
72
|
}).files;
|
|
69
73
|
}
|
|
70
74
|
// Don't lint SharkCraft's own asset/config dir by default (its .ts files
|
|
@@ -73,17 +77,46 @@ export const policyLintCommand = {
|
|
|
73
77
|
const excludeDirs = sharkcraftRel && !sharkcraftRel.startsWith('..') ? [sharkcraftRel] : [];
|
|
74
78
|
const reportRaw = runPolicyLint(cwd, rules, {
|
|
75
79
|
...(surfaces ? { surfaces } : {}),
|
|
76
|
-
|
|
80
|
+
// --changed-only narrows the SCAN; --new-only scans the full tree so it can
|
|
81
|
+
// still diff findings (it filters after, below).
|
|
82
|
+
...((changedOnly || since) && !newOnly ? { changedOnly: true, changedFiles: changedFiles ?? [] } : {}),
|
|
77
83
|
...(only ? { only: only.split(',').map((s) => s.trim()).filter(Boolean) } : {}),
|
|
78
84
|
...(excludeDirs.length > 0 ? { excludeDirs } : {}),
|
|
79
85
|
});
|
|
80
86
|
// Surface pack-plane merge notes (missing/invalid pack policy files, dropped
|
|
81
87
|
// collisions) in the same diagnostics array the engine already emits.
|
|
82
|
-
|
|
88
|
+
let report = planeDiagnostics.length > 0
|
|
83
89
|
? { ...reportRaw, diagnostics: [...reportRaw.diagnostics, ...planeDiagnostics] }
|
|
84
90
|
: reportRaw;
|
|
91
|
+
// --new-only: partition the full-tree findings against the changed set and
|
|
92
|
+
// keep only the ones the current change introduced; pre-existing baseline
|
|
93
|
+
// debt is bucketed as hidden (reported as a count, never printed as green).
|
|
94
|
+
let hiddenBaseline = 0;
|
|
95
|
+
if (newOnly) {
|
|
96
|
+
const keyOf = (f) => `${f.ruleId}|${f.file}:${f.line}|${f.match}`;
|
|
97
|
+
const classification = classifyChangedScope({
|
|
98
|
+
projectRoot: cwd,
|
|
99
|
+
current: report.findings.map((f) => ({
|
|
100
|
+
key: keyOf(f),
|
|
101
|
+
file: f.file,
|
|
102
|
+
code: f.ruleId,
|
|
103
|
+
severity: f.severity,
|
|
104
|
+
message: f.message,
|
|
105
|
+
})),
|
|
106
|
+
changedFiles: changedFiles ?? [],
|
|
107
|
+
});
|
|
108
|
+
const newKeys = new Set(classification.newIssues.map((n) => n.key));
|
|
109
|
+
const newFindings = report.findings.filter((f) => newKeys.has(keyOf(f)));
|
|
110
|
+
hiddenBaseline = report.findings.length - newFindings.length;
|
|
111
|
+
const verdict = newFindings.some((f) => f.severity === 'error')
|
|
112
|
+
? 'errors'
|
|
113
|
+
: newFindings.length > 0
|
|
114
|
+
? report.verdict
|
|
115
|
+
: 'pass';
|
|
116
|
+
report = { ...report, findings: newFindings, verdict };
|
|
117
|
+
}
|
|
85
118
|
if (wantJson) {
|
|
86
|
-
process.stdout.write(asJson(report) + '\n');
|
|
119
|
+
process.stdout.write(asJson({ ...report, ...(newOnly ? { newOnly: true, hiddenBaseline } : {}) }) + '\n');
|
|
87
120
|
return report.verdict === 'errors' ? 1 : 0;
|
|
88
121
|
}
|
|
89
122
|
process.stdout.write(header('Policy lint'));
|
|
@@ -99,13 +132,18 @@ export const policyLintCommand = {
|
|
|
99
132
|
const errors = report.findings.filter((f) => f.severity === 'error').length;
|
|
100
133
|
const warnings = report.findings.filter((f) => f.severity === 'warning').length;
|
|
101
134
|
process.stdout.write(kv('findings', `${errors} error(s), ${warnings} warning(s)`) + '\n');
|
|
135
|
+
if (newOnly) {
|
|
136
|
+
process.stdout.write(kv('scope', `new-only (${hiddenBaseline} pre-existing finding(s) hidden — run without --new-only to see all)`) + '\n');
|
|
137
|
+
}
|
|
102
138
|
if (report.diagnostics.length > 0) {
|
|
103
139
|
process.stdout.write('\nMisconfigured rules:\n');
|
|
104
140
|
for (const d of report.diagnostics)
|
|
105
141
|
process.stdout.write(` ! ${d}\n`);
|
|
106
142
|
}
|
|
107
143
|
if (report.findings.length === 0 && report.diagnostics.length === 0) {
|
|
108
|
-
process.stdout.write(
|
|
144
|
+
process.stdout.write(newOnly
|
|
145
|
+
? `\nNo NEW policy violations from this change${hiddenBaseline > 0 ? ` (${hiddenBaseline} pre-existing hidden)` : ''}. ✓\n`
|
|
146
|
+
: '\nNo policy violations on the scanned surfaces. ✓\n');
|
|
109
147
|
return 0;
|
|
110
148
|
}
|
|
111
149
|
// Group findings by rule.
|
|
@@ -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":"AA4BA,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAGhC,eAAO,MAAM,wBAAwB,EAAE,eAsCtC,CAAC;AAsIF,eAAO,MAAM,eAAe,EAAE,eA4B7B,CAAC"}
|
|
@@ -4,30 +4,57 @@
|
|
|
4
4
|
* shrk registry lifecycle [--json] # register/remove symmetry
|
|
5
5
|
* shrk registry <name> list [--json] # every declared id
|
|
6
6
|
* shrk registry <name> exists <id> [--json] # is the id taken? (exit 1 if not)
|
|
7
|
+
* [--resolve] # map a synonym → canonical id first
|
|
8
|
+
* [--fail-if-taken] # guard: non-zero when taken (free → 0)
|
|
9
|
+
* [--fail-if-missing] # guard: non-zero when NOT registered
|
|
7
10
|
* shrk registry <name> where <id> [--json] # declaration (+ consumer) sites
|
|
8
11
|
*
|
|
9
12
|
* `<name>` resolves a `registries[]` declaration in sharkcraft.config.ts — one
|
|
10
13
|
* deterministic multi-root scan that answers "is this id taken / where is it"
|
|
11
14
|
* without an agent re-running a fragile grep.
|
|
12
15
|
*/
|
|
13
|
-
import { buildRegistryLifecycleReport, renderRegistryLifecycleReportText, resolveProjectConfig, } from '@shrkcrft/inspector';
|
|
16
|
+
import { buildRegistryLifecycleReport, renderRegistryLifecycleReportText, resolveChangedFiles, resolveProjectConfig, } from '@shrkcrft/inspector';
|
|
14
17
|
import { scanRegistry, registryExists, registryWhere, } from '@shrkcrft/boundaries';
|
|
15
18
|
import { flagBool, flagString, resolveCwd, } from "../command-registry.js";
|
|
16
19
|
import { asJson } from "../output/format-output.js";
|
|
17
20
|
export const registryLifecycleCommand = {
|
|
18
21
|
name: 'lifecycle',
|
|
19
22
|
description: 'Scan the workspace for register/remove symmetry. Read-only.',
|
|
20
|
-
usage: 'shrk registry lifecycle [--scope <dir>] [--json]',
|
|
23
|
+
usage: 'shrk registry lifecycle [--scope <dir>] [--changed-only] [--since <ref>] [--json]',
|
|
21
24
|
async run(args) {
|
|
22
25
|
const cwd = resolveCwd(args);
|
|
23
26
|
const scope = flagString(args, 'scope');
|
|
24
|
-
const
|
|
27
|
+
const changedOnly = flagBool(args, 'changed-only');
|
|
28
|
+
const since = flagString(args, 'since');
|
|
29
|
+
let files;
|
|
30
|
+
if (changedOnly || since) {
|
|
31
|
+
const changed = resolveChangedFiles({
|
|
32
|
+
projectRoot: cwd,
|
|
33
|
+
...(since ? { since } : {}),
|
|
34
|
+
...(changedOnly && !since ? { includeWorktree: true } : {}),
|
|
35
|
+
});
|
|
36
|
+
files = changed.files;
|
|
37
|
+
}
|
|
38
|
+
// Full-tree walk honors the project's skipDirs override.
|
|
39
|
+
let skipDirs;
|
|
40
|
+
if (files === undefined) {
|
|
41
|
+
const loaded = await resolveProjectConfig(cwd);
|
|
42
|
+
if (loaded.ok)
|
|
43
|
+
skipDirs = loaded.value.config.registryLifecycle?.skipDirs;
|
|
44
|
+
}
|
|
45
|
+
const report = buildRegistryLifecycleReport({
|
|
46
|
+
projectRoot: cwd,
|
|
47
|
+
...(files !== undefined ? { files } : {}),
|
|
48
|
+
...(scope ? { scope } : {}),
|
|
49
|
+
...(skipDirs ? { skipDirs } : {}),
|
|
50
|
+
});
|
|
51
|
+
const exit = report.timedOut ? 2 : report.missingRemovers.length === 0 ? 0 : 1;
|
|
25
52
|
if (flagBool(args, 'json')) {
|
|
26
53
|
process.stdout.write(asJson(report) + '\n');
|
|
27
|
-
return
|
|
54
|
+
return exit;
|
|
28
55
|
}
|
|
29
56
|
process.stdout.write(renderRegistryLifecycleReportText(report));
|
|
30
|
-
return
|
|
57
|
+
return exit;
|
|
31
58
|
},
|
|
32
59
|
};
|
|
33
60
|
async function loadRegistries(cwd) {
|
|
@@ -87,15 +114,42 @@ async function runRegistryInventory(args, name) {
|
|
|
87
114
|
}
|
|
88
115
|
if (action === 'exists') {
|
|
89
116
|
if (!id) {
|
|
90
|
-
process.stderr.write(`Usage: shrk registry ${name} exists <id
|
|
117
|
+
process.stderr.write(`Usage: shrk registry ${name} exists <id> [--resolve] [--fail-if-taken|--fail-if-missing]\n`);
|
|
91
118
|
return 2;
|
|
92
119
|
}
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
120
|
+
const failIfTaken = flagBool(args, 'fail-if-taken');
|
|
121
|
+
const failIfMissing = flagBool(args, 'fail-if-missing');
|
|
122
|
+
if (failIfTaken && failIfMissing) {
|
|
123
|
+
process.stderr.write('Pass at most one of --fail-if-taken / --fail-if-missing.\n');
|
|
124
|
+
return 2;
|
|
125
|
+
}
|
|
126
|
+
// `--resolve` maps a human noun to the canonical registered id via the
|
|
127
|
+
// registry's `aliases` map before the existence test — so a duplicate guard
|
|
128
|
+
// can't return a false "free" on a synonym of an already-taken slug.
|
|
129
|
+
const doResolve = flagBool(args, 'resolve');
|
|
130
|
+
const canonical = doResolve ? (decl.aliases?.[id] ?? id) : id;
|
|
131
|
+
const resolved = canonical !== id;
|
|
132
|
+
const exists = registryExists(inventory, canonical);
|
|
133
|
+
// Exit-code convention:
|
|
134
|
+
// --fail-if-taken → non-zero when the id is already registered (free → 0),
|
|
135
|
+
// so `exists <id> --fail-if-taken && <author>` is a natural guard.
|
|
136
|
+
// --fail-if-missing → non-zero when the id is NOT registered (the consume-side check).
|
|
137
|
+
// neither → the historical query convention (taken → 0, free → 1).
|
|
138
|
+
const code = failIfTaken ? (exists ? 1 : 0) : exists ? 0 : 1;
|
|
139
|
+
if (json) {
|
|
140
|
+
process.stdout.write(asJson({
|
|
141
|
+
name: inventory.name,
|
|
142
|
+
id,
|
|
143
|
+
...(resolved ? { resolvedId: canonical } : {}),
|
|
144
|
+
exists,
|
|
145
|
+
exitCode: code,
|
|
146
|
+
}) + '\n');
|
|
147
|
+
return code;
|
|
148
|
+
}
|
|
149
|
+
if (resolved)
|
|
150
|
+
process.stdout.write(`resolved "${id}" → "${canonical}" (alias)\n`);
|
|
151
|
+
process.stdout.write(`${exists ? 'yes' : 'no'} — "${canonical}" is ${exists ? 'declared' : 'NOT declared'} in registry "${inventory.name}".\n`);
|
|
152
|
+
return code;
|
|
99
153
|
}
|
|
100
154
|
if (action === 'where') {
|
|
101
155
|
if (!id) {
|
|
@@ -124,7 +178,11 @@ async function runRegistryInventory(args, name) {
|
|
|
124
178
|
export const registryCommand = {
|
|
125
179
|
name: 'registry',
|
|
126
180
|
description: 'Registry inspections: lifecycle symmetry + declared-registry inventory. Read-only.',
|
|
127
|
-
usage: 'shrk registry lifecycle | <name> list | <name> exists <id> | <name> where <id>',
|
|
181
|
+
usage: 'shrk registry lifecycle | <name> list | <name> exists <id> [--resolve] [--fail-if-taken|--fail-if-missing] | <name> where <id>',
|
|
182
|
+
// Guard-mode + query flags take no value — declare them so `exists <id>
|
|
183
|
+
// --fail-if-taken` (flag last) and `exists --resolve <id>` (flag first) both
|
|
184
|
+
// keep the id as a positional instead of swallowing it.
|
|
185
|
+
booleanFlags: new Set(['json', 'resolve', 'fail-if-taken', 'fail-if-missing']),
|
|
128
186
|
async run(args) {
|
|
129
187
|
const sub = args.positional[0];
|
|
130
188
|
if (sub === 'lifecycle') {
|
|
@@ -1 +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;
|
|
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;AA2GhC,eAAO,MAAM,YAAY,EAAE,eA0N1B,CAAC"}
|