@shrkcrft/cli 0.1.0-alpha.24 → 0.1.0-alpha.26
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 +10 -0
- package/dist/command-registry.d.ts.map +1 -1
- package/dist/command-registry.js +16 -0
- 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 +94 -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 +134 -19
- 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 +46 -2
- 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 +63 -3
- 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/graph.command.d.ts.map +1 -1
- package/dist/commands/graph.command.js +68 -1
- package/dist/commands/policy-lint.command.d.ts.map +1 -1
- package/dist/commands/policy-lint.command.js +46 -8
- package/dist/commands/registry-resolve.d.ts +41 -0
- package/dist/commands/registry-resolve.d.ts.map +1 -0
- package/dist/commands/registry-resolve.js +89 -0
- package/dist/commands/registry.command.d.ts.map +1 -1
- package/dist/commands/registry.command.js +86 -13
- package/dist/commands/reuse.command.d.ts +20 -0
- package/dist/commands/reuse.command.d.ts.map +1 -1
- package/dist/commands/reuse.command.js +156 -20
- 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/exit-codes.d.ts +49 -0
- package/dist/exit-codes.d.ts.map +1 -0
- package/dist/exit-codes.js +61 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +26 -1
- 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
|
@@ -1,8 +1,63 @@
|
|
|
1
1
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import * as nodePath from 'node:path';
|
|
3
3
|
import { analyzeImportGraph, buildKnowledgeGraph, findGraphPath, getGraphNode, inspectSharkcraft, } from '@shrkcrft/inspector';
|
|
4
|
-
import { flagBool, flagString, resolveCwd, } from "../command-registry.js";
|
|
4
|
+
import { firstUnknownFlag, flagBool, flagString, resolveCwd, } from "../command-registry.js";
|
|
5
|
+
import { ExitCode } from "../exit-codes.js";
|
|
5
6
|
import { asJson, header, kv } from "../output/format-output.js";
|
|
7
|
+
/**
|
|
8
|
+
* The code-intelligence subverbs that share one arg parser. Each is guarded so
|
|
9
|
+
* an unrecognized/misspelled flag is rejected loudly instead of parsing as a
|
|
10
|
+
* silent `true` that reads as a confident opt-in (a25 §2.1). The allow-set is
|
|
11
|
+
* the UNION of every flag any code-graph subverb reads, plus the global flags,
|
|
12
|
+
* so a real flag is never false-rejected — only a genuinely unknown token is.
|
|
13
|
+
*/
|
|
14
|
+
const CODE_GRAPH_SUBVERBS = new Set([
|
|
15
|
+
'index',
|
|
16
|
+
'status',
|
|
17
|
+
'search',
|
|
18
|
+
'context',
|
|
19
|
+
'impact',
|
|
20
|
+
'path',
|
|
21
|
+
'hubs',
|
|
22
|
+
'callers',
|
|
23
|
+
'cycles',
|
|
24
|
+
'unresolved',
|
|
25
|
+
'deps',
|
|
26
|
+
]);
|
|
27
|
+
const CODE_GRAPH_ALLOWED_FLAGS = new Set([
|
|
28
|
+
// code-subverb flags (union across index/status/search/context/impact/path/
|
|
29
|
+
// hubs/callers/cycles/unresolved/deps)
|
|
30
|
+
'changed',
|
|
31
|
+
'compact',
|
|
32
|
+
'depth',
|
|
33
|
+
'full',
|
|
34
|
+
'has-unresolved-imports',
|
|
35
|
+
'include-type-edges',
|
|
36
|
+
'json',
|
|
37
|
+
'kind',
|
|
38
|
+
'limit',
|
|
39
|
+
'max-depth',
|
|
40
|
+
'min-size',
|
|
41
|
+
'mode',
|
|
42
|
+
'no-bridge',
|
|
43
|
+
'no-framework',
|
|
44
|
+
'no-refresh',
|
|
45
|
+
'path',
|
|
46
|
+
'since',
|
|
47
|
+
'table',
|
|
48
|
+
// --watch loop (graph index --watch)
|
|
49
|
+
'watch',
|
|
50
|
+
'paths',
|
|
51
|
+
'debounce',
|
|
52
|
+
'once',
|
|
53
|
+
// global / meta flags that reach any verb
|
|
54
|
+
'cwd',
|
|
55
|
+
'strict',
|
|
56
|
+
'help',
|
|
57
|
+
'h',
|
|
58
|
+
'no-color',
|
|
59
|
+
'color',
|
|
60
|
+
]);
|
|
6
61
|
import { runGraphCallers, runGraphContext, runGraphCycles, runGraphDeps, runGraphHubs, runGraphImpact, runGraphIndex, runGraphPath, runGraphSearch, runGraphStatus, runGraphUnresolved, } from "./graph-code-subverbs.js";
|
|
7
62
|
const KNOWN_KINDS = [
|
|
8
63
|
'knowledge',
|
|
@@ -26,6 +81,18 @@ export const graphCommand = {
|
|
|
26
81
|
// Code-intelligence subverbs (R65) don't need the knowledge graph —
|
|
27
82
|
// dispatch them before the expensive inspection so they stay fast.
|
|
28
83
|
const earlySub = args.positional[0];
|
|
84
|
+
// Reject unknown/misspelled flags on the code-graph family BEFORE dispatch:
|
|
85
|
+
// an unrecognized flag must never parse as a silent success (a25 §2.1). Any
|
|
86
|
+
// real flag is in the union allow-set, so this only fires on a genuine typo.
|
|
87
|
+
if (typeof earlySub === 'string' && CODE_GRAPH_SUBVERBS.has(earlySub)) {
|
|
88
|
+
const unknown = firstUnknownFlag(args, CODE_GRAPH_ALLOWED_FLAGS);
|
|
89
|
+
if (unknown) {
|
|
90
|
+
const dash = unknown.length === 1 ? '-' : '--';
|
|
91
|
+
process.stderr.write(`unknown option '${dash}${unknown}' for 'shrk graph ${earlySub}'. ` +
|
|
92
|
+
`Run 'shrk graph --help' for valid flags.\n`);
|
|
93
|
+
return ExitCode.NotVerified;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
29
96
|
if (earlySub === 'index')
|
|
30
97
|
return runGraphIndex(args);
|
|
31
98
|
if (earlySub === 'status')
|
|
@@ -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.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `--resolve` support for `shrk registry <name> exists <id>`.
|
|
3
|
+
*
|
|
4
|
+
* The noun an author types is not always the exact registered slug. `--resolve`
|
|
5
|
+
* bridges that vocabulary gap before the existence check so a duplicate guard
|
|
6
|
+
* can't return a false "not declared" for a construct that DOES exist under its
|
|
7
|
+
* canonical id (a25 §2.4). Resolution is deterministic and layered, most
|
|
8
|
+
* author-controlled first:
|
|
9
|
+
*
|
|
10
|
+
* 1. the registry's declared `aliases` map (explicit synonym → canonical),
|
|
11
|
+
* 2. a case-insensitive exact match against the declared ids,
|
|
12
|
+
* 3. singular/plural normalization (`commands` ↔ `command`),
|
|
13
|
+
* 4. suffix strip/append (`button` ↔ `button-command`, `foo` ↔ `foo.tool`).
|
|
14
|
+
*
|
|
15
|
+
* The first layer that lands on a DECLARED id wins; when nothing resolves, the
|
|
16
|
+
* noun is returned unchanged and unmatched (the honest "genuinely not declared"
|
|
17
|
+
* answer). No layer invents an id that isn't in the registry.
|
|
18
|
+
*/
|
|
19
|
+
/** How a noun resolved to its canonical registered id (for a truthful report). */
|
|
20
|
+
export declare enum ERegistryResolveVia {
|
|
21
|
+
Identity = "identity",
|
|
22
|
+
Alias = "alias",
|
|
23
|
+
Case = "case-fold",
|
|
24
|
+
SingularPlural = "singular/plural",
|
|
25
|
+
Suffix = "suffix"
|
|
26
|
+
}
|
|
27
|
+
export interface IRegistryResolution {
|
|
28
|
+
/** The canonical id to test for existence (may equal the input noun). */
|
|
29
|
+
readonly canonical: string;
|
|
30
|
+
/** True when `canonical` is actually a declared id in the registry. */
|
|
31
|
+
readonly matched: boolean;
|
|
32
|
+
/** Which normalization layer produced `canonical`. */
|
|
33
|
+
readonly via: ERegistryResolveVia;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Resolve a human/synonym `noun` to a canonical registered id. `declaredIds` is
|
|
37
|
+
* the registry's actual declared id list (so no layer can resolve to something
|
|
38
|
+
* that isn't declared); `aliases` is the registry's optional declared map.
|
|
39
|
+
*/
|
|
40
|
+
export declare function resolveRegistryNoun(declaredIds: readonly string[], aliases: Readonly<Record<string, string>> | undefined, noun: string): IRegistryResolution;
|
|
41
|
+
//# sourceMappingURL=registry-resolve.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry-resolve.d.ts","sourceRoot":"","sources":["../../src/commands/registry-resolve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,kFAAkF;AAClF,oBAAY,mBAAmB;IAC7B,QAAQ,aAAa;IACrB,KAAK,UAAU;IACf,IAAI,cAAc;IAClB,cAAc,oBAAoB;IAClC,MAAM,WAAW;CAClB;AAED,MAAM,WAAW,mBAAmB;IAClC,yEAAyE;IACzE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,uEAAuE;IACvE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,sDAAsD;IACtD,QAAQ,CAAC,GAAG,EAAE,mBAAmB,CAAC;CACnC;AAYD;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,SAAS,MAAM,EAAE,EAC9B,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,SAAS,EACrD,IAAI,EAAE,MAAM,GACX,mBAAmB,CAmDrB"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `--resolve` support for `shrk registry <name> exists <id>`.
|
|
3
|
+
*
|
|
4
|
+
* The noun an author types is not always the exact registered slug. `--resolve`
|
|
5
|
+
* bridges that vocabulary gap before the existence check so a duplicate guard
|
|
6
|
+
* can't return a false "not declared" for a construct that DOES exist under its
|
|
7
|
+
* canonical id (a25 §2.4). Resolution is deterministic and layered, most
|
|
8
|
+
* author-controlled first:
|
|
9
|
+
*
|
|
10
|
+
* 1. the registry's declared `aliases` map (explicit synonym → canonical),
|
|
11
|
+
* 2. a case-insensitive exact match against the declared ids,
|
|
12
|
+
* 3. singular/plural normalization (`commands` ↔ `command`),
|
|
13
|
+
* 4. suffix strip/append (`button` ↔ `button-command`, `foo` ↔ `foo.tool`).
|
|
14
|
+
*
|
|
15
|
+
* The first layer that lands on a DECLARED id wins; when nothing resolves, the
|
|
16
|
+
* noun is returned unchanged and unmatched (the honest "genuinely not declared"
|
|
17
|
+
* answer). No layer invents an id that isn't in the registry.
|
|
18
|
+
*/
|
|
19
|
+
/** How a noun resolved to its canonical registered id (for a truthful report). */
|
|
20
|
+
export var ERegistryResolveVia;
|
|
21
|
+
(function (ERegistryResolveVia) {
|
|
22
|
+
ERegistryResolveVia["Identity"] = "identity";
|
|
23
|
+
ERegistryResolveVia["Alias"] = "alias";
|
|
24
|
+
ERegistryResolveVia["Case"] = "case-fold";
|
|
25
|
+
ERegistryResolveVia["SingularPlural"] = "singular/plural";
|
|
26
|
+
ERegistryResolveVia["Suffix"] = "suffix";
|
|
27
|
+
})(ERegistryResolveVia || (ERegistryResolveVia = {}));
|
|
28
|
+
function singularPluralVariants(noun) {
|
|
29
|
+
const out = [];
|
|
30
|
+
const lower = noun.toLowerCase();
|
|
31
|
+
if (lower.endsWith('ies'))
|
|
32
|
+
out.push(noun.slice(0, -3) + 'y');
|
|
33
|
+
if (lower.endsWith('s'))
|
|
34
|
+
out.push(noun.slice(0, -1));
|
|
35
|
+
if (lower.endsWith('y'))
|
|
36
|
+
out.push(noun.slice(0, -1) + 'ies');
|
|
37
|
+
out.push(noun + 's');
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Resolve a human/synonym `noun` to a canonical registered id. `declaredIds` is
|
|
42
|
+
* the registry's actual declared id list (so no layer can resolve to something
|
|
43
|
+
* that isn't declared); `aliases` is the registry's optional declared map.
|
|
44
|
+
*/
|
|
45
|
+
export function resolveRegistryNoun(declaredIds, aliases, noun) {
|
|
46
|
+
const idSet = new Set(declaredIds);
|
|
47
|
+
// 1. Author-declared alias map — highest priority, honored even if it points
|
|
48
|
+
// at an id the current scan didn't find (the author asserted the mapping).
|
|
49
|
+
const aliased = aliases?.[noun];
|
|
50
|
+
if (aliased !== undefined) {
|
|
51
|
+
return { canonical: aliased, matched: idSet.has(aliased), via: ERegistryResolveVia.Alias };
|
|
52
|
+
}
|
|
53
|
+
// Exact identity.
|
|
54
|
+
if (idSet.has(noun)) {
|
|
55
|
+
return { canonical: noun, matched: true, via: ERegistryResolveVia.Identity };
|
|
56
|
+
}
|
|
57
|
+
const lower = noun.toLowerCase();
|
|
58
|
+
// 2. Case-insensitive exact.
|
|
59
|
+
const ciHit = declaredIds.find((d) => d.toLowerCase() === lower);
|
|
60
|
+
if (ciHit !== undefined) {
|
|
61
|
+
return { canonical: ciHit, matched: true, via: ERegistryResolveVia.Case };
|
|
62
|
+
}
|
|
63
|
+
// 3. Singular/plural.
|
|
64
|
+
for (const variant of singularPluralVariants(noun)) {
|
|
65
|
+
const v = variant.toLowerCase();
|
|
66
|
+
const hit = declaredIds.find((d) => d.toLowerCase() === v);
|
|
67
|
+
if (hit !== undefined) {
|
|
68
|
+
return { canonical: hit, matched: true, via: ERegistryResolveVia.SingularPlural };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// 4. Suffix strip/append. A declared id whose trailing `-`/`.`/`_` segment,
|
|
72
|
+
// once stripped, equals the noun (`button` ← `button-command`); or the noun
|
|
73
|
+
// plus a suffix another declared id carries yields a declared id.
|
|
74
|
+
const suffixStripped = declaredIds.find((d) => {
|
|
75
|
+
const stripped = d.replace(/[-_.][a-z0-9]+$/i, '');
|
|
76
|
+
return stripped.toLowerCase() === lower && stripped.toLowerCase() !== d.toLowerCase();
|
|
77
|
+
});
|
|
78
|
+
if (suffixStripped !== undefined) {
|
|
79
|
+
return { canonical: suffixStripped, matched: true, via: ERegistryResolveVia.Suffix };
|
|
80
|
+
}
|
|
81
|
+
for (const d of declaredIds) {
|
|
82
|
+
const m = d.match(/([-_.][a-z0-9]+)$/i);
|
|
83
|
+
if (m && `${lower}${m[1].toLowerCase()}` === d.toLowerCase()) {
|
|
84
|
+
return { canonical: d, matched: true, via: ERegistryResolveVia.Suffix };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Nothing resolved — the honest unmatched identity.
|
|
88
|
+
return { canonical: noun, matched: false, via: ERegistryResolveVia.Identity };
|
|
89
|
+
}
|
|
@@ -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;AAKhC,eAAO,MAAM,wBAAwB,EAAE,eA8CtC,CAAC;AA6IF,eAAO,MAAM,eAAe,EAAE,eA4B7B,CAAC"}
|
|
@@ -4,30 +4,66 @@
|
|
|
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";
|
|
19
|
+
import { ExitCode } from "../exit-codes.js";
|
|
16
20
|
import { asJson } from "../output/format-output.js";
|
|
21
|
+
import { resolveRegistryNoun } from "./registry-resolve.js";
|
|
17
22
|
export const registryLifecycleCommand = {
|
|
18
23
|
name: 'lifecycle',
|
|
19
24
|
description: 'Scan the workspace for register/remove symmetry. Read-only.',
|
|
20
|
-
usage: 'shrk registry lifecycle [--scope <dir>] [--json]',
|
|
25
|
+
usage: 'shrk registry lifecycle [--scope <dir>] [--changed-only] [--since <ref>] [--json]',
|
|
21
26
|
async run(args) {
|
|
22
27
|
const cwd = resolveCwd(args);
|
|
23
28
|
const scope = flagString(args, 'scope');
|
|
24
|
-
const
|
|
29
|
+
const changedOnly = flagBool(args, 'changed-only');
|
|
30
|
+
const since = flagString(args, 'since');
|
|
31
|
+
let files;
|
|
32
|
+
if (changedOnly || since) {
|
|
33
|
+
const changed = resolveChangedFiles({
|
|
34
|
+
projectRoot: cwd,
|
|
35
|
+
...(since ? { since } : {}),
|
|
36
|
+
...(changedOnly && !since ? { includeWorktree: true } : {}),
|
|
37
|
+
});
|
|
38
|
+
files = changed.files;
|
|
39
|
+
}
|
|
40
|
+
// Full-tree walk honors the project's skipDirs override.
|
|
41
|
+
let skipDirs;
|
|
42
|
+
if (files === undefined) {
|
|
43
|
+
const loaded = await resolveProjectConfig(cwd);
|
|
44
|
+
if (loaded.ok)
|
|
45
|
+
skipDirs = loaded.value.config.registryLifecycle?.skipDirs;
|
|
46
|
+
}
|
|
47
|
+
const report = buildRegistryLifecycleReport({
|
|
48
|
+
projectRoot: cwd,
|
|
49
|
+
...(files !== undefined ? { files } : {}),
|
|
50
|
+
...(scope ? { scope } : {}),
|
|
51
|
+
...(skipDirs ? { skipDirs } : {}),
|
|
52
|
+
});
|
|
53
|
+
// Honest exit-code contract (a25 §1 / §2.5): a wedged/timed-out scan OR a
|
|
54
|
+
// scope with zero registrations to check is NOT verified (`2`), never a
|
|
55
|
+
// green `0` an agent's `&& next` would march past.
|
|
56
|
+
const exit = report.timedOut || report.registersFound === 0
|
|
57
|
+
? ExitCode.NotVerified
|
|
58
|
+
: report.missingRemovers.length === 0
|
|
59
|
+
? ExitCode.VerifiedPass
|
|
60
|
+
: ExitCode.Failure;
|
|
25
61
|
if (flagBool(args, 'json')) {
|
|
26
62
|
process.stdout.write(asJson(report) + '\n');
|
|
27
|
-
return
|
|
63
|
+
return exit;
|
|
28
64
|
}
|
|
29
65
|
process.stdout.write(renderRegistryLifecycleReportText(report));
|
|
30
|
-
return
|
|
66
|
+
return exit;
|
|
31
67
|
},
|
|
32
68
|
};
|
|
33
69
|
async function loadRegistries(cwd) {
|
|
@@ -87,15 +123,48 @@ async function runRegistryInventory(args, name) {
|
|
|
87
123
|
}
|
|
88
124
|
if (action === 'exists') {
|
|
89
125
|
if (!id) {
|
|
90
|
-
process.stderr.write(`Usage: shrk registry ${name} exists <id
|
|
126
|
+
process.stderr.write(`Usage: shrk registry ${name} exists <id> [--resolve] [--fail-if-taken|--fail-if-missing]\n`);
|
|
91
127
|
return 2;
|
|
92
128
|
}
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
129
|
+
const failIfTaken = flagBool(args, 'fail-if-taken');
|
|
130
|
+
const failIfMissing = flagBool(args, 'fail-if-missing');
|
|
131
|
+
if (failIfTaken && failIfMissing) {
|
|
132
|
+
process.stderr.write('Pass at most one of --fail-if-taken / --fail-if-missing.\n');
|
|
133
|
+
return 2;
|
|
134
|
+
}
|
|
135
|
+
// `--resolve` maps a human noun to the canonical registered id before the
|
|
136
|
+
// existence test — via the registry's declared `aliases` map AND generic
|
|
137
|
+
// normalization (case-fold, singular/plural, suffix strip/append) — so a
|
|
138
|
+
// duplicate guard can't return a false "free" on a synonym of an
|
|
139
|
+
// already-taken slug (a25 §2.4).
|
|
140
|
+
const doResolve = flagBool(args, 'resolve');
|
|
141
|
+
const resolution = doResolve
|
|
142
|
+
? resolveRegistryNoun(inventory.entries.map((e) => e.id), decl.aliases, id)
|
|
143
|
+
: undefined;
|
|
144
|
+
const canonical = resolution ? resolution.canonical : id;
|
|
145
|
+
const resolved = canonical !== id;
|
|
146
|
+
const exists = registryExists(inventory, canonical);
|
|
147
|
+
// Exit-code convention:
|
|
148
|
+
// --fail-if-taken → non-zero when the id is already registered (free → 0),
|
|
149
|
+
// so `exists <id> --fail-if-taken && <author>` is a natural guard.
|
|
150
|
+
// --fail-if-missing → non-zero when the id is NOT registered (the consume-side check).
|
|
151
|
+
// neither → the historical query convention (taken → 0, free → 1).
|
|
152
|
+
const code = failIfTaken ? (exists ? 1 : 0) : exists ? 0 : 1;
|
|
153
|
+
if (json) {
|
|
154
|
+
process.stdout.write(asJson({
|
|
155
|
+
name: inventory.name,
|
|
156
|
+
id,
|
|
157
|
+
...(resolved ? { resolvedId: canonical, resolvedVia: resolution?.via } : {}),
|
|
158
|
+
exists,
|
|
159
|
+
exitCode: code,
|
|
160
|
+
}) + '\n');
|
|
161
|
+
return code;
|
|
162
|
+
}
|
|
163
|
+
if (resolved) {
|
|
164
|
+
process.stdout.write(`resolved "${id}" → "${canonical}" (${resolution?.via})\n`);
|
|
165
|
+
}
|
|
166
|
+
process.stdout.write(`${exists ? 'yes' : 'no'} — "${canonical}" is ${exists ? 'declared' : 'NOT declared'} in registry "${inventory.name}".\n`);
|
|
167
|
+
return code;
|
|
99
168
|
}
|
|
100
169
|
if (action === 'where') {
|
|
101
170
|
if (!id) {
|
|
@@ -124,7 +193,11 @@ async function runRegistryInventory(args, name) {
|
|
|
124
193
|
export const registryCommand = {
|
|
125
194
|
name: 'registry',
|
|
126
195
|
description: 'Registry inspections: lifecycle symmetry + declared-registry inventory. Read-only.',
|
|
127
|
-
usage: 'shrk registry lifecycle | <name> list | <name> exists <id> | <name> where <id>',
|
|
196
|
+
usage: 'shrk registry lifecycle | <name> list | <name> exists <id> [--resolve] [--fail-if-taken|--fail-if-missing] | <name> where <id>',
|
|
197
|
+
// Guard-mode + query flags take no value — declare them so `exists <id>
|
|
198
|
+
// --fail-if-taken` (flag last) and `exists --resolve <id>` (flag first) both
|
|
199
|
+
// keep the id as a positional instead of swallowing it.
|
|
200
|
+
booleanFlags: new Set(['json', 'resolve', 'fail-if-taken', 'fail-if-missing']),
|
|
128
201
|
async run(args) {
|
|
129
202
|
const sub = args.positional[0];
|
|
130
203
|
if (sub === 'lifecycle') {
|
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
import type { IReusePrimitive } from '@shrkcrft/core';
|
|
1
2
|
import { type ICommandHandler } from '../command-registry.js';
|
|
3
|
+
/** A weak (below-confidence) candidate offered as a did-you-mean, never as an answer. */
|
|
4
|
+
interface IReuseSuggestion {
|
|
5
|
+
symbol: string;
|
|
6
|
+
score: number;
|
|
7
|
+
confidence: number;
|
|
8
|
+
matched: readonly string[];
|
|
9
|
+
roles: readonly string[];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Rank ALL primitives by the matcher's own score (descending; ties broken by
|
|
13
|
+
* symbol name so the order is deterministic), then return the top-`k` as scored
|
|
14
|
+
* did-you-mean suggestions. Pure — no graph, no IO — so it is directly
|
|
15
|
+
* unit-testable. When every candidate scores 0 (a nonsense intent that shares no
|
|
16
|
+
* term) the result is the alphabetically-first `k` primitives, each with
|
|
17
|
+
* `score: 0`; the caller states "no candidate shares any term" in that case
|
|
18
|
+
* rather than dumping the whole catalog.
|
|
19
|
+
*/
|
|
20
|
+
export declare function rankReuseSuggestions(primitives: readonly IReusePrimitive[], tokens: readonly string[], k: number): IReuseSuggestion[];
|
|
2
21
|
export declare const reuseCommand: ICommandHandler;
|
|
22
|
+
export {};
|
|
3
23
|
//# sourceMappingURL=reuse.command.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reuse.command.d.ts","sourceRoot":"","sources":["../../src/commands/reuse.command.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"reuse.command.d.ts","sourceRoot":"","sources":["../../src/commands/reuse.command.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACtD,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAkGhC,yFAAyF;AACzF,UAAU,gBAAgB;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1B;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAClC,UAAU,EAAE,SAAS,eAAe,EAAE,EACtC,MAAM,EAAE,SAAS,MAAM,EAAE,EACzB,CAAC,EAAE,MAAM,GACR,gBAAgB,EAAE,CAapB;AAED,eAAO,MAAM,YAAY,EAAE,eA6Q1B,CAAC"}
|