@shrkcrft/presets 0.1.0-alpha.1
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/LICENSE +21 -0
- package/README.md +15 -0
- package/dist/apply/preview-apply.d.ts +52 -0
- package/dist/apply/preview-apply.d.ts.map +1 -0
- package/dist/apply/preview-apply.js +85 -0
- package/dist/builtin/builtin-presets.d.ts +3 -0
- package/dist/builtin/builtin-presets.d.ts.map +1 -0
- package/dist/builtin/builtin-presets.js +520 -0
- package/dist/builtin/r26-presets.d.ts +31 -0
- package/dist/builtin/r26-presets.d.ts.map +1 -0
- package/dist/builtin/r26-presets.js +458 -0
- package/dist/builtin/r26-snippets.d.ts +39 -0
- package/dist/builtin/r26-snippets.d.ts.map +1 -0
- package/dist/builtin/r26-snippets.js +257 -0
- package/dist/builtin/r45-presets.d.ts +7 -0
- package/dist/builtin/r45-presets.d.ts.map +1 -0
- package/dist/builtin/r45-presets.js +186 -0
- package/dist/builtin/r47-presets.d.ts +5 -0
- package/dist/builtin/r47-presets.d.ts.map +1 -0
- package/dist/builtin/r47-presets.js +65 -0
- package/dist/builtin/shared-snippets.d.ts +17 -0
- package/dist/builtin/shared-snippets.d.ts.map +1 -0
- package/dist/builtin/shared-snippets.js +264 -0
- package/dist/define/define-preset.d.ts +4 -0
- package/dist/define/define-preset.d.ts.map +1 -0
- package/dist/define/define-preset.js +4 -0
- package/dist/emit/synthesize-files.d.ts +26 -0
- package/dist/emit/synthesize-files.d.ts.map +1 -0
- package/dist/emit/synthesize-files.js +172 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/model/preset.d.ts +83 -0
- package/dist/model/preset.d.ts.map +1 -0
- package/dist/model/preset.js +21 -0
- package/dist/registry/load-presets.d.ts +12 -0
- package/dist/registry/load-presets.d.ts.map +1 -0
- package/dist/registry/load-presets.js +33 -0
- package/dist/registry/preset-registry.d.ts +11 -0
- package/dist/registry/preset-registry.d.ts.map +1 -0
- package/dist/registry/preset-registry.js +22 -0
- package/dist/registry/recommend.d.ts +30 -0
- package/dist/registry/recommend.d.ts.map +1 -0
- package/dist/registry/recommend.js +59 -0
- package/dist/registry/resolve-preset.d.ts +75 -0
- package/dist/registry/resolve-preset.d.ts.map +1 -0
- package/dist/registry/resolve-preset.js +207 -0
- package/dist/registry/resolve-references.d.ts +49 -0
- package/dist/registry/resolve-references.d.ts.map +1 -0
- package/dist/registry/resolve-references.js +38 -0
- package/package.json +51 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { safeImport } from '@shrkcrft/core';
|
|
3
|
+
import { validatePreset } from "../model/preset.js";
|
|
4
|
+
export async function loadPresetsFromFile(absPath, options = {}) {
|
|
5
|
+
const out = { source: absPath, presets: [], warnings: [] };
|
|
6
|
+
if (!existsSync(absPath)) {
|
|
7
|
+
out.warnings.push(`preset file not found: ${absPath}`);
|
|
8
|
+
return out;
|
|
9
|
+
}
|
|
10
|
+
const result = options.importContext
|
|
11
|
+
? await options.importContext.load(absPath)
|
|
12
|
+
: await safeImport(absPath, { skipExistsCheck: true });
|
|
13
|
+
if (!result.ok) {
|
|
14
|
+
const label = result.timedOut ? 'timed out loading presets from' : 'failed to load presets from';
|
|
15
|
+
out.warnings.push(`${label} ${absPath}: ${result.error.message}`);
|
|
16
|
+
return out;
|
|
17
|
+
}
|
|
18
|
+
const candidates = pickArray(result.module.default) ?? pickArray(result.module.presets) ?? [];
|
|
19
|
+
for (const candidate of candidates) {
|
|
20
|
+
const v = validatePreset(candidate);
|
|
21
|
+
if (!v.valid) {
|
|
22
|
+
out.warnings.push(`${absPath}: skipping invalid preset (${v.issues.map((i) => i.field).join(', ')})`);
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
out.presets.push(candidate);
|
|
26
|
+
}
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
function pickArray(v) {
|
|
30
|
+
if (Array.isArray(v))
|
|
31
|
+
return v;
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { IPreset } from '../model/preset.js';
|
|
2
|
+
export declare class PresetRegistry {
|
|
3
|
+
private readonly byId;
|
|
4
|
+
constructor(presets?: readonly IPreset[]);
|
|
5
|
+
add(preset: IPreset): void;
|
|
6
|
+
has(id: string): boolean;
|
|
7
|
+
get(id: string): IPreset | undefined;
|
|
8
|
+
list(): readonly IPreset[];
|
|
9
|
+
size(): number;
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=preset-registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preset-registry.d.ts","sourceRoot":"","sources":["../../src/registry/preset-registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAElD,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA8B;gBAEvC,OAAO,GAAE,SAAS,OAAO,EAAO;IAI5C,GAAG,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI;IAI1B,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAIxB,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS;IAIpC,IAAI,IAAI,SAAS,OAAO,EAAE;IAI1B,IAAI,IAAI,MAAM;CAGf"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export class PresetRegistry {
|
|
2
|
+
byId = new Map();
|
|
3
|
+
constructor(presets = []) {
|
|
4
|
+
for (const p of presets)
|
|
5
|
+
this.add(p);
|
|
6
|
+
}
|
|
7
|
+
add(preset) {
|
|
8
|
+
this.byId.set(preset.id, preset);
|
|
9
|
+
}
|
|
10
|
+
has(id) {
|
|
11
|
+
return this.byId.has(id);
|
|
12
|
+
}
|
|
13
|
+
get(id) {
|
|
14
|
+
return this.byId.get(id);
|
|
15
|
+
}
|
|
16
|
+
list() {
|
|
17
|
+
return [...this.byId.values()];
|
|
18
|
+
}
|
|
19
|
+
size() {
|
|
20
|
+
return this.byId.size;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { WorkspaceProfile } from '@shrkcrft/workspace';
|
|
2
|
+
import type { IPreset } from '../model/preset.js';
|
|
3
|
+
export interface IPresetRecommendation {
|
|
4
|
+
preset: IPreset;
|
|
5
|
+
score: number;
|
|
6
|
+
confidence: 'high' | 'medium' | 'low';
|
|
7
|
+
reasons: string[];
|
|
8
|
+
}
|
|
9
|
+
export interface IRecommendOptions {
|
|
10
|
+
profiles: readonly WorkspaceProfile[];
|
|
11
|
+
/** Optional list of preset ids to exclude (e.g. already applied). */
|
|
12
|
+
exclude?: readonly string[];
|
|
13
|
+
/** Max number of recommendations. Default: 5. */
|
|
14
|
+
limit?: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Rank presets against detected workspace profiles. Pure function:
|
|
18
|
+
* - +5 for each profile in appliesTo that is present
|
|
19
|
+
* - −3 for each profile in appliesTo that is missing
|
|
20
|
+
* (so a preset that lists `[HasNext, HasReact, IsFrontend]` does
|
|
21
|
+
* not outrank `[HasReact, IsFrontend]` on a pure-React repo just
|
|
22
|
+
* because of its base weight). The penalty is intentionally
|
|
23
|
+
* smaller than the +5 match so a partial match still beats no
|
|
24
|
+
* match.
|
|
25
|
+
* - −5 per profile in notAppropriateFor that is present (drops the
|
|
26
|
+
* preset entirely)
|
|
27
|
+
* - +base weight (default 5) to break ties
|
|
28
|
+
*/
|
|
29
|
+
export declare function recommendPresets(presets: readonly IPreset[], options: IRecommendOptions): IPresetRecommendation[];
|
|
30
|
+
//# sourceMappingURL=recommend.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recommend.d.ts","sourceRoot":"","sources":["../../src/registry/recommend.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAElD,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,OAAO,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAC;IACtC,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,SAAS,gBAAgB,EAAE,CAAC;IACtC,qEAAqE;IACrE,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,iDAAiD;IACjD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,SAAS,OAAO,EAAE,EAC3B,OAAO,EAAE,iBAAiB,GACzB,qBAAqB,EAAE,CA2CzB"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rank presets against detected workspace profiles. Pure function:
|
|
3
|
+
* - +5 for each profile in appliesTo that is present
|
|
4
|
+
* - −3 for each profile in appliesTo that is missing
|
|
5
|
+
* (so a preset that lists `[HasNext, HasReact, IsFrontend]` does
|
|
6
|
+
* not outrank `[HasReact, IsFrontend]` on a pure-React repo just
|
|
7
|
+
* because of its base weight). The penalty is intentionally
|
|
8
|
+
* smaller than the +5 match so a partial match still beats no
|
|
9
|
+
* match.
|
|
10
|
+
* - −5 per profile in notAppropriateFor that is present (drops the
|
|
11
|
+
* preset entirely)
|
|
12
|
+
* - +base weight (default 5) to break ties
|
|
13
|
+
*/
|
|
14
|
+
export function recommendPresets(presets, options) {
|
|
15
|
+
const profileSet = new Set(options.profiles);
|
|
16
|
+
const exclude = new Set(options.exclude ?? []);
|
|
17
|
+
const out = [];
|
|
18
|
+
for (const preset of presets) {
|
|
19
|
+
if (exclude.has(preset.id))
|
|
20
|
+
continue;
|
|
21
|
+
const reasons = [];
|
|
22
|
+
let score = preset.weight ?? 5;
|
|
23
|
+
let dq = false;
|
|
24
|
+
let matchedCount = 0;
|
|
25
|
+
for (const need of preset.appliesTo ?? []) {
|
|
26
|
+
if (profileSet.has(need)) {
|
|
27
|
+
score += 5;
|
|
28
|
+
matchedCount += 1;
|
|
29
|
+
reasons.push(`matches profile: ${need}`);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
// Small miss penalty so longer / more-specific appliesTo
|
|
33
|
+
// lists do not falsely dominate shorter, more-targeted ones.
|
|
34
|
+
score -= 3;
|
|
35
|
+
reasons.push(`missing profile: ${need}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
for (const block of preset.notAppropriateFor ?? []) {
|
|
39
|
+
if (profileSet.has(block)) {
|
|
40
|
+
score -= 5;
|
|
41
|
+
reasons.push(`not appropriate (profile: ${block})`);
|
|
42
|
+
dq = true;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (dq)
|
|
46
|
+
continue;
|
|
47
|
+
if ((preset.appliesTo?.length ?? 0) === 0 && reasons.length === 0) {
|
|
48
|
+
reasons.push('universal preset');
|
|
49
|
+
}
|
|
50
|
+
const confidence = score >= 15 ? 'high' : score >= 9 ? 'medium' : 'low';
|
|
51
|
+
out.push({ preset, score, confidence, reasons });
|
|
52
|
+
// matchedCount is reserved for future tie-breaking; surface in reasons
|
|
53
|
+
// for now so the explanation captures it.
|
|
54
|
+
void matchedCount;
|
|
55
|
+
}
|
|
56
|
+
out.sort((a, b) => b.score - a.score);
|
|
57
|
+
const limit = options.limit ?? 5;
|
|
58
|
+
return out.slice(0, limit);
|
|
59
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { IPreset, IPresetFile, IPresetIncludes } from '../model/preset.js';
|
|
2
|
+
import type { PresetRegistry } from './preset-registry.js';
|
|
3
|
+
export interface IResolvedPresetIssue {
|
|
4
|
+
severity: 'error' | 'warning';
|
|
5
|
+
code: 'composition-cycle' | 'composed-not-found' | 'invalid-self-compose';
|
|
6
|
+
message: string;
|
|
7
|
+
presetId: string;
|
|
8
|
+
}
|
|
9
|
+
export interface IProvenanceEntry {
|
|
10
|
+
/** The id of the preset that contributed this item. */
|
|
11
|
+
presetId: string;
|
|
12
|
+
/** Path through the composition graph: root → … → contributor. */
|
|
13
|
+
chain: readonly string[];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* A flattened preset with composition resolved. Arrays are merged, deduped,
|
|
17
|
+
* and provenance is recorded so callers can show "this entry came from
|
|
18
|
+
* preset X via composition through Y".
|
|
19
|
+
*/
|
|
20
|
+
export interface IResolvedPreset {
|
|
21
|
+
/** The root preset id that was requested. */
|
|
22
|
+
rootId: string;
|
|
23
|
+
/** Every preset id visited, in resolution order (root first, deepest last). */
|
|
24
|
+
composedFrom: readonly string[];
|
|
25
|
+
/** Final merged metadata (title/description/tags/etc. come from the root). */
|
|
26
|
+
preset: IPreset;
|
|
27
|
+
/** Merged includes block; arrays deduplicated. */
|
|
28
|
+
includes: IPresetIncludes & {
|
|
29
|
+
knowledgeIds?: readonly string[];
|
|
30
|
+
ruleIds?: readonly string[];
|
|
31
|
+
pathConventionIds?: readonly string[];
|
|
32
|
+
templateIds?: readonly string[];
|
|
33
|
+
pipelineIds?: readonly string[];
|
|
34
|
+
knowledge?: readonly string[];
|
|
35
|
+
rules?: readonly string[];
|
|
36
|
+
paths?: readonly string[];
|
|
37
|
+
templates?: readonly string[];
|
|
38
|
+
pipelines?: readonly string[];
|
|
39
|
+
};
|
|
40
|
+
/** Files to create — local preset wins on path conflict. */
|
|
41
|
+
filesToCreate: readonly IPresetFile[];
|
|
42
|
+
/** Merged recommendedNextCommands (deduped, root order preserved). */
|
|
43
|
+
recommendedNextCommands: readonly string[];
|
|
44
|
+
/** Merged postInstallNotes. */
|
|
45
|
+
postInstallNotes: readonly string[];
|
|
46
|
+
/** Merged safetyNotes. */
|
|
47
|
+
safetyNotes: readonly string[];
|
|
48
|
+
/** Per-item provenance for arrays. */
|
|
49
|
+
provenance: {
|
|
50
|
+
knowledge: ReadonlyMap<string, IProvenanceEntry>;
|
|
51
|
+
rules: ReadonlyMap<string, IProvenanceEntry>;
|
|
52
|
+
paths: ReadonlyMap<string, IProvenanceEntry>;
|
|
53
|
+
templates: ReadonlyMap<string, IProvenanceEntry>;
|
|
54
|
+
pipelines: ReadonlyMap<string, IProvenanceEntry>;
|
|
55
|
+
knowledgeIds: ReadonlyMap<string, IProvenanceEntry>;
|
|
56
|
+
ruleIds: ReadonlyMap<string, IProvenanceEntry>;
|
|
57
|
+
pathConventionIds: ReadonlyMap<string, IProvenanceEntry>;
|
|
58
|
+
templateIds: ReadonlyMap<string, IProvenanceEntry>;
|
|
59
|
+
pipelineIds: ReadonlyMap<string, IProvenanceEntry>;
|
|
60
|
+
files: ReadonlyMap<string, IProvenanceEntry>;
|
|
61
|
+
};
|
|
62
|
+
issues: readonly IResolvedPresetIssue[];
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Resolve a preset by id, recursively expanding `composes`. Returns:
|
|
66
|
+
* - the merged preset shape (with provenance per array entry),
|
|
67
|
+
* - a list of issues (cycle, missing-composed, …).
|
|
68
|
+
*
|
|
69
|
+
* Resolution order: the **root** preset's local items come first, then each
|
|
70
|
+
* composed preset's items are appended depth-first (post-order). "First
|
|
71
|
+
* contributor wins" — so the root naturally overrides composed presets on
|
|
72
|
+
* duplicate ids.
|
|
73
|
+
*/
|
|
74
|
+
export declare function resolvePreset(registry: PresetRegistry, rootId: string): IResolvedPreset;
|
|
75
|
+
//# sourceMappingURL=resolve-preset.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve-preset.d.ts","sourceRoot":"","sources":["../../src/registry/resolve-preset.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAChF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE3D,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,OAAO,GAAG,SAAS,CAAC;IAC9B,IAAI,EACA,mBAAmB,GACnB,oBAAoB,GACpB,sBAAsB,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,uDAAuD;IACvD,QAAQ,EAAE,MAAM,CAAC;IACjB,kEAAkE;IAClE,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1B;AAED;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,6CAA6C;IAC7C,MAAM,EAAE,MAAM,CAAC;IACf,+EAA+E;IAC/E,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC,8EAA8E;IAC9E,MAAM,EAAE,OAAO,CAAC;IAChB,kDAAkD;IAClD,QAAQ,EAAE,eAAe,GAAG;QAC1B,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;QACjC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;QAC5B,iBAAiB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;QACtC,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;QAChC,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;QAChC,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;QAC9B,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;QAC1B,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;QAC1B,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;QAC9B,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;KAC/B,CAAC;IACF,4DAA4D;IAC5D,aAAa,EAAE,SAAS,WAAW,EAAE,CAAC;IACtC,sEAAsE;IACtE,uBAAuB,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3C,+BAA+B;IAC/B,gBAAgB,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,0BAA0B;IAC1B,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/B,sCAAsC;IACtC,UAAU,EAAE;QACV,SAAS,EAAE,WAAW,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;QACjD,KAAK,EAAE,WAAW,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;QAC7C,KAAK,EAAE,WAAW,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;QAC7C,SAAS,EAAE,WAAW,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;QACjD,SAAS,EAAE,WAAW,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;QACjD,YAAY,EAAE,WAAW,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;QACpD,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;QAC/C,iBAAiB,EAAE,WAAW,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;QACzD,WAAW,EAAE,WAAW,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;QACnD,WAAW,EAAE,WAAW,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;QACnD,KAAK,EAAE,WAAW,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;KAC9C,CAAC;IACF,MAAM,EAAE,SAAS,oBAAoB,EAAE,CAAC;CACzC;AAoDD;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,cAAc,EACxB,MAAM,EAAE,MAAM,GACb,eAAe,CAqLjB"}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/** Stable-key for a TS-source array item: the slug between `id:` and the next `,`. */
|
|
2
|
+
function snippetKey(snippet, fallback) {
|
|
3
|
+
const m = snippet.match(/id:\s*['"`]([^'"`]+)['"`]/);
|
|
4
|
+
return m?.[1] ?? fallback;
|
|
5
|
+
}
|
|
6
|
+
function recordWithProvenance(items, bucket, prov, chain, presetId) {
|
|
7
|
+
if (!items)
|
|
8
|
+
return;
|
|
9
|
+
for (let i = 0; i < items.length; i += 1) {
|
|
10
|
+
const value = items[i];
|
|
11
|
+
const key = snippetKey(value, `${presetId}:${i}`);
|
|
12
|
+
if (prov.has(key))
|
|
13
|
+
continue; // first contributor wins (root → composed depth-first)
|
|
14
|
+
bucket.push(value);
|
|
15
|
+
prov.set(key, { presetId, chain });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function recordIds(items, bucket, prov, chain, presetId) {
|
|
19
|
+
if (!items)
|
|
20
|
+
return;
|
|
21
|
+
for (const id of items) {
|
|
22
|
+
if (prov.has(id))
|
|
23
|
+
continue;
|
|
24
|
+
bucket.push(id);
|
|
25
|
+
prov.set(id, { presetId, chain });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function recordKvMap(v, out) {
|
|
29
|
+
if (!v)
|
|
30
|
+
return;
|
|
31
|
+
const it = v instanceof Map ? v.entries() : Object.entries(v);
|
|
32
|
+
for (const [k, val] of it) {
|
|
33
|
+
if (out.has(k))
|
|
34
|
+
continue;
|
|
35
|
+
out.set(k, val);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Resolve a preset by id, recursively expanding `composes`. Returns:
|
|
40
|
+
* - the merged preset shape (with provenance per array entry),
|
|
41
|
+
* - a list of issues (cycle, missing-composed, …).
|
|
42
|
+
*
|
|
43
|
+
* Resolution order: the **root** preset's local items come first, then each
|
|
44
|
+
* composed preset's items are appended depth-first (post-order). "First
|
|
45
|
+
* contributor wins" — so the root naturally overrides composed presets on
|
|
46
|
+
* duplicate ids.
|
|
47
|
+
*/
|
|
48
|
+
export function resolvePreset(registry, rootId) {
|
|
49
|
+
const root = registry.get(rootId);
|
|
50
|
+
if (!root) {
|
|
51
|
+
throw new Error(`Cannot resolve preset "${rootId}" — not found in registry.`);
|
|
52
|
+
}
|
|
53
|
+
const issues = [];
|
|
54
|
+
const composedFrom = [];
|
|
55
|
+
const visited = new Set();
|
|
56
|
+
// Accumulators.
|
|
57
|
+
const knowledge = [];
|
|
58
|
+
const rules = [];
|
|
59
|
+
const paths = [];
|
|
60
|
+
const templates = [];
|
|
61
|
+
const pipelines = [];
|
|
62
|
+
const knowledgeIds = [];
|
|
63
|
+
const ruleIds = [];
|
|
64
|
+
const pathConventionIds = [];
|
|
65
|
+
const templateIds = [];
|
|
66
|
+
const pipelineIds = [];
|
|
67
|
+
const docsMap = new Map();
|
|
68
|
+
const tasksMap = new Map();
|
|
69
|
+
const filePaths = new Map();
|
|
70
|
+
const provFiles = new Map();
|
|
71
|
+
const provKnowledge = new Map();
|
|
72
|
+
const provRules = new Map();
|
|
73
|
+
const provPaths = new Map();
|
|
74
|
+
const provTemplates = new Map();
|
|
75
|
+
const provPipelines = new Map();
|
|
76
|
+
const provKnowledgeIds = new Map();
|
|
77
|
+
const provRuleIds = new Map();
|
|
78
|
+
const provPathConventionIds = new Map();
|
|
79
|
+
const provTemplateIds = new Map();
|
|
80
|
+
const provPipelineIds = new Map();
|
|
81
|
+
const recommendedNextCommands = [];
|
|
82
|
+
const seenCommands = new Set();
|
|
83
|
+
const postInstallNotes = [];
|
|
84
|
+
const seenNotes = new Set();
|
|
85
|
+
const safetyNotes = [];
|
|
86
|
+
const seenSafety = new Set();
|
|
87
|
+
function visit(presetId, chain) {
|
|
88
|
+
// Cycle check FIRST — otherwise the visited-cache below would silently
|
|
89
|
+
// swallow a back-edge cycle (e.g. a → b → a) and we'd never report it.
|
|
90
|
+
if (chain.includes(presetId)) {
|
|
91
|
+
issues.push({
|
|
92
|
+
severity: 'error',
|
|
93
|
+
code: 'composition-cycle',
|
|
94
|
+
message: `Cycle detected: ${[...chain, presetId].join(' → ')}`,
|
|
95
|
+
presetId,
|
|
96
|
+
});
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (visited.has(presetId))
|
|
100
|
+
return;
|
|
101
|
+
if (presetId === rootId && chain.length > 0) {
|
|
102
|
+
issues.push({
|
|
103
|
+
severity: 'error',
|
|
104
|
+
code: 'invalid-self-compose',
|
|
105
|
+
message: `Preset "${presetId}" composes itself`,
|
|
106
|
+
presetId,
|
|
107
|
+
});
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const preset = registry.get(presetId);
|
|
111
|
+
if (!preset) {
|
|
112
|
+
issues.push({
|
|
113
|
+
severity: 'error',
|
|
114
|
+
code: 'composed-not-found',
|
|
115
|
+
message: `Preset "${chain[chain.length - 1] ?? rootId}" composes unknown preset "${presetId}"`,
|
|
116
|
+
presetId,
|
|
117
|
+
});
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
visited.add(presetId);
|
|
121
|
+
composedFrom.push(presetId);
|
|
122
|
+
// First: visit composed presets so the root keeps "first contributor wins"
|
|
123
|
+
// priority over composed entries. We do this by recording the ROOT's
|
|
124
|
+
// contributions before recursing. Composed presets append after.
|
|
125
|
+
const includes = preset.includes;
|
|
126
|
+
recordWithProvenance(includes.knowledge, knowledge, provKnowledge, chain, presetId);
|
|
127
|
+
recordWithProvenance(includes.rules, rules, provRules, chain, presetId);
|
|
128
|
+
recordWithProvenance(includes.paths, paths, provPaths, chain, presetId);
|
|
129
|
+
recordWithProvenance(includes.templates, templates, provTemplates, chain, presetId);
|
|
130
|
+
recordWithProvenance(includes.pipelines, pipelines, provPipelines, chain, presetId);
|
|
131
|
+
recordIds(includes.knowledgeIds, knowledgeIds, provKnowledgeIds, chain, presetId);
|
|
132
|
+
recordIds(includes.ruleIds, ruleIds, provRuleIds, chain, presetId);
|
|
133
|
+
recordIds(includes.pathConventionIds, pathConventionIds, provPathConventionIds, chain, presetId);
|
|
134
|
+
recordIds(includes.templateIds, templateIds, provTemplateIds, chain, presetId);
|
|
135
|
+
recordIds(includes.pipelineIds, pipelineIds, provPipelineIds, chain, presetId);
|
|
136
|
+
recordKvMap(includes.docs, docsMap);
|
|
137
|
+
recordKvMap(includes.tasks, tasksMap);
|
|
138
|
+
for (const f of preset.filesToCreate ?? []) {
|
|
139
|
+
if (!filePaths.has(f.path)) {
|
|
140
|
+
filePaths.set(f.path, f);
|
|
141
|
+
provFiles.set(f.path, { presetId, chain });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
for (const cmd of preset.recommendedNextCommands ?? []) {
|
|
145
|
+
if (seenCommands.has(cmd))
|
|
146
|
+
continue;
|
|
147
|
+
seenCommands.add(cmd);
|
|
148
|
+
recommendedNextCommands.push(cmd);
|
|
149
|
+
}
|
|
150
|
+
for (const note of preset.postInstallNotes ?? []) {
|
|
151
|
+
if (seenNotes.has(note))
|
|
152
|
+
continue;
|
|
153
|
+
seenNotes.add(note);
|
|
154
|
+
postInstallNotes.push(note);
|
|
155
|
+
}
|
|
156
|
+
for (const sn of preset.safetyNotes ?? []) {
|
|
157
|
+
if (seenSafety.has(sn))
|
|
158
|
+
continue;
|
|
159
|
+
seenSafety.add(sn);
|
|
160
|
+
safetyNotes.push(sn);
|
|
161
|
+
}
|
|
162
|
+
// Recurse into composed presets.
|
|
163
|
+
const nextChain = [...chain, presetId];
|
|
164
|
+
for (const dep of preset.composes ?? []) {
|
|
165
|
+
visit(dep, nextChain);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
visit(rootId, []);
|
|
169
|
+
const resolvedIncludes = {
|
|
170
|
+
...(knowledge.length ? { knowledge } : {}),
|
|
171
|
+
...(rules.length ? { rules } : {}),
|
|
172
|
+
...(paths.length ? { paths } : {}),
|
|
173
|
+
...(templates.length ? { templates } : {}),
|
|
174
|
+
...(pipelines.length ? { pipelines } : {}),
|
|
175
|
+
...(knowledgeIds.length ? { knowledgeIds } : {}),
|
|
176
|
+
...(ruleIds.length ? { ruleIds } : {}),
|
|
177
|
+
...(pathConventionIds.length ? { pathConventionIds } : {}),
|
|
178
|
+
...(templateIds.length ? { templateIds } : {}),
|
|
179
|
+
...(pipelineIds.length ? { pipelineIds } : {}),
|
|
180
|
+
...(docsMap.size ? { docs: docsMap } : {}),
|
|
181
|
+
...(tasksMap.size ? { tasks: tasksMap } : {}),
|
|
182
|
+
};
|
|
183
|
+
return {
|
|
184
|
+
rootId,
|
|
185
|
+
composedFrom,
|
|
186
|
+
preset: root,
|
|
187
|
+
includes: resolvedIncludes,
|
|
188
|
+
filesToCreate: [...filePaths.values()],
|
|
189
|
+
recommendedNextCommands,
|
|
190
|
+
postInstallNotes,
|
|
191
|
+
safetyNotes,
|
|
192
|
+
provenance: {
|
|
193
|
+
knowledge: provKnowledge,
|
|
194
|
+
rules: provRules,
|
|
195
|
+
paths: provPaths,
|
|
196
|
+
templates: provTemplates,
|
|
197
|
+
pipelines: provPipelines,
|
|
198
|
+
knowledgeIds: provKnowledgeIds,
|
|
199
|
+
ruleIds: provRuleIds,
|
|
200
|
+
pathConventionIds: provPathConventionIds,
|
|
201
|
+
templateIds: provTemplateIds,
|
|
202
|
+
pipelineIds: provPipelineIds,
|
|
203
|
+
files: provFiles,
|
|
204
|
+
},
|
|
205
|
+
issues,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { IResolvedPreset } from './resolve-preset.js';
|
|
2
|
+
/**
|
|
3
|
+
* The minimal shape the reference-resolver needs from the inspection. This
|
|
4
|
+
* lets the presets package stay independent of @shrkcrft/inspector at the
|
|
5
|
+
* type level.
|
|
6
|
+
*/
|
|
7
|
+
export interface IReferenceLookup {
|
|
8
|
+
hasKnowledge(id: string): boolean;
|
|
9
|
+
hasRule(id: string): boolean;
|
|
10
|
+
hasPath(id: string): boolean;
|
|
11
|
+
hasTemplate(id: string): boolean;
|
|
12
|
+
hasPipeline(id: string): boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface IReferenceMissing {
|
|
15
|
+
kind: 'knowledge' | 'rule' | 'path' | 'template' | 'pipeline';
|
|
16
|
+
id: string;
|
|
17
|
+
}
|
|
18
|
+
export interface IResolvedReferences {
|
|
19
|
+
knowledge: {
|
|
20
|
+
resolved: string[];
|
|
21
|
+
missing: string[];
|
|
22
|
+
};
|
|
23
|
+
rules: {
|
|
24
|
+
resolved: string[];
|
|
25
|
+
missing: string[];
|
|
26
|
+
};
|
|
27
|
+
paths: {
|
|
28
|
+
resolved: string[];
|
|
29
|
+
missing: string[];
|
|
30
|
+
};
|
|
31
|
+
templates: {
|
|
32
|
+
resolved: string[];
|
|
33
|
+
missing: string[];
|
|
34
|
+
};
|
|
35
|
+
pipelines: {
|
|
36
|
+
resolved: string[];
|
|
37
|
+
missing: string[];
|
|
38
|
+
};
|
|
39
|
+
/** Flat list of every missing reference for convenience. */
|
|
40
|
+
missing: readonly IReferenceMissing[];
|
|
41
|
+
totalReferenced: number;
|
|
42
|
+
totalMissing: number;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Resolve the *Ids fields on a resolved preset against the inspection's
|
|
46
|
+
* registries. Returns which references exist and which are missing.
|
|
47
|
+
*/
|
|
48
|
+
export declare function resolvePresetReferences(resolved: IResolvedPreset, lookup: IReferenceLookup): IResolvedReferences;
|
|
49
|
+
//# sourceMappingURL=resolve-references.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve-references.d.ts","sourceRoot":"","sources":["../../src/registry/resolve-references.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAE3D;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC;IAClC,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC;IAC7B,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC;IAC7B,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC;IACjC,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC;CAClC;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,WAAW,GAAG,MAAM,GAAG,MAAM,GAAG,UAAU,GAAG,UAAU,CAAC;IAC9D,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE;QAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;QAAC,OAAO,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IACrD,KAAK,EAAE;QAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;QAAC,OAAO,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IACjD,KAAK,EAAE;QAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;QAAC,OAAO,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IACjD,SAAS,EAAE;QAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;QAAC,OAAO,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IACrD,SAAS,EAAE;QAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;QAAC,OAAO,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IACrD,4DAA4D;IAC5D,OAAO,EAAE,SAAS,iBAAiB,EAAE,CAAC;IACtC,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,eAAe,EACzB,MAAM,EAAE,gBAAgB,GACvB,mBAAmB,CAoCrB"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the *Ids fields on a resolved preset against the inspection's
|
|
3
|
+
* registries. Returns which references exist and which are missing.
|
|
4
|
+
*/
|
|
5
|
+
export function resolvePresetReferences(resolved, lookup) {
|
|
6
|
+
const inc = resolved.includes;
|
|
7
|
+
const out = {
|
|
8
|
+
knowledge: { resolved: [], missing: [] },
|
|
9
|
+
rules: { resolved: [], missing: [] },
|
|
10
|
+
paths: { resolved: [], missing: [] },
|
|
11
|
+
templates: { resolved: [], missing: [] },
|
|
12
|
+
pipelines: { resolved: [], missing: [] },
|
|
13
|
+
missing: [],
|
|
14
|
+
totalReferenced: 0,
|
|
15
|
+
totalMissing: 0,
|
|
16
|
+
};
|
|
17
|
+
function check(ids, bucket, kind, has) {
|
|
18
|
+
if (!ids)
|
|
19
|
+
return;
|
|
20
|
+
for (const id of ids) {
|
|
21
|
+
out.totalReferenced += 1;
|
|
22
|
+
if (has(id)) {
|
|
23
|
+
bucket.resolved.push(id);
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
bucket.missing.push(id);
|
|
27
|
+
out.missing.push({ kind, id });
|
|
28
|
+
out.totalMissing += 1;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
check(inc.knowledgeIds, out.knowledge, 'knowledge', (id) => lookup.hasKnowledge(id));
|
|
33
|
+
check(inc.ruleIds, out.rules, 'rule', (id) => lookup.hasRule(id));
|
|
34
|
+
check(inc.pathConventionIds, out.paths, 'path', (id) => lookup.hasPath(id));
|
|
35
|
+
check(inc.templateIds, out.templates, 'template', (id) => lookup.hasTemplate(id));
|
|
36
|
+
check(inc.pipelineIds, out.pipelines, 'pipeline', (id) => lookup.hasPipeline(id));
|
|
37
|
+
return out;
|
|
38
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@shrkcrft/presets",
|
|
3
|
+
"version": "0.1.0-alpha.1",
|
|
4
|
+
"description": "SharkCraft presets: reusable project setups (knowledge/rules/paths/templates/pipelines/docs) that can be applied to a target repo through the CLI.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "SharkCraft contributors",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"README.md",
|
|
20
|
+
"LICENSE"
|
|
21
|
+
],
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/sharkcraft/sharkcraft.git",
|
|
25
|
+
"directory": "packages/presets"
|
|
26
|
+
},
|
|
27
|
+
"homepage": "https://github.com/sharkcraft/sharkcraft",
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/sharkcraft/sharkcraft/issues"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"sharkcraft",
|
|
33
|
+
"presets",
|
|
34
|
+
"ai-agent"
|
|
35
|
+
],
|
|
36
|
+
"engines": {
|
|
37
|
+
"bun": ">=1.1.0",
|
|
38
|
+
"node": ">=18"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@shrkcrft/core": "^0.1.0-alpha.1",
|
|
45
|
+
"@shrkcrft/knowledge": "^0.1.0-alpha.1",
|
|
46
|
+
"@shrkcrft/workspace": "^0.1.0-alpha.1"
|
|
47
|
+
},
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public"
|
|
50
|
+
}
|
|
51
|
+
}
|