@friehub/blueprint 0.1.0

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 (138) hide show
  1. package/.github/workflows/ci.yml +122 -0
  2. package/.github/workflows/publish.yml +24 -0
  3. package/README.md +266 -0
  4. package/adapters/analytics/amplitude.yaml +44 -0
  5. package/adapters/analytics/mixpanel.yaml +47 -0
  6. package/adapters/analytics/segment.yaml +40 -0
  7. package/adapters/auth/auth0.yaml +56 -0
  8. package/adapters/auth/clerk.yaml +53 -0
  9. package/adapters/auth/supertokens.yaml +55 -0
  10. package/adapters/billing/paddle.yaml +57 -0
  11. package/adapters/billing/stripe.yaml +49 -0
  12. package/adapters/caching/memcached.yaml +28 -0
  13. package/adapters/caching/redis.yaml +37 -0
  14. package/adapters/chargebacks/chargebacks911.yaml +45 -0
  15. package/adapters/chargebacks/stripe.yaml +45 -0
  16. package/adapters/crm_leads/hubspot.yaml +43 -0
  17. package/adapters/crm_leads/salesforce.yaml +60 -0
  18. package/adapters/customer_support/intercom.yaml +44 -0
  19. package/adapters/customer_support/zendesk.yaml +51 -0
  20. package/adapters/donations/paypal.yaml +47 -0
  21. package/adapters/donations/stripe.yaml +47 -0
  22. package/adapters/emails/mailgun.yaml +47 -0
  23. package/adapters/emails/resend.yaml +43 -0
  24. package/adapters/emails/sendgrid.yaml +43 -0
  25. package/adapters/error_tracking/bugsnag.yaml +52 -0
  26. package/adapters/error_tracking/sentry.yaml +58 -0
  27. package/adapters/feature_flags/flagsmith.yaml +41 -0
  28. package/adapters/feature_flags/launchdarkly.yaml +41 -0
  29. package/adapters/feature_flags/unleash.yaml +41 -0
  30. package/adapters/fraud_detection/riskified.yaml +41 -0
  31. package/adapters/fraud_detection/sift.yaml +40 -0
  32. package/adapters/fulfillment/easyship.yaml +51 -0
  33. package/adapters/fulfillment/shipengine.yaml +51 -0
  34. package/adapters/incident_management/opsgenie.yaml +49 -0
  35. package/adapters/incident_management/pagerduty.yaml +48 -0
  36. package/adapters/invoicing/freshbooks.yaml +54 -0
  37. package/adapters/invoicing/stripe.yaml +47 -0
  38. package/adapters/ip_intelligence/ipinfo.yaml +37 -0
  39. package/adapters/ip_intelligence/maxmind.yaml +39 -0
  40. package/adapters/jobs/bullmq.yaml +54 -0
  41. package/adapters/jobs/temporal.yaml +53 -0
  42. package/adapters/kyc/jumio.yaml +54 -0
  43. package/adapters/kyc/onfido.yaml +53 -0
  44. package/adapters/media/cloudinary.yaml +48 -0
  45. package/adapters/media/imgix.yaml +47 -0
  46. package/adapters/notifications/firebase.yaml +45 -0
  47. package/adapters/notifications/novu.yaml +46 -0
  48. package/adapters/notifications/onesignal.yaml +45 -0
  49. package/adapters/payments/adyen.yaml +46 -0
  50. package/adapters/payments/paystack.yaml +45 -0
  51. package/adapters/payments/stripe.yaml +49 -0
  52. package/adapters/payouts/paypal.yaml +49 -0
  53. package/adapters/payouts/stripe.yaml +49 -0
  54. package/adapters/projects/asana.yaml +49 -0
  55. package/adapters/projects/jira.yaml +58 -0
  56. package/adapters/projects/linear.yaml +49 -0
  57. package/adapters/queues/bullmq.yaml +47 -0
  58. package/adapters/queues/rabbitmq.yaml +51 -0
  59. package/adapters/queues/sqs.yaml +45 -0
  60. package/adapters/rate_limiting/cloudflare.yaml +37 -0
  61. package/adapters/rate_limiting/upstash.yaml +35 -0
  62. package/adapters/search/algolia.yaml +39 -0
  63. package/adapters/search/meilisearch.yaml +39 -0
  64. package/adapters/search/typesense.yaml +42 -0
  65. package/adapters/shipping/easyship.yaml +45 -0
  66. package/adapters/shipping/shipengine.yaml +45 -0
  67. package/adapters/sms/twilio.yaml +41 -0
  68. package/adapters/sms/vonage.yaml +41 -0
  69. package/adapters/storage/azure-blob.yaml +42 -0
  70. package/adapters/storage/gcs.yaml +41 -0
  71. package/adapters/storage/s3.yaml +49 -0
  72. package/adapters/subscriptions/chargebee.yaml +32 -0
  73. package/adapters/subscriptions/stripe.yaml +37 -0
  74. package/adapters/tasks/asana.yaml +50 -0
  75. package/adapters/tasks/jira.yaml +59 -0
  76. package/adapters/tasks/linear.yaml +50 -0
  77. package/adapters/taxation/avalara.yaml +52 -0
  78. package/adapters/taxation/taxjar.yaml +48 -0
  79. package/adapters/trace_query/datadog.yaml +49 -0
  80. package/adapters/trace_query/honeycomb.yaml +42 -0
  81. package/adapters/trace_query/jaeger.yaml +42 -0
  82. package/adapters/web_analytics/google-analytics.yaml +42 -0
  83. package/adapters/web_analytics/plausible.yaml +34 -0
  84. package/adapters/web_analytics/posthog.yaml +34 -0
  85. package/adapters/webhooks/relay.yaml +35 -0
  86. package/adapters/webhooks/svix.yaml +41 -0
  87. package/blueprint.json +5 -0
  88. package/blueprinter_system_design.svg +139 -0
  89. package/catalog.json +37943 -0
  90. package/dist/cli/commands.js +362 -0
  91. package/dist/cli/help.js +211 -0
  92. package/dist/cli/render.js +109 -0
  93. package/dist/cli.js +69 -0
  94. package/dist/core/adapters/adapter-audit.test.js +85 -0
  95. package/dist/core/adapters/adapter.test.js +66 -0
  96. package/dist/core/adapters/index.js +4 -0
  97. package/dist/core/adapters/load.js +131 -0
  98. package/dist/core/adapters/resolve.js +78 -0
  99. package/dist/core/adapters/select.js +80 -0
  100. package/dist/core/adapters/types.js +1 -0
  101. package/dist/core/adapters/validate.js +121 -0
  102. package/dist/core/catalog.js +3 -0
  103. package/dist/core/collectors.js +126 -0
  104. package/dist/core/discovery.js +57 -0
  105. package/dist/core/edge-cases.test.js +147 -0
  106. package/dist/core/envelope.js +1 -0
  107. package/dist/core/graph.js +123 -0
  108. package/dist/core/graph.test.js +62 -0
  109. package/dist/core/implement.js +48 -0
  110. package/dist/core/index.js +9 -0
  111. package/dist/core/load-catalog.js +24 -0
  112. package/dist/core/parse-document.js +114 -0
  113. package/dist/core/parse-document.test.js +104 -0
  114. package/dist/core/parser.js +6 -0
  115. package/dist/core/parser.test.js +134 -0
  116. package/dist/core/resolve.js +119 -0
  117. package/dist/core/resolve.test.js +108 -0
  118. package/dist/core/scanner.js +163 -0
  119. package/dist/core/search.js +34 -0
  120. package/dist/core/search.test.js +43 -0
  121. package/dist/core/section-body.js +258 -0
  122. package/dist/core/sections.js +53 -0
  123. package/dist/core/verify-implement.test.js +123 -0
  124. package/dist/core/verify.js +156 -0
  125. package/dist/generators/engine.js +86 -0
  126. package/dist/generators/generator.test.js +112 -0
  127. package/dist/generators/index.js +3 -0
  128. package/dist/generators/prototype/index.js +242 -0
  129. package/dist/generators/render.js +146 -0
  130. package/dist/generators/types.js +124 -0
  131. package/dist/generators/typescript/helpers.js +125 -0
  132. package/dist/generators/typescript/index.js +206 -0
  133. package/dist/index.js +8 -0
  134. package/dist/mcp/server.js +202 -0
  135. package/dist/mcp/server.test.js +136 -0
  136. package/dist/utils/args.js +142 -0
  137. package/package.json +38 -0
  138. package/tsconfig.json +16 -0
package/dist/cli.js ADDED
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env node
2
+ import { loadCatalogFromRoot } from "./core/load-catalog.js";
3
+ import { registerGenerator } from "./generators/engine.js";
4
+ import { TypeScriptGenerator } from "./generators/typescript/index.js";
5
+ import { parseArguments } from "./utils/args.js";
6
+ import { printHelp } from "./cli/help.js";
7
+ import { checkErrors } from "./cli/commands.js";
8
+ import { handleList, handleInspect, handleGraph, handleSearch, handleResolve, handleAdapters, handleGenerate, handlePrototype, handleSchema, handleBuild, handleVerify, handleImplement, } from "./cli/commands.js";
9
+ import { readFileSync } from "node:fs";
10
+ import { fileURLToPath } from "node:url";
11
+ import { dirname, resolve as pathResolve } from "node:path";
12
+ const __dirname = dirname(fileURLToPath(import.meta.url));
13
+ function getVersion() {
14
+ try {
15
+ const pkgPath = pathResolve(__dirname, "../package.json");
16
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
17
+ return pkg.version ?? "0.0.0";
18
+ }
19
+ catch {
20
+ return "0.0.0";
21
+ }
22
+ }
23
+ async function main() {
24
+ registerGenerator(new TypeScriptGenerator());
25
+ const args = parseArguments(process.argv.slice(2));
26
+ if (args.unknown.length > 0) {
27
+ args.unknown.forEach((flag) => console.error(`Unknown option: ${flag}`));
28
+ process.exit(1);
29
+ }
30
+ if (args.help) {
31
+ printHelp(args.command);
32
+ return;
33
+ }
34
+ if (args.version) {
35
+ console.log(`@friehub/blueprint@${getVersion()}`);
36
+ return;
37
+ }
38
+ const root = args.root ?? process.cwd();
39
+ const result = await loadCatalogFromRoot(root, args.strict ? "strict" : "loose");
40
+ checkErrors(result, args.strict ?? false, args.quiet ?? false);
41
+ if (!result.value) {
42
+ console.error("Failed to load catalog.");
43
+ process.exit(1);
44
+ }
45
+ const handlers = {
46
+ list: handleList,
47
+ inspect: handleInspect,
48
+ graph: handleGraph,
49
+ search: handleSearch,
50
+ resolve: handleResolve,
51
+ adapters: handleAdapters,
52
+ generate: handleGenerate,
53
+ prototype: handlePrototype,
54
+ schema: handleSchema,
55
+ verify: handleVerify,
56
+ implement: handleImplement,
57
+ build: handleBuild,
58
+ };
59
+ if (args.command === "mcp") {
60
+ await import("./mcp/server.js");
61
+ return;
62
+ }
63
+ const handler = handlers[args.command] ?? handleBuild;
64
+ await handler(result, args, root);
65
+ }
66
+ main().catch((error) => {
67
+ console.error("Unexpected error:", error);
68
+ process.exit(1);
69
+ });
@@ -0,0 +1,85 @@
1
+ import { strict as assert } from "node:assert";
2
+ import { describe, it } from "node:test";
3
+ import { loadCatalogFromRoot } from "../index.js";
4
+ import { loadAdapters } from "./load.js";
5
+ import { validateAdapter } from "./validate.js";
6
+ import { loadSelection, addAdapter } from "./select.js";
7
+ import { resolveAdapters, listAdaptersByModule } from "./resolve.js";
8
+ import { fileURLToPath } from "node:url";
9
+ import { join } from "node:path";
10
+ const ROOT = fileURLToPath(new URL("../../../", import.meta.url));
11
+ const ADAPTERS_DIR = join(ROOT, "adapters");
12
+ describe("adapter system audit", () => {
13
+ it("loads all adapters without errors", async () => {
14
+ const { adapters, errors } = await loadAdapters(ADAPTERS_DIR);
15
+ assert.equal(errors.length, 0, `No load errors: ${errors.join(", ")}`);
16
+ assert.ok(adapters.length >= 82, `At least 82 adapters, got ${adapters.length}`);
17
+ });
18
+ it("every adapter has required fields", async () => {
19
+ const { adapters } = await loadAdapters(ADAPTERS_DIR);
20
+ for (const adapter of adapters) {
21
+ assert.ok(adapter.name, `${adapter.module}/${adapter.name} has name`);
22
+ assert.ok(adapter.module, `${adapter.name} has module`);
23
+ assert.ok(adapter.version, `${adapter.name} has version`);
24
+ assert.ok(adapter.implements.length > 0, `${adapter.name} implements functions`);
25
+ assert.ok(adapter.config.required.length > 0, `${adapter.name} has required config`);
26
+ }
27
+ });
28
+ it("every adapter module exists in catalog", async () => {
29
+ const { adapters } = await loadAdapters(ADAPTERS_DIR);
30
+ const result = await loadCatalogFromRoot(ROOT, "loose");
31
+ for (const adapter of adapters) {
32
+ const mod = result.value.modules.find((m) => m.name === adapter.module);
33
+ assert.ok(mod, `Module ${adapter.module} exists for adapter ${adapter.name}`);
34
+ }
35
+ });
36
+ it("validates all adapters against their contracts", async () => {
37
+ const { adapters } = await loadAdapters(ADAPTERS_DIR);
38
+ const result = await loadCatalogFromRoot(ROOT, "loose");
39
+ let totalErrors = 0;
40
+ for (const adapter of adapters) {
41
+ const validation = validateAdapter(adapter, result.value);
42
+ const errors = validation.issues.filter((i) => i.severity === "error");
43
+ totalErrors += errors.length;
44
+ }
45
+ assert.equal(totalErrors, 0, `No validation errors across all adapters`);
46
+ });
47
+ it("lists adapters by module correctly", async () => {
48
+ const { adapters } = await loadAdapters(ADAPTERS_DIR);
49
+ const byModule = listAdaptersByModule(adapters);
50
+ assert.ok(byModule.payments, "payments has adapters");
51
+ assert.ok(byModule.payments.includes("stripe"), "payments has stripe");
52
+ assert.ok(byModule.payments.includes("paystack"), "payments has paystack");
53
+ assert.ok(byModule.payments.includes("adyen"), "payments has adyen");
54
+ });
55
+ it("adapter selection and resolution works", async () => {
56
+ const { adapters } = await loadAdapters(ADAPTERS_DIR);
57
+ const result = await loadCatalogFromRoot(ROOT, "loose");
58
+ await addAdapter(ROOT, "payments", "stripe");
59
+ await addAdapter(ROOT, "caching", "redis");
60
+ const { selection } = await loadSelection(ROOT);
61
+ const resolution = resolveAdapters(selection, adapters, result.value);
62
+ assert.equal(resolution.selected.payments, "stripe");
63
+ assert.equal(resolution.selected.caching, "redis");
64
+ });
65
+ });
66
+ describe("adapter coverage", () => {
67
+ it("all 35 modules have at least one adapter", async () => {
68
+ const { adapters } = await loadAdapters(ADAPTERS_DIR);
69
+ const byModule = listAdaptersByModule(adapters);
70
+ const expectedModules = [
71
+ "payments", "billing", "subscriptions", "invoicing", "donations",
72
+ "payouts", "chargebacks", "emails", "sms", "notifications",
73
+ "webhooks", "caching", "storage", "search", "queues",
74
+ "jobs", "feature_flags", "rate_limiting", "auth", "kyc",
75
+ "analytics", "web_analytics", "fraud_detection", "error_tracking",
76
+ "incident_management", "trace_query", "ip_intelligence", "media",
77
+ "customer_support", "crm_leads", "projects", "tasks",
78
+ "shipping", "taxation", "fulfillment",
79
+ ];
80
+ for (const module of expectedModules) {
81
+ assert.ok(byModule[module], `${module} has adapters`);
82
+ assert.ok(byModule[module].length >= 2, `${module} has at least 2 adapters`);
83
+ }
84
+ });
85
+ });
@@ -0,0 +1,66 @@
1
+ import { strict as assert } from "node:assert";
2
+ import { describe, it } from "node:test";
3
+ import { loadCatalogFromRoot } from "../index.js";
4
+ import { loadAdapters, loadAdapter } from "./load.js";
5
+ import { validateAdapter } from "./validate.js";
6
+ import { loadSelection, addAdapter, removeAdapter } from "./select.js";
7
+ import { resolveAdapters, listAdaptersByModule } from "./resolve.js";
8
+ import { fileURLToPath } from "node:url";
9
+ import { join } from "node:path";
10
+ const ROOT = fileURLToPath(new URL("../../../", import.meta.url));
11
+ const ADAPTERS_DIR = join(ROOT, "adapters");
12
+ describe("adapter system", () => {
13
+ it("loads adapters from directory", async () => {
14
+ const { adapters, errors } = await loadAdapters(ADAPTERS_DIR);
15
+ assert.equal(errors.length, 0);
16
+ assert.ok(adapters.length > 0);
17
+ assert.ok(adapters.some((a) => a.name === "stripe"));
18
+ assert.ok(adapters.some((a) => a.name === "redis"));
19
+ assert.ok(adapters.some((a) => a.name === "bullmq"));
20
+ });
21
+ it("loads a single adapter", async () => {
22
+ const { adapter, error } = await loadAdapter(ADAPTERS_DIR, "payments", "stripe");
23
+ assert.equal(error, null);
24
+ assert.notEqual(adapter, null);
25
+ assert.equal(adapter?.name, "stripe");
26
+ assert.equal(adapter?.module, "payments");
27
+ });
28
+ it("returns error for missing adapter", async () => {
29
+ const { adapter, error } = await loadAdapter(ADAPTERS_DIR, "payments", "nonexistent");
30
+ assert.equal(adapter, null);
31
+ assert.ok(error?.includes("not found"));
32
+ });
33
+ it("validates adapter against contract", async () => {
34
+ const result = await loadCatalogFromRoot(ROOT, "loose");
35
+ assert.notEqual(result.value, null);
36
+ const { adapter } = await loadAdapter(ADAPTERS_DIR, "payments", "stripe");
37
+ assert.notEqual(adapter, null);
38
+ const validation = validateAdapter(adapter, result.value);
39
+ assert.ok(validation.issues.length >= 0);
40
+ assert.ok(typeof validation.valid === "boolean");
41
+ });
42
+ it("lists adapters by module", async () => {
43
+ const { adapters } = await loadAdapters(ADAPTERS_DIR);
44
+ const byModule = listAdaptersByModule(adapters);
45
+ assert.ok(byModule.payments);
46
+ assert.ok(byModule.caching);
47
+ assert.ok(byModule.queues);
48
+ assert.ok(byModule.payments.includes("stripe"));
49
+ });
50
+ it("adds and removes adapter selection", async () => {
51
+ const { selection: afterAdd, error: addError } = await addAdapter(ROOT, "payments", "stripe");
52
+ assert.equal(addError, null);
53
+ assert.equal(afterAdd.adapters.payments, "stripe");
54
+ const { selection: afterRemove, error: removeError } = await removeAdapter(ROOT, "payments");
55
+ assert.equal(removeError, null);
56
+ assert.equal(afterRemove.adapters.payments, undefined);
57
+ });
58
+ it("resolves adapters with validation", async () => {
59
+ const result = await loadCatalogFromRoot(ROOT, "loose");
60
+ assert.notEqual(result.value, null);
61
+ const { adapters } = await loadAdapters(ADAPTERS_DIR);
62
+ const { selection } = await loadSelection(ROOT);
63
+ const resolution = resolveAdapters(selection, adapters, result.value);
64
+ assert.ok(resolution.selected);
65
+ });
66
+ });
@@ -0,0 +1,4 @@
1
+ export { loadAdapters, loadAdapter } from "./load.js";
2
+ export { validateAdapter, validateAdapterSelection } from "./validate.js";
3
+ export { loadSelection, saveSelection, addAdapter, removeAdapter, getAdapterForModule, getPrimaryAdapter } from "./select.js";
4
+ export { resolveAdapters, listAdaptersByModule, findAdapter } from "./resolve.js";
@@ -0,0 +1,131 @@
1
+ import { readFile, readdir, stat } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { parse as parseYaml } from "yaml";
4
+ export async function loadAdapters(adaptersDir) {
5
+ const adapters = [];
6
+ const errors = [];
7
+ if (!(await dirExists(adaptersDir))) {
8
+ return { adapters, errors };
9
+ }
10
+ const modules = await readdir(adaptersDir, { withFileTypes: true });
11
+ for (const moduleEntry of modules) {
12
+ if (!moduleEntry.isDirectory() || moduleEntry.name.startsWith("_")) {
13
+ continue;
14
+ }
15
+ const moduleDir = join(adaptersDir, moduleEntry.name);
16
+ const files = await readdir(moduleDir);
17
+ for (const file of files) {
18
+ if (!file.endsWith(".yaml") && !file.endsWith(".yml")) {
19
+ continue;
20
+ }
21
+ const filePath = join(moduleDir, file);
22
+ try {
23
+ const content = await readFile(filePath, "utf8");
24
+ const parsed = parseYaml(content);
25
+ const adapter = parseAdapterDefinition(parsed, filePath);
26
+ if (adapter) {
27
+ adapters.push(adapter);
28
+ }
29
+ }
30
+ catch (error) {
31
+ errors.push(`Failed to load ${filePath}: ${error instanceof Error ? error.message : error}`);
32
+ }
33
+ }
34
+ }
35
+ return { adapters, errors };
36
+ }
37
+ export async function loadAdapter(adaptersDir, module, provider) {
38
+ const filePath = join(adaptersDir, module, `${provider}.yaml`);
39
+ try {
40
+ const content = await readFile(filePath, "utf8");
41
+ const parsed = parseYaml(content);
42
+ const adapter = parseAdapterDefinition(parsed, filePath);
43
+ if (!adapter) {
44
+ return { adapter: null, error: `Invalid adapter definition in ${filePath}` };
45
+ }
46
+ return { adapter, error: null };
47
+ }
48
+ catch (error) {
49
+ if (error.code === "ENOENT") {
50
+ return { adapter: null, error: `Adapter not found: ${module}/${provider}` };
51
+ }
52
+ return { adapter: null, error: `Failed to load ${filePath}: ${error instanceof Error ? error.message : error}` };
53
+ }
54
+ }
55
+ function parseAdapterDefinition(data, filePath) {
56
+ if (!data || typeof data !== "object") {
57
+ return null;
58
+ }
59
+ const obj = data;
60
+ if (typeof obj.name !== "string" || typeof obj.module !== "string" || typeof obj.version !== "string") {
61
+ return null;
62
+ }
63
+ if (!Array.isArray(obj.implements)) {
64
+ return null;
65
+ }
66
+ const config = parseConfig(obj.config);
67
+ const adapter = {
68
+ name: obj.name,
69
+ module: obj.module,
70
+ version: obj.version,
71
+ implements: obj.implements,
72
+ config,
73
+ };
74
+ if (typeof obj.description === "string") {
75
+ adapter.description = obj.description;
76
+ }
77
+ if (Array.isArray(obj.does_not_implement)) {
78
+ adapter.does_not_implement = obj.does_not_implement;
79
+ }
80
+ if (Array.isArray(obj.dependencies)) {
81
+ adapter.dependencies = parseDependencies(obj.dependencies);
82
+ }
83
+ if (typeof obj.metadata === "object" && obj.metadata !== null) {
84
+ adapter.metadata = obj.metadata;
85
+ }
86
+ return adapter;
87
+ }
88
+ function parseConfig(data) {
89
+ const config = { required: [], optional: [] };
90
+ if (!data || typeof data !== "object") {
91
+ return config;
92
+ }
93
+ const obj = data;
94
+ if (Array.isArray(obj.required)) {
95
+ config.required = obj.required.filter((f) => {
96
+ if (!f || typeof f !== "object")
97
+ return false;
98
+ const field = f;
99
+ return typeof field.name === "string" && typeof field.type === "string";
100
+ });
101
+ }
102
+ if (Array.isArray(obj.optional)) {
103
+ config.optional = obj.optional.filter((f) => {
104
+ if (!f || typeof f !== "object")
105
+ return false;
106
+ const field = f;
107
+ return typeof field.name === "string" && typeof field.type === "string";
108
+ });
109
+ }
110
+ return config;
111
+ }
112
+ function parseDependencies(data) {
113
+ if (!Array.isArray(data)) {
114
+ return [];
115
+ }
116
+ return data.filter((d) => {
117
+ if (!d || typeof d !== "object")
118
+ return false;
119
+ const dep = d;
120
+ return typeof dep.module === "string" && typeof dep.purpose === "string";
121
+ });
122
+ }
123
+ async function dirExists(dir) {
124
+ try {
125
+ const s = await stat(dir);
126
+ return s.isDirectory();
127
+ }
128
+ catch {
129
+ return false;
130
+ }
131
+ }
@@ -0,0 +1,78 @@
1
+ import { validateAdapter } from "./validate.js";
2
+ export function resolveAdapters(selection, adapters, catalog) {
3
+ const selected = {};
4
+ const fallbacks = {};
5
+ const issues = [];
6
+ for (const [module, adapterRef] of Object.entries(selection.adapters)) {
7
+ if (typeof adapterRef === "string") {
8
+ selected[module] = adapterRef;
9
+ }
10
+ else {
11
+ selected[module] = adapterRef.primary;
12
+ if (adapterRef.fallback) {
13
+ fallbacks[module] = adapterRef.fallback;
14
+ }
15
+ }
16
+ }
17
+ for (const [module, provider] of Object.entries(selected)) {
18
+ const adapter = adapters.find((a) => a.module === module && a.name === provider);
19
+ if (!adapter) {
20
+ issues.push({
21
+ adapter: provider,
22
+ module,
23
+ message: `Adapter "${provider}" not found for module "${module}"`,
24
+ severity: "error",
25
+ });
26
+ continue;
27
+ }
28
+ const result = validateAdapter(adapter, catalog);
29
+ issues.push(...result.issues);
30
+ }
31
+ for (const [module, provider] of Object.entries(fallbacks)) {
32
+ const adapter = adapters.find((a) => a.module === module && a.name === provider);
33
+ if (!adapter) {
34
+ issues.push({
35
+ adapter: provider,
36
+ module,
37
+ message: `Fallback adapter "${provider}" not found for module "${module}"`,
38
+ severity: "warning",
39
+ });
40
+ }
41
+ }
42
+ for (const module of catalog.modules) {
43
+ if (!selected[module.name] && module.hardDeps.length > 0) {
44
+ const hasAdapterSelection = Object.keys(selected).some((m) => module.hardDeps.includes(m) || m === module.name);
45
+ if (hasAdapterSelection) {
46
+ issues.push({
47
+ adapter: "",
48
+ module: module.name,
49
+ message: `Module "${module.name}" is required but has no adapter selected`,
50
+ severity: "warning",
51
+ });
52
+ }
53
+ }
54
+ }
55
+ return { selected, fallbacks, issues };
56
+ }
57
+ export function listAdaptersByModule(adapters) {
58
+ const byModule = {};
59
+ for (const adapter of adapters) {
60
+ const list = byModule[adapter.module];
61
+ if (list) {
62
+ list.push(adapter.name);
63
+ }
64
+ else {
65
+ byModule[adapter.module] = [adapter.name];
66
+ }
67
+ }
68
+ for (const module of Object.keys(byModule)) {
69
+ const list = byModule[module];
70
+ if (list) {
71
+ list.sort();
72
+ }
73
+ }
74
+ return byModule;
75
+ }
76
+ export function findAdapter(adapters, module, provider) {
77
+ return adapters.find((a) => a.module === module && a.name === provider) ?? null;
78
+ }
@@ -0,0 +1,80 @@
1
+ import { readFile, writeFile, mkdir } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ export async function loadSelection(rootDir) {
4
+ const configPath = join(rootDir, "blueprint.json");
5
+ try {
6
+ const content = await readFile(configPath, "utf8");
7
+ const config = JSON.parse(content);
8
+ if (!config.adapters || typeof config.adapters !== "object") {
9
+ return { selection: { adapters: {} }, error: null };
10
+ }
11
+ return { selection: { adapters: config.adapters }, error: null };
12
+ }
13
+ catch (error) {
14
+ if (error.code === "ENOENT") {
15
+ return { selection: { adapters: {} }, error: null };
16
+ }
17
+ return { selection: { adapters: {} }, error: `Failed to load selection: ${error instanceof Error ? error.message : error}` };
18
+ }
19
+ }
20
+ export async function saveSelection(rootDir, selection) {
21
+ const configPath = join(rootDir, "blueprint.json");
22
+ try {
23
+ let config = {};
24
+ try {
25
+ const content = await readFile(configPath, "utf8");
26
+ config = JSON.parse(content);
27
+ }
28
+ catch {
29
+ // File doesn't exist or is invalid, start fresh
30
+ }
31
+ config.adapters = selection.adapters;
32
+ await mkdir(dirname(configPath), { recursive: true });
33
+ await writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
34
+ return null;
35
+ }
36
+ catch (error) {
37
+ return `Failed to save selection: ${error instanceof Error ? error.message : error}`;
38
+ }
39
+ }
40
+ export async function addAdapter(rootDir, module, provider) {
41
+ const { selection, error } = await loadSelection(rootDir);
42
+ if (error) {
43
+ return { selection, error };
44
+ }
45
+ selection.adapters[module] = provider;
46
+ const saveError = await saveSelection(rootDir, selection);
47
+ if (saveError) {
48
+ return { selection, error: saveError };
49
+ }
50
+ return { selection, error: null };
51
+ }
52
+ export async function removeAdapter(rootDir, module) {
53
+ const { selection, error } = await loadSelection(rootDir);
54
+ if (error) {
55
+ return { selection, error };
56
+ }
57
+ delete selection.adapters[module];
58
+ const saveError = await saveSelection(rootDir, selection);
59
+ if (saveError) {
60
+ return { selection, error: saveError };
61
+ }
62
+ return { selection, error: null };
63
+ }
64
+ export function getAdapterForModule(selection, module) {
65
+ const adapter = selection.adapters[module];
66
+ if (!adapter) {
67
+ return null;
68
+ }
69
+ return adapter;
70
+ }
71
+ export function getPrimaryAdapter(selection, module) {
72
+ const adapter = selection.adapters[module];
73
+ if (!adapter) {
74
+ return null;
75
+ }
76
+ if (typeof adapter === "string") {
77
+ return adapter;
78
+ }
79
+ return adapter.primary;
80
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,121 @@
1
+ export function validateAdapter(adapter, catalog) {
2
+ const issues = [];
3
+ const module = catalog.modules.find((m) => m.name === adapter.module);
4
+ if (!module) {
5
+ issues.push({
6
+ adapter: adapter.name,
7
+ module: adapter.module,
8
+ message: `Module "${adapter.module}" not found in catalog`,
9
+ severity: "error",
10
+ });
11
+ return { valid: false, issues };
12
+ }
13
+ for (const fn of adapter.implements) {
14
+ const contractFn = module.functions.find((f) => f.name === fn);
15
+ if (!contractFn) {
16
+ const similar = findSimilarFunction(fn, module);
17
+ const suggestion = similar ? ` Did you mean "${similar}"?` : "";
18
+ issues.push({
19
+ adapter: adapter.name,
20
+ module: adapter.module,
21
+ message: `Adapter implements "${fn}" but contract does not define it.${suggestion}`,
22
+ severity: "warning",
23
+ });
24
+ }
25
+ }
26
+ for (const fn of module.functions) {
27
+ if (!adapter.implements.includes(fn.name)) {
28
+ if (adapter.does_not_implement?.includes(fn.name)) {
29
+ continue;
30
+ }
31
+ const similar = findSimilarFunction(fn.name, adapter);
32
+ const suggestion = similar ? ` Did you mean "${similar}"?` : "";
33
+ const params = fn.params.map((p) => `${p.name}${p.optional ? "?" : ""}: ${p.type ?? "unknown"}`).join(", ");
34
+ issues.push({
35
+ adapter: adapter.name,
36
+ module: adapter.module,
37
+ message: `Contract defines "${fn.name}(${params})" but adapter does not implement it.${suggestion}`,
38
+ severity: "error",
39
+ });
40
+ }
41
+ }
42
+ for (const dep of adapter.dependencies ?? []) {
43
+ const depModule = catalog.modules.find((m) => m.name === dep.module);
44
+ if (!depModule) {
45
+ issues.push({
46
+ adapter: adapter.name,
47
+ module: adapter.module,
48
+ message: `Adapter depends on "${dep.module}" but it is not in the catalog`,
49
+ severity: "warning",
50
+ });
51
+ }
52
+ }
53
+ return {
54
+ valid: issues.filter((i) => i.severity === "error").length === 0,
55
+ issues,
56
+ };
57
+ }
58
+ function findSimilarFunction(target, source) {
59
+ let names = [];
60
+ if ("functions" in source && source.functions) {
61
+ names = source.functions.map((f) => f.name);
62
+ }
63
+ else if ("implements" in source && source.implements) {
64
+ names = source.implements;
65
+ }
66
+ const targetLower = target.toLowerCase();
67
+ for (const name of names) {
68
+ if (name.toLowerCase() === targetLower)
69
+ continue;
70
+ if (name.toLowerCase().includes(targetLower) || targetLower.includes(name.toLowerCase())) {
71
+ return name;
72
+ }
73
+ if (levenshteinDistance(name.toLowerCase(), targetLower) <= 3) {
74
+ return name;
75
+ }
76
+ }
77
+ return null;
78
+ }
79
+ function levenshteinDistance(a, b) {
80
+ const m = a.length;
81
+ const n = b.length;
82
+ const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
83
+ for (let i = 0; i <= m; i++)
84
+ dp[i][0] = i;
85
+ for (let j = 0; j <= n; j++)
86
+ dp[0][j] = j;
87
+ for (let i = 1; i <= m; i++) {
88
+ for (let j = 1; j <= n; j++) {
89
+ if (a[i - 1] === b[j - 1]) {
90
+ dp[i][j] = dp[i - 1][j - 1];
91
+ }
92
+ else {
93
+ dp[i][j] = 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
94
+ }
95
+ }
96
+ }
97
+ return dp[m][n];
98
+ }
99
+ export function validateAdapterSelection(selected, adapters, catalog) {
100
+ const issues = [];
101
+ for (const [module, provider] of Object.entries(selected)) {
102
+ const adapter = adapters.find((a) => a.module === module && a.name === provider);
103
+ if (!adapter) {
104
+ const available = adapters.filter((a) => a.module === module).map((a) => a.name);
105
+ const suggestion = available.length > 0 ? ` Available adapters: ${available.join(", ")}` : "";
106
+ issues.push({
107
+ adapter: provider,
108
+ module,
109
+ message: `Adapter "${provider}" not found for module "${module}".${suggestion}`,
110
+ severity: "error",
111
+ });
112
+ continue;
113
+ }
114
+ const result = validateAdapter(adapter, catalog);
115
+ issues.push(...result.issues);
116
+ }
117
+ return {
118
+ valid: issues.filter((i) => i.severity === "error").length === 0,
119
+ issues,
120
+ };
121
+ }
@@ -0,0 +1,3 @@
1
+ export function implicitCores(catalog) {
2
+ return catalog.core.filter((c) => c.implicit);
3
+ }