@shrkcrft/structural-search 0.1.0-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/engine/apply-rewrite.d.ts +29 -0
  2. package/dist/engine/apply-rewrite.d.ts.map +1 -0
  3. package/dist/engine/apply-rewrite.js +68 -0
  4. package/dist/engine/match-pattern.d.ts +12 -0
  5. package/dist/engine/match-pattern.d.ts.map +1 -0
  6. package/dist/engine/match-pattern.js +150 -0
  7. package/dist/engine/plan-rewrite.d.ts +21 -0
  8. package/dist/engine/plan-rewrite.d.ts.map +1 -0
  9. package/dist/engine/plan-rewrite.js +187 -0
  10. package/dist/engine/run-search.d.ts +17 -0
  11. package/dist/engine/run-search.d.ts.map +1 -0
  12. package/dist/engine/run-search.js +157 -0
  13. package/dist/engine/sign-rewrite.d.ts +39 -0
  14. package/dist/engine/sign-rewrite.d.ts.map +1 -0
  15. package/dist/engine/sign-rewrite.js +87 -0
  16. package/dist/index.d.ts +13 -0
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/index.js +13 -0
  19. package/dist/registry/pattern-registry-store.d.ts +46 -0
  20. package/dist/registry/pattern-registry-store.d.ts.map +1 -0
  21. package/dist/registry/pattern-registry-store.js +120 -0
  22. package/dist/registry/starter-patterns.d.ts +12 -0
  23. package/dist/registry/starter-patterns.d.ts.map +1 -0
  24. package/dist/registry/starter-patterns.js +84 -0
  25. package/dist/schema/match.d.ts +25 -0
  26. package/dist/schema/match.d.ts.map +1 -0
  27. package/dist/schema/match.js +1 -0
  28. package/dist/schema/pattern-registry.d.ts +52 -0
  29. package/dist/schema/pattern-registry.d.ts.map +1 -0
  30. package/dist/schema/pattern-registry.js +60 -0
  31. package/dist/schema/pattern.d.ts +85 -0
  32. package/dist/schema/pattern.d.ts.map +1 -0
  33. package/dist/schema/pattern.js +22 -0
  34. package/dist/schema/rewrite.d.ts +58 -0
  35. package/dist/schema/rewrite.d.ts.map +1 -0
  36. package/dist/schema/rewrite.js +1 -0
  37. package/dist/schema/signed-rewrite.d.ts +20 -0
  38. package/dist/schema/signed-rewrite.d.ts.map +1 -0
  39. package/dist/schema/signed-rewrite.js +1 -0
  40. package/package.json +52 -0
@@ -0,0 +1,39 @@
1
+ import { type ISignedRewritePlan } from '../schema/signed-rewrite.js';
2
+ import type { IRewritePlan } from '../schema/rewrite.js';
3
+ /**
4
+ * Sign a rewrite plan with HMAC-SHA256.
5
+ *
6
+ * Canonicalisation: the plan + provenance are serialised with sorted
7
+ * keys (deep) before HMAC. This guarantees the signature matches when
8
+ * the plan is round-tripped through any tool that preserves the data.
9
+ *
10
+ * `secret` is required; callers can read it from a project config or
11
+ * environment (e.g. `SHRKCRFT_REWRITE_SECRET`). There's no built-in
12
+ * default — passing an empty secret throws to prevent accidentally
13
+ * "signing" with the empty string.
14
+ */
15
+ export declare function signRewritePlan(plan: IRewritePlan, options: {
16
+ secret: string;
17
+ signedBy?: string;
18
+ }): ISignedRewritePlan;
19
+ export interface IVerifySignedPlanResult {
20
+ ok: boolean;
21
+ /** Set when `ok` is false. Stable, machine-readable code. */
22
+ reason?: 'schema-mismatch' | 'algo-mismatch' | 'invalid-signature' | 'malformed-plan';
23
+ /** Human-readable detail (free-form). */
24
+ message?: string;
25
+ /** Computed HMAC, hex, for diagnostics. */
26
+ expectedHmac?: string;
27
+ }
28
+ /**
29
+ * Verify a signed rewrite plan. Constant-time HMAC comparison.
30
+ *
31
+ * Verification can fail for three reasons (each surfaced via
32
+ * `reason`): a different schema (forward-incompat), a different algo
33
+ * (different version of the signer), or a signature mismatch (wrong
34
+ * secret OR tampered plan).
35
+ */
36
+ export declare function verifySignedRewritePlan(signed: ISignedRewritePlan, options: {
37
+ secret: string;
38
+ }): IVerifySignedPlanResult;
39
+ //# sourceMappingURL=sign-rewrite.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sign-rewrite.d.ts","sourceRoot":"","sources":["../../src/engine/sign-rewrite.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,kBAAkB,EAExB,MAAM,6BAA6B,CAAC;AACrC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAEzD;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,YAAY,EAClB,OAAO,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAC7C,kBAAkB,CAiBpB;AAED,MAAM,WAAW,uBAAuB;IACtC,EAAE,EAAE,OAAO,CAAC;IACZ,6DAA6D;IAC7D,MAAM,CAAC,EACH,iBAAiB,GACjB,eAAe,GACf,mBAAmB,GACnB,gBAAgB,CAAC;IACrB,yCAAyC;IACzC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,2CAA2C;IAC3C,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,kBAAkB,EAC1B,OAAO,EAAE;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,GAC1B,uBAAuB,CAoBzB"}
@@ -0,0 +1,87 @@
1
+ import { createHmac } from 'node:crypto';
2
+ import { SIGNED_REWRITE_SCHEMA, } from "../schema/signed-rewrite.js";
3
+ /**
4
+ * Sign a rewrite plan with HMAC-SHA256.
5
+ *
6
+ * Canonicalisation: the plan + provenance are serialised with sorted
7
+ * keys (deep) before HMAC. This guarantees the signature matches when
8
+ * the plan is round-tripped through any tool that preserves the data.
9
+ *
10
+ * `secret` is required; callers can read it from a project config or
11
+ * environment (e.g. `SHRKCRFT_REWRITE_SECRET`). There's no built-in
12
+ * default — passing an empty secret throws to prevent accidentally
13
+ * "signing" with the empty string.
14
+ */
15
+ export function signRewritePlan(plan, options) {
16
+ if (!options.secret) {
17
+ throw new Error('signRewritePlan: secret is required');
18
+ }
19
+ const provenance = {
20
+ signedAt: new Date().toISOString(),
21
+ signedBy: options.signedBy ?? '@shrkcrft/structural-search',
22
+ planSchema: plan.schema,
23
+ };
24
+ const hmac = computeHmac(plan, provenance, options.secret);
25
+ return {
26
+ schema: SIGNED_REWRITE_SCHEMA,
27
+ algo: 'sha256',
28
+ hmac,
29
+ provenance,
30
+ plan,
31
+ };
32
+ }
33
+ /**
34
+ * Verify a signed rewrite plan. Constant-time HMAC comparison.
35
+ *
36
+ * Verification can fail for three reasons (each surfaced via
37
+ * `reason`): a different schema (forward-incompat), a different algo
38
+ * (different version of the signer), or a signature mismatch (wrong
39
+ * secret OR tampered plan).
40
+ */
41
+ export function verifySignedRewritePlan(signed, options) {
42
+ if (signed.schema !== SIGNED_REWRITE_SCHEMA) {
43
+ return { ok: false, reason: 'schema-mismatch', message: `unexpected schema: ${signed.schema}` };
44
+ }
45
+ if (signed.algo !== 'sha256') {
46
+ return { ok: false, reason: 'algo-mismatch', message: `unsupported algo: ${signed.algo}` };
47
+ }
48
+ if (!signed.plan || !signed.provenance) {
49
+ return { ok: false, reason: 'malformed-plan', message: 'missing plan or provenance' };
50
+ }
51
+ const expected = computeHmac(signed.plan, signed.provenance, options.secret);
52
+ if (!constantTimeEqualHex(expected, signed.hmac)) {
53
+ return {
54
+ ok: false,
55
+ reason: 'invalid-signature',
56
+ message: 'HMAC mismatch — wrong secret or tampered plan',
57
+ expectedHmac: expected,
58
+ };
59
+ }
60
+ return { ok: true };
61
+ }
62
+ function computeHmac(plan, provenance, secret) {
63
+ const canonical = canonicalJson({ plan, provenance });
64
+ return createHmac('sha256', secret).update(canonical).digest('hex');
65
+ }
66
+ function canonicalJson(value) {
67
+ return JSON.stringify(sortDeep(value));
68
+ }
69
+ function sortDeep(value) {
70
+ if (Array.isArray(value))
71
+ return value.map(sortDeep);
72
+ if (value === null || typeof value !== 'object')
73
+ return value;
74
+ const obj = value;
75
+ const sorted = {};
76
+ for (const key of Object.keys(obj).sort())
77
+ sorted[key] = sortDeep(obj[key]);
78
+ return sorted;
79
+ }
80
+ function constantTimeEqualHex(a, b) {
81
+ if (a.length !== b.length)
82
+ return false;
83
+ let acc = 0;
84
+ for (let i = 0; i < a.length; i += 1)
85
+ acc |= a.charCodeAt(i) ^ b.charCodeAt(i);
86
+ return acc === 0;
87
+ }
@@ -0,0 +1,13 @@
1
+ export * from './schema/pattern.js';
2
+ export * from './schema/pattern-registry.js';
3
+ export * from './schema/match.js';
4
+ export * from './schema/rewrite.js';
5
+ export * from './schema/signed-rewrite.js';
6
+ export * from './engine/match-pattern.js';
7
+ export * from './engine/run-search.js';
8
+ export * from './engine/plan-rewrite.js';
9
+ export * from './engine/apply-rewrite.js';
10
+ export * from './engine/sign-rewrite.js';
11
+ export * from './registry/pattern-registry-store.js';
12
+ export * from './registry/starter-patterns.js';
13
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,qBAAqB,CAAC;AACpC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,wBAAwB,CAAC;AACvC,cAAc,0BAA0B,CAAC;AACzC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,0BAA0B,CAAC;AACzC,cAAc,sCAAsC,CAAC;AACrD,cAAc,gCAAgC,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,13 @@
1
+ // Public surface of @shrkcrft/structural-search.
2
+ export * from "./schema/pattern.js";
3
+ export * from "./schema/pattern-registry.js";
4
+ export * from "./schema/match.js";
5
+ export * from "./schema/rewrite.js";
6
+ export * from "./schema/signed-rewrite.js";
7
+ export * from "./engine/match-pattern.js";
8
+ export * from "./engine/run-search.js";
9
+ export * from "./engine/plan-rewrite.js";
10
+ export * from "./engine/apply-rewrite.js";
11
+ export * from "./engine/sign-rewrite.js";
12
+ export * from "./registry/pattern-registry-store.js";
13
+ export * from "./registry/starter-patterns.js";
@@ -0,0 +1,46 @@
1
+ import type { IPatternEnvelope } from '../schema/pattern.js';
2
+ import { type IPatternRegistry, type IPatternValidationResult, type IRegisteredPattern } from '../schema/pattern-registry.js';
3
+ /**
4
+ * On-disk registry of structural-search patterns. Pattern authors call
5
+ * `add(envelope)` to register a reusable pattern by id; the doctor
6
+ * reads the list to surface freshness + validation state without
7
+ * recomputing matches against the codebase.
8
+ *
9
+ * The store deliberately validates the envelope at write time so a
10
+ * malformed pattern can never sneak past the boundary. Replaying via
11
+ * `validate()` is a no-op when every entry already has
12
+ * `lastValidatedAt` newer than the registry's own mtime.
13
+ */
14
+ export declare class PatternRegistryStore {
15
+ private readonly projectRoot;
16
+ readonly absPath: string;
17
+ constructor(projectRoot: string);
18
+ exists(): boolean;
19
+ read(): IPatternRegistry;
20
+ write(registry: IPatternRegistry): void;
21
+ /**
22
+ * Add or replace a pattern by id. Returns the validation result of
23
+ * the envelope; when invalid the registry is NOT modified.
24
+ */
25
+ add(envelope: IPatternEnvelope): {
26
+ result: IPatternValidationResult;
27
+ entry?: IRegisteredPattern;
28
+ };
29
+ remove(id: string): boolean;
30
+ clear(): boolean;
31
+ /**
32
+ * Re-validate every entry. Updates `lastValidatedAt` on success and
33
+ * `lastValidationError` on failure (preserving the entry in the
34
+ * registry — the user can decide whether to remove it). Returns the
35
+ * count of failed entries.
36
+ */
37
+ validateAll(): {
38
+ total: number;
39
+ failed: number;
40
+ errors: ReadonlyArray<{
41
+ id: string;
42
+ error: string;
43
+ }>;
44
+ };
45
+ }
46
+ //# sourceMappingURL=pattern-registry-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pattern-registry-store.d.ts","sourceRoot":"","sources":["../../src/registry/pattern-registry-store.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,OAAO,EAGL,KAAK,gBAAgB,EACrB,KAAK,wBAAwB,EAC7B,KAAK,kBAAkB,EACxB,MAAM,+BAA+B,CAAC;AAIvC;;;;;;;;;;GAUG;AACH,qBAAa,oBAAoB;IAGnB,OAAO,CAAC,QAAQ,CAAC,WAAW;IAFxC,SAAgB,OAAO,EAAE,MAAM,CAAC;gBAEH,WAAW,EAAE,MAAM;IAIhD,MAAM,IAAI,OAAO;IAIjB,IAAI,IAAI,gBAAgB;IAWxB,KAAK,CAAC,QAAQ,EAAE,gBAAgB,GAAG,IAAI;IAKvC;;;OAGG;IACH,GAAG,CAAC,QAAQ,EAAE,gBAAgB,GAAG;QAC/B,MAAM,EAAE,wBAAwB,CAAC;QACjC,KAAK,CAAC,EAAE,kBAAkB,CAAC;KAC5B;IAwBD,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAQ3B,KAAK,IAAI,OAAO;IAMhB;;;;;OAKG;IACH,WAAW,IAAI;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,aAAa,CAAC;YAAE,EAAE,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;KAAE;CA0BvG"}
@@ -0,0 +1,120 @@
1
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
2
+ import * as nodePath from 'node:path';
3
+ import { STRUCTURAL_PATTERN_REGISTRY_SCHEMA, validatePatternEnvelope, } from "../schema/pattern-registry.js";
4
+ const REGISTRY_REL = '.sharkcraft/structural/patterns.json';
5
+ /**
6
+ * On-disk registry of structural-search patterns. Pattern authors call
7
+ * `add(envelope)` to register a reusable pattern by id; the doctor
8
+ * reads the list to surface freshness + validation state without
9
+ * recomputing matches against the codebase.
10
+ *
11
+ * The store deliberately validates the envelope at write time so a
12
+ * malformed pattern can never sneak past the boundary. Replaying via
13
+ * `validate()` is a no-op when every entry already has
14
+ * `lastValidatedAt` newer than the registry's own mtime.
15
+ */
16
+ export class PatternRegistryStore {
17
+ projectRoot;
18
+ absPath;
19
+ constructor(projectRoot) {
20
+ this.projectRoot = projectRoot;
21
+ this.absPath = nodePath.join(projectRoot, REGISTRY_REL);
22
+ }
23
+ exists() {
24
+ return existsSync(this.absPath);
25
+ }
26
+ read() {
27
+ if (!this.exists())
28
+ return emptyRegistry();
29
+ try {
30
+ const raw = JSON.parse(readFileSync(this.absPath, 'utf8'));
31
+ if (raw.schema !== STRUCTURAL_PATTERN_REGISTRY_SCHEMA)
32
+ return emptyRegistry();
33
+ return raw;
34
+ }
35
+ catch {
36
+ return emptyRegistry();
37
+ }
38
+ }
39
+ write(registry) {
40
+ mkdirSync(nodePath.dirname(this.absPath), { recursive: true });
41
+ writeFileSync(this.absPath, JSON.stringify(registry, null, 2), 'utf8');
42
+ }
43
+ /**
44
+ * Add or replace a pattern by id. Returns the validation result of
45
+ * the envelope; when invalid the registry is NOT modified.
46
+ */
47
+ add(envelope) {
48
+ if (!envelope.id) {
49
+ return { result: { ok: false, error: 'pattern envelope is missing required `id`' } };
50
+ }
51
+ const result = validatePatternEnvelope(envelope);
52
+ if (!result.ok)
53
+ return { result };
54
+ const now = new Date().toISOString();
55
+ const entry = {
56
+ id: envelope.id,
57
+ ...(envelope.title ? { title: envelope.title } : {}),
58
+ ...(envelope.description ? { description: envelope.description } : {}),
59
+ pattern: envelope.pattern,
60
+ addedAt: now,
61
+ lastValidatedAt: now,
62
+ };
63
+ const reg = this.read();
64
+ const filtered = reg.patterns.filter((p) => p.id !== envelope.id);
65
+ this.write({
66
+ schema: STRUCTURAL_PATTERN_REGISTRY_SCHEMA,
67
+ patterns: [...filtered, entry].sort((a, b) => a.id.localeCompare(b.id)),
68
+ });
69
+ return { result, entry };
70
+ }
71
+ remove(id) {
72
+ const reg = this.read();
73
+ const next = reg.patterns.filter((p) => p.id !== id);
74
+ if (next.length === reg.patterns.length)
75
+ return false;
76
+ this.write({ schema: STRUCTURAL_PATTERN_REGISTRY_SCHEMA, patterns: next });
77
+ return true;
78
+ }
79
+ clear() {
80
+ if (!this.exists())
81
+ return false;
82
+ rmSync(this.absPath);
83
+ return true;
84
+ }
85
+ /**
86
+ * Re-validate every entry. Updates `lastValidatedAt` on success and
87
+ * `lastValidationError` on failure (preserving the entry in the
88
+ * registry — the user can decide whether to remove it). Returns the
89
+ * count of failed entries.
90
+ */
91
+ validateAll() {
92
+ const reg = this.read();
93
+ const failed = [];
94
+ const now = new Date().toISOString();
95
+ const next = reg.patterns.map((entry) => {
96
+ const envelope = {
97
+ schema: 'sharkcraft.structural-pattern/v1',
98
+ id: entry.id,
99
+ ...(entry.title ? { title: entry.title } : {}),
100
+ ...(entry.description ? { description: entry.description } : {}),
101
+ pattern: entry.pattern,
102
+ };
103
+ const result = validatePatternEnvelope(envelope);
104
+ if (result.ok) {
105
+ const cleaned = { ...entry, lastValidatedAt: now };
106
+ delete cleaned.lastValidationError;
107
+ return cleaned;
108
+ }
109
+ failed.push({ id: entry.id, error: result.error ?? 'invalid pattern' });
110
+ return { ...entry, lastValidationError: result.error ?? 'invalid pattern' };
111
+ });
112
+ if (reg.patterns.length > 0) {
113
+ this.write({ schema: STRUCTURAL_PATTERN_REGISTRY_SCHEMA, patterns: next });
114
+ }
115
+ return { total: reg.patterns.length, failed: failed.length, errors: failed };
116
+ }
117
+ }
118
+ function emptyRegistry() {
119
+ return { schema: STRUCTURAL_PATTERN_REGISTRY_SCHEMA, patterns: [] };
120
+ }
@@ -0,0 +1,12 @@
1
+ import type { IPatternEnvelope } from '../schema/pattern.js';
2
+ /**
3
+ * Curated starter pattern set. These are intentionally narrow and
4
+ * useful out of the box: they catch real DX issues that a clean repo
5
+ * should have zero of (console.log left in committed code,
6
+ * `@Controller()` decorators without a route argument, etc.).
7
+ *
8
+ * Used by `shrk search-structural registry seed` to populate a fresh
9
+ * registry. Authors are expected to grow / prune the set per project.
10
+ */
11
+ export declare const STARTER_PATTERNS: readonly IPatternEnvelope[];
12
+ //# sourceMappingURL=starter-patterns.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"starter-patterns.d.ts","sourceRoot":"","sources":["../../src/registry/starter-patterns.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAG7D;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB,EAAE,SAAS,gBAAgB,EAgFvD,CAAC"}
@@ -0,0 +1,84 @@
1
+ import { STRUCTURAL_PATTERN_SCHEMA } from "../schema/pattern.js";
2
+ /**
3
+ * Curated starter pattern set. These are intentionally narrow and
4
+ * useful out of the box: they catch real DX issues that a clean repo
5
+ * should have zero of (console.log left in committed code,
6
+ * `@Controller()` decorators without a route argument, etc.).
7
+ *
8
+ * Used by `shrk search-structural registry seed` to populate a fresh
9
+ * registry. Authors are expected to grow / prune the set per project.
10
+ */
11
+ export const STARTER_PATTERNS = [
12
+ {
13
+ schema: STRUCTURAL_PATTERN_SCHEMA,
14
+ id: 'starter.no-console-log',
15
+ title: 'No console.log calls',
16
+ description: 'Catches `console.log(...)` left in committed source. Pair with a rule that allows console.warn / console.error.',
17
+ pattern: {
18
+ kind: 'CallExpression',
19
+ callee: { kind: 'Identifier', nameRegex: '^log$' },
20
+ minArgs: 0,
21
+ },
22
+ },
23
+ {
24
+ schema: STRUCTURAL_PATTERN_SCHEMA,
25
+ id: 'starter.no-debugger',
26
+ title: 'No debugger calls',
27
+ description: 'Catches `debugger;` statements via a synthetic call shape match. False-positive-prone — use as a quick scan, not a hard gate.',
28
+ pattern: {
29
+ kind: 'Identifier',
30
+ name: 'debugger',
31
+ },
32
+ },
33
+ {
34
+ schema: STRUCTURAL_PATTERN_SCHEMA,
35
+ id: 'starter.nest.bare-controller',
36
+ title: 'NestJS @Controller() decorator without a route argument',
37
+ description: 'Flags `@Controller()` (no args), which mounts at the app root and is usually a typo. The shape match is callee-only — combine with `argCount: 0` to require zero args.',
38
+ pattern: {
39
+ kind: 'CallExpression',
40
+ callee: { kind: 'Identifier', name: 'Controller' },
41
+ argCount: 0,
42
+ },
43
+ },
44
+ {
45
+ schema: STRUCTURAL_PATTERN_SCHEMA,
46
+ id: 'starter.nest.injectable',
47
+ title: 'NestJS @Injectable() decorator usage',
48
+ description: 'Locates every `@Injectable()` call. Useful as a starting point for "find every provider in this app".',
49
+ pattern: {
50
+ kind: 'CallExpression',
51
+ callee: { kind: 'Identifier', name: 'Injectable' },
52
+ },
53
+ },
54
+ {
55
+ schema: STRUCTURAL_PATTERN_SCHEMA,
56
+ id: 'starter.react.unsafe-eval',
57
+ title: 'eval() call',
58
+ description: 'Catches `eval(...)` — almost always a security smell. Pair with a rule that forbids dynamic code in user-input paths.',
59
+ pattern: {
60
+ kind: 'CallExpression',
61
+ callee: { kind: 'Identifier', name: 'eval' },
62
+ },
63
+ },
64
+ {
65
+ schema: STRUCTURAL_PATTERN_SCHEMA,
66
+ id: 'starter.imports.dynamic-require',
67
+ title: 'Dynamic require() with a variable specifier',
68
+ description: 'Matches `require(...)` callsites. Useful in TypeScript-first repos that should be using `import` exclusively.',
69
+ pattern: {
70
+ kind: 'CallExpression',
71
+ callee: { kind: 'Identifier', name: 'require' },
72
+ },
73
+ },
74
+ {
75
+ schema: STRUCTURAL_PATTERN_SCHEMA,
76
+ id: 'starter.imports.from-internal',
77
+ title: 'Cross-package import from internal/ path',
78
+ description: 'Locates imports whose source path mentions `/internal/` — usually a private boundary another package should NOT cross. Combine with `shrk arch check` for the layered enforcement.',
79
+ pattern: {
80
+ kind: 'ImportDeclaration',
81
+ fromRegex: '/internal/',
82
+ },
83
+ },
84
+ ];
@@ -0,0 +1,25 @@
1
+ export interface IStructuralMatch {
2
+ /** Project-relative POSIX path. */
3
+ file: string;
4
+ /** 1-based line number of the matched node start. */
5
+ line: number;
6
+ /** 0-based column number of the matched node start. */
7
+ column: number;
8
+ /** AST node kind name (e.g. 'CallExpression'). */
9
+ nodeKind: string;
10
+ /** First ~140 chars of the matched text, single-lined. */
11
+ excerpt: string;
12
+ }
13
+ export interface IStructuralSearchResult {
14
+ schema: 'sharkcraft.structural-search/v1';
15
+ pattern: {
16
+ kind: string;
17
+ summary: string;
18
+ };
19
+ filesScanned: number;
20
+ matchCount: number;
21
+ truncated: boolean;
22
+ matches: readonly IStructuralMatch[];
23
+ diagnostics: readonly string[];
24
+ }
25
+ //# sourceMappingURL=match.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"match.d.ts","sourceRoot":"","sources":["../../src/schema/match.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,gBAAgB;IAC/B,mCAAmC;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,qDAAqD;IACrD,IAAI,EAAE,MAAM,CAAC;IACb,uDAAuD;IACvD,MAAM,EAAE,MAAM,CAAC;IACf,kDAAkD;IAClD,QAAQ,EAAE,MAAM,CAAC;IACjB,0DAA0D;IAC1D,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,iCAAiC,CAAC;IAC1C,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3C,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,SAAS,gBAAgB,EAAE,CAAC;IACrC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;CAChC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,52 @@
1
+ import { type IPatternEnvelope, type StructuralPattern } from './pattern.js';
2
+ export declare const STRUCTURAL_PATTERN_REGISTRY_SCHEMA: "sharkcraft.structural-pattern-registry/v1";
3
+ /**
4
+ * One entry in the on-disk pattern registry. Carries the full pattern
5
+ * envelope plus bookkeeping (added / validated timestamps + last error
6
+ * if any) so the doctor can surface decayed entries.
7
+ */
8
+ export interface IRegisteredPattern {
9
+ id: string;
10
+ title?: string;
11
+ description?: string;
12
+ pattern: StructuralPattern;
13
+ /** ISO timestamp the entry was added. */
14
+ addedAt: string;
15
+ /** ISO timestamp the entry was last validated successfully. */
16
+ lastValidatedAt?: string;
17
+ /** Last validation error message (set when validation failed). */
18
+ lastValidationError?: string;
19
+ }
20
+ /**
21
+ * On-disk pattern registry shape, persisted at
22
+ * `.sharkcraft/structural/patterns.json`. Lives alongside the pattern
23
+ * engine so packs can ship patterns and the doctor can check their
24
+ * health without a custom loader per pack.
25
+ */
26
+ export interface IPatternRegistry {
27
+ schema: typeof STRUCTURAL_PATTERN_REGISTRY_SCHEMA;
28
+ patterns: readonly IRegisteredPattern[];
29
+ }
30
+ /**
31
+ * Allowed `pattern.kind` strings. Hard-coded so we can validate
32
+ * entries without invoking the matcher itself — a registry entry with
33
+ * an unknown kind is rejected at registration time.
34
+ */
35
+ export declare const KNOWN_PATTERN_KINDS: Set<string>;
36
+ export interface IPatternValidationResult {
37
+ ok: boolean;
38
+ error?: string;
39
+ }
40
+ /**
41
+ * Light-weight envelope validation. Confirms:
42
+ * - schema field matches the canonical version,
43
+ * - pattern.kind is one of the known matcher kinds,
44
+ * - any regex field compiles.
45
+ *
46
+ * This is deliberately not a full structural typecheck — TypeScript
47
+ * already enforces the runtime fields. The goal is to catch
48
+ * pack-contributed JSON that fails at the boundary instead of at
49
+ * match time.
50
+ */
51
+ export declare function validatePatternEnvelope(envelope: IPatternEnvelope): IPatternValidationResult;
52
+ //# sourceMappingURL=pattern-registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pattern-registry.d.ts","sourceRoot":"","sources":["../../src/schema/pattern-registry.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACvB,MAAM,cAAc,CAAC;AAEtB,eAAO,MAAM,kCAAkC,EAC7C,2CAAoD,CAAC;AAEvD;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,iBAAiB,CAAC;IAC3B,yCAAyC;IACzC,OAAO,EAAE,MAAM,CAAC;IAChB,+DAA+D;IAC/D,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,kEAAkE;IAClE,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,OAAO,kCAAkC,CAAC;IAClD,QAAQ,EAAE,SAAS,kBAAkB,EAAE,CAAC;CACzC;AAED;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,aAQ9B,CAAC;AAEH,MAAM,WAAW,wBAAwB;IACvC,EAAE,EAAE,OAAO,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,gBAAgB,GACzB,wBAAwB,CA+B1B"}
@@ -0,0 +1,60 @@
1
+ import { STRUCTURAL_PATTERN_SCHEMA, } from "./pattern.js";
2
+ export const STRUCTURAL_PATTERN_REGISTRY_SCHEMA = 'sharkcraft.structural-pattern-registry/v1';
3
+ /**
4
+ * Allowed `pattern.kind` strings. Hard-coded so we can validate
5
+ * entries without invoking the matcher itself — a registry entry with
6
+ * an unknown kind is rejected at registration time.
7
+ */
8
+ export const KNOWN_PATTERN_KINDS = new Set([
9
+ 'Identifier',
10
+ 'StringLiteral',
11
+ 'CallExpression',
12
+ 'NewExpression',
13
+ 'ImportDeclaration',
14
+ 'ClassDeclaration',
15
+ 'Decorator',
16
+ ]);
17
+ /**
18
+ * Light-weight envelope validation. Confirms:
19
+ * - schema field matches the canonical version,
20
+ * - pattern.kind is one of the known matcher kinds,
21
+ * - any regex field compiles.
22
+ *
23
+ * This is deliberately not a full structural typecheck — TypeScript
24
+ * already enforces the runtime fields. The goal is to catch
25
+ * pack-contributed JSON that fails at the boundary instead of at
26
+ * match time.
27
+ */
28
+ export function validatePatternEnvelope(envelope) {
29
+ if (envelope.schema !== STRUCTURAL_PATTERN_SCHEMA) {
30
+ return {
31
+ ok: false,
32
+ error: `schema mismatch: got "${envelope.schema}", expected "${STRUCTURAL_PATTERN_SCHEMA}"`,
33
+ };
34
+ }
35
+ const p = envelope.pattern;
36
+ if (!p || typeof p.kind !== 'string') {
37
+ return { ok: false, error: 'pattern.kind is missing or not a string' };
38
+ }
39
+ if (!KNOWN_PATTERN_KINDS.has(p.kind)) {
40
+ return {
41
+ ok: false,
42
+ error: `pattern.kind "${p.kind}" is not a known matcher kind (one of ${[...KNOWN_PATTERN_KINDS].sort().join(', ')})`,
43
+ };
44
+ }
45
+ for (const key of ['nameRegex', 'textRegex', 'fromRegex']) {
46
+ const raw = p[key];
47
+ if (typeof raw === 'string' && raw.length > 0) {
48
+ try {
49
+ new RegExp(raw);
50
+ }
51
+ catch (e) {
52
+ return {
53
+ ok: false,
54
+ error: `pattern.${key} is not a valid regex: ${e.message}`,
55
+ };
56
+ }
57
+ }
58
+ }
59
+ return { ok: true };
60
+ }