@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.
- package/dist/engine/apply-rewrite.d.ts +29 -0
- package/dist/engine/apply-rewrite.d.ts.map +1 -0
- package/dist/engine/apply-rewrite.js +68 -0
- package/dist/engine/match-pattern.d.ts +12 -0
- package/dist/engine/match-pattern.d.ts.map +1 -0
- package/dist/engine/match-pattern.js +150 -0
- package/dist/engine/plan-rewrite.d.ts +21 -0
- package/dist/engine/plan-rewrite.d.ts.map +1 -0
- package/dist/engine/plan-rewrite.js +187 -0
- package/dist/engine/run-search.d.ts +17 -0
- package/dist/engine/run-search.d.ts.map +1 -0
- package/dist/engine/run-search.js +157 -0
- package/dist/engine/sign-rewrite.d.ts +39 -0
- package/dist/engine/sign-rewrite.d.ts.map +1 -0
- package/dist/engine/sign-rewrite.js +87 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/registry/pattern-registry-store.d.ts +46 -0
- package/dist/registry/pattern-registry-store.d.ts.map +1 -0
- package/dist/registry/pattern-registry-store.js +120 -0
- package/dist/registry/starter-patterns.d.ts +12 -0
- package/dist/registry/starter-patterns.d.ts.map +1 -0
- package/dist/registry/starter-patterns.js +84 -0
- package/dist/schema/match.d.ts +25 -0
- package/dist/schema/match.d.ts.map +1 -0
- package/dist/schema/match.js +1 -0
- package/dist/schema/pattern-registry.d.ts +52 -0
- package/dist/schema/pattern-registry.d.ts.map +1 -0
- package/dist/schema/pattern-registry.js +60 -0
- package/dist/schema/pattern.d.ts +85 -0
- package/dist/schema/pattern.d.ts.map +1 -0
- package/dist/schema/pattern.js +22 -0
- package/dist/schema/rewrite.d.ts +58 -0
- package/dist/schema/rewrite.d.ts.map +1 -0
- package/dist/schema/rewrite.js +1 -0
- package/dist/schema/signed-rewrite.d.ts +20 -0
- package/dist/schema/signed-rewrite.d.ts.map +1 -0
- package/dist/schema/signed-rewrite.js +1 -0
- 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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|