@michaelfromyeg/loom-index 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Michael DeMarco
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,148 @@
1
+ import { EvalReport } from '@michaelfromyeg/loom-eval';
2
+ import { Badge, Target, IndexFile, IndexEntry, IndexVersion } from '@michaelfromyeg/loom-schema';
3
+ import { HarnessDriver } from '@michaelfromyeg/loom-adapter-kit';
4
+ import { Diagnostic, AdapterRegistry } from '@michaelfromyeg/loom-core';
5
+
6
+ interface BadgeInputs {
7
+ /** Static validation (the valid badge) passed. */
8
+ validPassed: boolean;
9
+ /** Reports from running the deterministic eval tier (trace + output). */
10
+ evalReports?: EvalReport[];
11
+ /** Security scan found nothing (the scanned badge). */
12
+ scanClean?: boolean;
13
+ /** A valid signature over the lockfile-hashed artifacts (the signed badge). */
14
+ signatureValid?: boolean;
15
+ /** Publisher namespace ownership proven (the verified badge). */
16
+ ownershipVerified?: boolean;
17
+ }
18
+ interface BadgeResult {
19
+ badges: Badge[];
20
+ /** Harnesses with a passing deterministic run. */
21
+ harnessCoverage: Target[];
22
+ }
23
+ /**
24
+ * Compute the Phase 2 badges from validation + eval results (spec §10).
25
+ * - `valid`: static/lint passes.
26
+ * - `tested`: eval cases exist AND the deterministic tier passes on >= 1 harness;
27
+ * `harnessCoverage` is the set of harnesses with a passing run. An UNTESTED
28
+ * harness never counts -- honest coverage.
29
+ * (`verified`/`scanned`/`signed` are computed in Phase 3.)
30
+ */
31
+ declare function computeBadges(input: BadgeInputs): BadgeResult;
32
+
33
+ interface IndexPluginInput {
34
+ id: string;
35
+ source: string;
36
+ version: string;
37
+ ref: string;
38
+ sha: string;
39
+ badges: Badge[];
40
+ harnessCoverage: Target[];
41
+ telemetry?: {
42
+ installs: number;
43
+ activeUsage: number;
44
+ };
45
+ }
46
+ /**
47
+ * Build a `loom.index/1` from per-version plugin metadata (spec §10). Entries are
48
+ * grouped by id and accumulate versions; the index is metadata only -- it never
49
+ * hosts plugin contents.
50
+ */
51
+ declare function buildIndex(plugins: IndexPluginInput[], federated?: IndexFile["federated"]): IndexFile;
52
+ declare function serializeIndex(index: IndexFile): string;
53
+
54
+ /** Parse + validate a serialized index. */
55
+ declare function loadIndex(text: string): IndexFile;
56
+ declare function findPlugin(index: IndexFile, id: string): IndexEntry | undefined;
57
+ /** The last-listed version of an entry (publish order); undefined when none. */
58
+ declare function latestVersion(entry: IndexEntry): IndexVersion | undefined;
59
+ /** Entries whose latest version carries a given badge. */
60
+ declare function pluginsWithBadge(index: IndexFile, badge: Badge): IndexEntry[];
61
+
62
+ /** The inline server.json shape the MCP Registry serves (subset; spec §10). */
63
+ interface McpServerJson {
64
+ name: string;
65
+ description?: string;
66
+ version: string;
67
+ repository?: {
68
+ url?: string;
69
+ };
70
+ }
71
+ interface McpServerResponse {
72
+ server: McpServerJson;
73
+ _meta?: unknown;
74
+ }
75
+ type FetchLike = (url: string) => Promise<{
76
+ json(): Promise<unknown>;
77
+ }>;
78
+ /**
79
+ * Fetch the official MCP Registry's `GET /v0.1/servers` (spec §10). `fetchImpl` is
80
+ * injectable so tests run offline; the live default uses global fetch. We ingest
81
+ * the registry's own scheme rather than reinventing it.
82
+ */
83
+ declare function fetchMcpRegistry(opts?: {
84
+ baseUrl?: string;
85
+ limit?: number;
86
+ fetchImpl?: FetchLike;
87
+ }): Promise<McpServerResponse[]>;
88
+ /** Map MCP Registry servers into MCP-only index entries. */
89
+ declare function mcpServersToEntries(servers: McpServerResponse[]): IndexEntry[];
90
+ /** Merge federated MCP entries into an index and stamp the ingestion. */
91
+ declare function federate(index: IndexFile, servers: McpServerResponse[], ingestedAt: string): IndexFile;
92
+
93
+ /**
94
+ * Build a metadata index from a set of plugin directories (spec §10, Phase 2
95
+ * "index builds from a set of plugins"). Each plugin is statically validated for
96
+ * the `valid` badge and stamped with its git ref/SHA. The richer `tested` badge
97
+ * comes from the publish gate (which runs evals).
98
+ */
99
+ declare function indexFromPluginDirs(dirs: string[], opts?: {
100
+ sourceFor?: (dir: string) => string;
101
+ }): Promise<IndexFile>;
102
+
103
+ interface ScanFinding {
104
+ file: string;
105
+ pattern: string;
106
+ line: number;
107
+ }
108
+ interface ScanResult {
109
+ clean: boolean;
110
+ findings: ScanFinding[];
111
+ }
112
+ declare function scanText(file: string, text: string): ScanFinding[];
113
+ /** Scan a plugin's executable/hook/passthrough artifacts (the `scanned` badge, §10). */
114
+ declare function scanPlugin(pluginDir: string): ScanResult;
115
+
116
+ interface PublishResult {
117
+ id: string;
118
+ version: string;
119
+ /** The deterministic gate (spec §12): static valid + no failing trace/output runs. */
120
+ ok: boolean;
121
+ validPassed: boolean;
122
+ evalFailed: boolean;
123
+ badges: Badge[];
124
+ harnessCoverage: Target[];
125
+ diagnostics: Diagnostic[];
126
+ evalReports: EvalReport[];
127
+ scan: ScanResult;
128
+ }
129
+ /**
130
+ * The deterministic publish gate (spec §9.1 step 9, §12): static validation always
131
+ * runs and must pass; then every component's evals run against available harnesses,
132
+ * and any FAILED trace/output run blocks the publish. Judge/differential are
133
+ * advisory (reported `skipped`), and UNTESTED harnesses never block -- honest.
134
+ */
135
+ declare function publishCheck(pluginDir: string, opts: {
136
+ registry: AdapterRegistry;
137
+ drivers: Record<Target, HarnessDriver>;
138
+ /** Snapshot each harness's score into evals/.baselines/ for the next release. */
139
+ snapshot?: boolean;
140
+ }): Promise<PublishResult>;
141
+
142
+ /**
143
+ * Opt-in, aggregate-only install record (spec §2, §10): increments an entry's
144
+ * `installs`. There is no per-user data -- telemetry is a count, never identity.
145
+ */
146
+ declare function recordInstall(index: IndexFile, id: string): IndexFile;
147
+
148
+ export { type BadgeInputs, type BadgeResult, type IndexPluginInput, type McpServerJson, type McpServerResponse, type PublishResult, type ScanFinding, type ScanResult, buildIndex, computeBadges, federate, fetchMcpRegistry, findPlugin, indexFromPluginDirs, latestVersion, loadIndex, mcpServersToEntries, pluginsWithBadge, publishCheck, recordInstall, scanPlugin, scanText, serializeIndex };
package/dist/index.js ADDED
@@ -0,0 +1,210 @@
1
+ import { IndexFile, kindOf, refOf } from '@michaelfromyeg/loom-schema';
2
+ import { lint, gitInfo, loadPluginDir } from '@michaelfromyeg/loom-core';
3
+ import { discoverEvals, runEval } from '@michaelfromyeg/loom-eval';
4
+
5
+ // src/badges.ts
6
+ function computeBadges(input) {
7
+ const badges = [];
8
+ if (input.validPassed) badges.push("valid");
9
+ const coverage = /* @__PURE__ */ new Set();
10
+ let anyCases = false;
11
+ let anyTestedPass = false;
12
+ for (const report of input.evalReports ?? []) {
13
+ for (const h of report.harnesses) {
14
+ if (h.cases.length > 0) anyCases = true;
15
+ if (h.status === "tested" && h.pass) {
16
+ coverage.add(h.harness);
17
+ anyTestedPass = true;
18
+ }
19
+ }
20
+ }
21
+ if (anyCases && anyTestedPass) badges.push("tested");
22
+ if (input.ownershipVerified) badges.push("verified");
23
+ if (input.scanClean) badges.push("scanned");
24
+ if (input.signatureValid) badges.push("signed");
25
+ return { badges, harnessCoverage: [...coverage] };
26
+ }
27
+
28
+ // src/build.ts
29
+ function buildIndex(plugins, federated) {
30
+ const byId = /* @__PURE__ */ new Map();
31
+ for (const p of plugins) {
32
+ const entry = byId.get(p.id) ?? { id: p.id, source: p.source, versions: [] };
33
+ entry.versions.push({
34
+ version: p.version,
35
+ ref: p.ref,
36
+ sha: p.sha,
37
+ badges: p.badges,
38
+ harnessCoverage: p.harnessCoverage
39
+ });
40
+ if (p.telemetry) entry.telemetry = p.telemetry;
41
+ byId.set(p.id, entry);
42
+ }
43
+ return {
44
+ schema: "loom.index/1",
45
+ plugins: [...byId.values()],
46
+ ...federated && federated.length > 0 ? { federated } : {}
47
+ };
48
+ }
49
+ function serializeIndex(index) {
50
+ return `${JSON.stringify(index, null, 2)}
51
+ `;
52
+ }
53
+ function loadIndex(text) {
54
+ return IndexFile.parse(JSON.parse(text));
55
+ }
56
+ function findPlugin(index, id) {
57
+ return index.plugins.find((p) => p.id === id);
58
+ }
59
+ function latestVersion(entry) {
60
+ return entry.versions.at(-1);
61
+ }
62
+ function pluginsWithBadge(index, badge) {
63
+ return index.plugins.filter((p) => latestVersion(p)?.badges.includes(badge));
64
+ }
65
+
66
+ // src/federation.ts
67
+ async function fetchMcpRegistry(opts = {}) {
68
+ const base = opts.baseUrl ?? "https://registry.modelcontextprotocol.io";
69
+ const f = opts.fetchImpl ?? globalThis.fetch;
70
+ const res = await f(`${base}/v0.1/servers?limit=${opts.limit ?? 30}`);
71
+ const data = await res.json();
72
+ return data.servers ?? [];
73
+ }
74
+ function mcpServersToEntries(servers) {
75
+ return servers.map(({ server }) => ({
76
+ id: server.name,
77
+ source: server.repository?.url ?? "mcp-registry",
78
+ versions: [
79
+ {
80
+ version: server.version,
81
+ ref: server.version,
82
+ sha: "",
83
+ badges: ["valid"],
84
+ harnessCoverage: []
85
+ }
86
+ ]
87
+ }));
88
+ }
89
+ function federate(index, servers, ingestedAt) {
90
+ const federated = [...index.federated ?? [], { source: "mcp-registry", ingestedAt }];
91
+ return { ...index, plugins: [...index.plugins, ...mcpServersToEntries(servers)], federated };
92
+ }
93
+ async function indexFromPluginDirs(dirs, opts = {}) {
94
+ const inputs = [];
95
+ for (const dir of dirs) {
96
+ const linted = lint(dir);
97
+ const { ref, sha } = await gitInfo(dir);
98
+ inputs.push({
99
+ id: linted.id,
100
+ source: opts.sourceFor?.(dir) ?? dir,
101
+ version: linted.plugin.version,
102
+ ref,
103
+ sha,
104
+ badges: linted.diagnostics.hasErrors ? [] : ["valid"],
105
+ harnessCoverage: []
106
+ });
107
+ }
108
+ return buildIndex(inputs);
109
+ }
110
+ var DANGEROUS = [
111
+ { re: /rm\s+-rf?\s+[/~]/, name: "recursive force delete of root/home" },
112
+ { re: /curl\s+[^|]*\|\s*(sudo\s+)?(ba)?sh/, name: "curl-pipe-to-shell" },
113
+ { re: /wget\s+[^|]*\|\s*(ba)?sh/, name: "wget-pipe-to-shell" },
114
+ { re: /:\(\)\s*\{\s*:\s*\|\s*:&\s*\}\s*;\s*:/, name: "fork bomb" },
115
+ { re: /\b(AKIA[0-9A-Z]{16}|aws_secret_access_key)/i, name: "embedded AWS credential" }
116
+ ];
117
+ function scanText(file, text) {
118
+ const findings = [];
119
+ text.split("\n").forEach((line, i) => {
120
+ for (const d of DANGEROUS) {
121
+ if (d.re.test(line)) findings.push({ file, pattern: d.name, line: i + 1 });
122
+ }
123
+ });
124
+ return findings;
125
+ }
126
+ function scanPlugin(pluginDir) {
127
+ const loaded = loadPluginDir(pluginDir);
128
+ if (!loaded.ok) {
129
+ return {
130
+ clean: false,
131
+ findings: [{ file: pluginDir, pattern: "could not load plugin", line: 0 }]
132
+ };
133
+ }
134
+ const findings = [];
135
+ for (const c of loaded.value.plugin.components) {
136
+ const kind = kindOf(c);
137
+ if (kind !== "hook" && kind !== "passthrough") continue;
138
+ const ref = refOf(c);
139
+ const files = loaded.value.list(ref);
140
+ for (const f of files.length > 0 ? files : [ref]) {
141
+ try {
142
+ findings.push(...scanText(f, loaded.value.read(f).toString("utf8")));
143
+ } catch {
144
+ }
145
+ }
146
+ }
147
+ return { clean: findings.length === 0, findings };
148
+ }
149
+
150
+ // src/publish.ts
151
+ async function publishCheck(pluginDir, opts) {
152
+ const linted = lint(pluginDir);
153
+ const validPassed = !linted.diagnostics.hasErrors;
154
+ const evalReports = [];
155
+ if (validPassed) {
156
+ for (const d of discoverEvals(pluginDir)) {
157
+ evalReports.push(
158
+ await runEval({
159
+ evalFile: d.evalFile,
160
+ pluginDir,
161
+ componentLeaf: d.componentLeaf,
162
+ registry: opts.registry,
163
+ drivers: opts.drivers,
164
+ snapshotBaselines: opts.snapshot
165
+ })
166
+ );
167
+ }
168
+ }
169
+ const evalFailed = evalReports.some(
170
+ (r) => r.harnesses.some((h) => h.status === "tested" && !h.pass)
171
+ );
172
+ const scan = validPassed ? scanPlugin(pluginDir) : { clean: false, findings: [] };
173
+ const { badges, harnessCoverage } = computeBadges({
174
+ validPassed,
175
+ evalReports,
176
+ scanClean: scan.clean
177
+ });
178
+ return {
179
+ id: linted.id,
180
+ version: linted.plugin.version,
181
+ ok: validPassed && !evalFailed,
182
+ validPassed,
183
+ evalFailed,
184
+ badges,
185
+ harnessCoverage,
186
+ diagnostics: linted.diagnostics.items,
187
+ evalReports,
188
+ scan
189
+ };
190
+ }
191
+
192
+ // src/telemetry.ts
193
+ function recordInstall(index, id) {
194
+ return {
195
+ ...index,
196
+ plugins: index.plugins.map(
197
+ (p) => p.id === id ? {
198
+ ...p,
199
+ telemetry: {
200
+ installs: (p.telemetry?.installs ?? 0) + 1,
201
+ activeUsage: p.telemetry?.activeUsage ?? 0
202
+ }
203
+ } : p
204
+ )
205
+ };
206
+ }
207
+
208
+ export { buildIndex, computeBadges, federate, fetchMcpRegistry, findPlugin, indexFromPluginDirs, latestVersion, loadIndex, mcpServersToEntries, pluginsWithBadge, publishCheck, recordInstall, scanPlugin, scanText, serializeIndex };
209
+ //# sourceMappingURL=index.js.map
210
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/badges.ts","../src/build.ts","../src/client.ts","../src/federation.ts","../src/from-dirs.ts","../src/scan.ts","../src/publish.ts","../src/telemetry.ts"],"names":["lint"],"mappings":"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","file":"index.js","sourcesContent":["import type { EvalReport } from \"@michaelfromyeg/loom-eval\";\nimport type { Badge, Target } from \"@michaelfromyeg/loom-schema\";\n\nexport interface BadgeInputs {\n /** Static validation (the valid badge) passed. */\n validPassed: boolean;\n /** Reports from running the deterministic eval tier (trace + output). */\n evalReports?: EvalReport[];\n /** Security scan found nothing (the scanned badge). */\n scanClean?: boolean;\n /** A valid signature over the lockfile-hashed artifacts (the signed badge). */\n signatureValid?: boolean;\n /** Publisher namespace ownership proven (the verified badge). */\n ownershipVerified?: boolean;\n}\n\nexport interface BadgeResult {\n badges: Badge[];\n /** Harnesses with a passing deterministic run. */\n harnessCoverage: Target[];\n}\n\n/**\n * Compute the Phase 2 badges from validation + eval results (spec §10).\n * - `valid`: static/lint passes.\n * - `tested`: eval cases exist AND the deterministic tier passes on >= 1 harness;\n * `harnessCoverage` is the set of harnesses with a passing run. An UNTESTED\n * harness never counts -- honest coverage.\n * (`verified`/`scanned`/`signed` are computed in Phase 3.)\n */\nexport function computeBadges(input: BadgeInputs): BadgeResult {\n const badges: Badge[] = [];\n if (input.validPassed) badges.push(\"valid\");\n\n const coverage = new Set<Target>();\n let anyCases = false;\n let anyTestedPass = false;\n for (const report of input.evalReports ?? []) {\n for (const h of report.harnesses) {\n if (h.cases.length > 0) anyCases = true;\n if (h.status === \"tested\" && h.pass) {\n coverage.add(h.harness);\n anyTestedPass = true;\n }\n }\n }\n if (anyCases && anyTestedPass) badges.push(\"tested\");\n if (input.ownershipVerified) badges.push(\"verified\");\n if (input.scanClean) badges.push(\"scanned\");\n if (input.signatureValid) badges.push(\"signed\");\n\n return { badges, harnessCoverage: [...coverage] };\n}\n","import type { Badge, IndexEntry, IndexFile, Target } from \"@michaelfromyeg/loom-schema\";\n\nexport interface IndexPluginInput {\n id: string;\n source: string;\n version: string;\n ref: string;\n sha: string;\n badges: Badge[];\n harnessCoverage: Target[];\n telemetry?: { installs: number; activeUsage: number };\n}\n\n/**\n * Build a `loom.index/1` from per-version plugin metadata (spec §10). Entries are\n * grouped by id and accumulate versions; the index is metadata only -- it never\n * hosts plugin contents.\n */\nexport function buildIndex(\n plugins: IndexPluginInput[],\n federated?: IndexFile[\"federated\"],\n): IndexFile {\n const byId = new Map<string, IndexEntry>();\n for (const p of plugins) {\n const entry = byId.get(p.id) ?? { id: p.id, source: p.source, versions: [] };\n entry.versions.push({\n version: p.version,\n ref: p.ref,\n sha: p.sha,\n badges: p.badges,\n harnessCoverage: p.harnessCoverage,\n });\n if (p.telemetry) entry.telemetry = p.telemetry;\n byId.set(p.id, entry);\n }\n return {\n schema: \"loom.index/1\",\n plugins: [...byId.values()],\n ...(federated && federated.length > 0 ? { federated } : {}),\n };\n}\n\nexport function serializeIndex(index: IndexFile): string {\n return `${JSON.stringify(index, null, 2)}\\n`;\n}\n","import {\n type Badge,\n type IndexEntry,\n IndexFile,\n type IndexVersion,\n} from \"@michaelfromyeg/loom-schema\";\n\n/** Parse + validate a serialized index. */\nexport function loadIndex(text: string): IndexFile {\n return IndexFile.parse(JSON.parse(text));\n}\n\nexport function findPlugin(index: IndexFile, id: string): IndexEntry | undefined {\n return index.plugins.find((p) => p.id === id);\n}\n\n/** The last-listed version of an entry (publish order); undefined when none. */\nexport function latestVersion(entry: IndexEntry): IndexVersion | undefined {\n return entry.versions.at(-1);\n}\n\n/** Entries whose latest version carries a given badge. */\nexport function pluginsWithBadge(index: IndexFile, badge: Badge): IndexEntry[] {\n return index.plugins.filter((p) => latestVersion(p)?.badges.includes(badge));\n}\n","import type { IndexEntry, IndexFile } from \"@michaelfromyeg/loom-schema\";\n\n/** The inline server.json shape the MCP Registry serves (subset; spec §10). */\nexport interface McpServerJson {\n name: string;\n description?: string;\n version: string;\n repository?: { url?: string };\n}\nexport interface McpServerResponse {\n server: McpServerJson;\n _meta?: unknown;\n}\n\ntype FetchLike = (url: string) => Promise<{ json(): Promise<unknown> }>;\n\n/**\n * Fetch the official MCP Registry's `GET /v0.1/servers` (spec §10). `fetchImpl` is\n * injectable so tests run offline; the live default uses global fetch. We ingest\n * the registry's own scheme rather than reinventing it.\n */\nexport async function fetchMcpRegistry(\n opts: { baseUrl?: string; limit?: number; fetchImpl?: FetchLike } = {},\n): Promise<McpServerResponse[]> {\n const base = opts.baseUrl ?? \"https://registry.modelcontextprotocol.io\";\n const f = opts.fetchImpl ?? (globalThis.fetch as unknown as FetchLike);\n const res = await f(`${base}/v0.1/servers?limit=${opts.limit ?? 30}`);\n const data = (await res.json()) as { servers?: McpServerResponse[] };\n return data.servers ?? [];\n}\n\n/** Map MCP Registry servers into MCP-only index entries. */\nexport function mcpServersToEntries(servers: McpServerResponse[]): IndexEntry[] {\n return servers.map(({ server }) => ({\n id: server.name,\n source: server.repository?.url ?? \"mcp-registry\",\n versions: [\n {\n version: server.version,\n ref: server.version,\n sha: \"\",\n badges: [\"valid\"],\n harnessCoverage: [],\n },\n ],\n }));\n}\n\n/** Merge federated MCP entries into an index and stamp the ingestion. */\nexport function federate(\n index: IndexFile,\n servers: McpServerResponse[],\n ingestedAt: string,\n): IndexFile {\n const federated = [...(index.federated ?? []), { source: \"mcp-registry\", ingestedAt }];\n return { ...index, plugins: [...index.plugins, ...mcpServersToEntries(servers)], federated };\n}\n","import { gitInfo, lint } from \"@michaelfromyeg/loom-core\";\nimport type { IndexFile } from \"@michaelfromyeg/loom-schema\";\nimport { buildIndex, type IndexPluginInput } from \"./build\";\n\n/**\n * Build a metadata index from a set of plugin directories (spec §10, Phase 2\n * \"index builds from a set of plugins\"). Each plugin is statically validated for\n * the `valid` badge and stamped with its git ref/SHA. The richer `tested` badge\n * comes from the publish gate (which runs evals).\n */\nexport async function indexFromPluginDirs(\n dirs: string[],\n opts: { sourceFor?: (dir: string) => string } = {},\n): Promise<IndexFile> {\n const inputs: IndexPluginInput[] = [];\n for (const dir of dirs) {\n const linted = lint(dir);\n const { ref, sha } = await gitInfo(dir);\n inputs.push({\n id: linted.id,\n source: opts.sourceFor?.(dir) ?? dir,\n version: linted.plugin.version,\n ref,\n sha,\n badges: linted.diagnostics.hasErrors ? [] : [\"valid\"],\n harnessCoverage: [],\n });\n }\n return buildIndex(inputs);\n}\n","import { loadPluginDir } from \"@michaelfromyeg/loom-core\";\nimport { kindOf, refOf } from \"@michaelfromyeg/loom-schema\";\n\nexport interface ScanFinding {\n file: string;\n pattern: string;\n line: number;\n}\nexport interface ScanResult {\n clean: boolean;\n findings: ScanFinding[];\n}\n\n/**\n * A small built-in heuristic scanner for the `scanned` badge. It flags obviously\n * dangerous patterns in executable/hook/passthrough artifacts. Production would\n * also run garak / AI-Infra-Guard-style scanners; this is the offline default.\n */\nconst DANGEROUS: Array<{ re: RegExp; name: string }> = [\n { re: /rm\\s+-rf?\\s+[/~]/, name: \"recursive force delete of root/home\" },\n { re: /curl\\s+[^|]*\\|\\s*(sudo\\s+)?(ba)?sh/, name: \"curl-pipe-to-shell\" },\n { re: /wget\\s+[^|]*\\|\\s*(ba)?sh/, name: \"wget-pipe-to-shell\" },\n { re: /:\\(\\)\\s*\\{\\s*:\\s*\\|\\s*:&\\s*\\}\\s*;\\s*:/, name: \"fork bomb\" },\n { re: /\\b(AKIA[0-9A-Z]{16}|aws_secret_access_key)/i, name: \"embedded AWS credential\" },\n];\n\nexport function scanText(file: string, text: string): ScanFinding[] {\n const findings: ScanFinding[] = [];\n text.split(\"\\n\").forEach((line, i) => {\n for (const d of DANGEROUS) {\n if (d.re.test(line)) findings.push({ file, pattern: d.name, line: i + 1 });\n }\n });\n return findings;\n}\n\n/** Scan a plugin's executable/hook/passthrough artifacts (the `scanned` badge, §10). */\nexport function scanPlugin(pluginDir: string): ScanResult {\n const loaded = loadPluginDir(pluginDir);\n if (!loaded.ok) {\n return {\n clean: false,\n findings: [{ file: pluginDir, pattern: \"could not load plugin\", line: 0 }],\n };\n }\n const findings: ScanFinding[] = [];\n for (const c of loaded.value.plugin.components) {\n const kind = kindOf(c);\n if (kind !== \"hook\" && kind !== \"passthrough\") continue;\n const ref = refOf(c);\n const files = loaded.value.list(ref);\n for (const f of files.length > 0 ? files : [ref]) {\n try {\n findings.push(...scanText(f, loaded.value.read(f).toString(\"utf8\")));\n } catch {\n // unreadable artifact -- skip\n }\n }\n }\n return { clean: findings.length === 0, findings };\n}\n","import type { HarnessDriver } from \"@michaelfromyeg/loom-adapter-kit\";\nimport { type AdapterRegistry, type Diagnostic, lint } from \"@michaelfromyeg/loom-core\";\nimport { discoverEvals, type EvalReport, runEval } from \"@michaelfromyeg/loom-eval\";\nimport type { Badge, Target } from \"@michaelfromyeg/loom-schema\";\nimport { computeBadges } from \"./badges\";\nimport { type ScanResult, scanPlugin } from \"./scan\";\n\nexport interface PublishResult {\n id: string;\n version: string;\n /** The deterministic gate (spec §12): static valid + no failing trace/output runs. */\n ok: boolean;\n validPassed: boolean;\n evalFailed: boolean;\n badges: Badge[];\n harnessCoverage: Target[];\n diagnostics: Diagnostic[];\n evalReports: EvalReport[];\n scan: ScanResult;\n}\n\n/**\n * The deterministic publish gate (spec §9.1 step 9, §12): static validation always\n * runs and must pass; then every component's evals run against available harnesses,\n * and any FAILED trace/output run blocks the publish. Judge/differential are\n * advisory (reported `skipped`), and UNTESTED harnesses never block -- honest.\n */\nexport async function publishCheck(\n pluginDir: string,\n opts: {\n registry: AdapterRegistry;\n drivers: Record<Target, HarnessDriver>;\n /** Snapshot each harness's score into evals/.baselines/ for the next release. */\n snapshot?: boolean;\n },\n): Promise<PublishResult> {\n const linted = lint(pluginDir);\n const validPassed = !linted.diagnostics.hasErrors;\n\n const evalReports: EvalReport[] = [];\n if (validPassed) {\n for (const d of discoverEvals(pluginDir)) {\n evalReports.push(\n await runEval({\n evalFile: d.evalFile,\n pluginDir,\n componentLeaf: d.componentLeaf,\n registry: opts.registry,\n drivers: opts.drivers,\n snapshotBaselines: opts.snapshot,\n }),\n );\n }\n }\n\n const evalFailed = evalReports.some((r) =>\n r.harnesses.some((h) => h.status === \"tested\" && !h.pass),\n );\n const scan = validPassed ? scanPlugin(pluginDir) : { clean: false, findings: [] };\n const { badges, harnessCoverage } = computeBadges({\n validPassed,\n evalReports,\n scanClean: scan.clean,\n });\n\n return {\n id: linted.id,\n version: linted.plugin.version,\n ok: validPassed && !evalFailed,\n validPassed,\n evalFailed,\n badges,\n harnessCoverage,\n diagnostics: linted.diagnostics.items,\n evalReports,\n scan,\n };\n}\n","import type { IndexFile } from \"@michaelfromyeg/loom-schema\";\n\n/**\n * Opt-in, aggregate-only install record (spec §2, §10): increments an entry's\n * `installs`. There is no per-user data -- telemetry is a count, never identity.\n */\nexport function recordInstall(index: IndexFile, id: string): IndexFile {\n return {\n ...index,\n plugins: index.plugins.map((p) =>\n p.id === id\n ? {\n ...p,\n telemetry: {\n installs: (p.telemetry?.installs ?? 0) + 1,\n activeUsage: p.telemetry?.activeUsage ?? 0,\n },\n }\n : p,\n ),\n };\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@michaelfromyeg/loom-index",
3
+ "version": "0.1.0",
4
+ "description": "Loom metadata index: builder, client, MCP-Registry federation, badges, publish gate.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "dependencies": {
19
+ "@michaelfromyeg/loom-adapter-kit": "0.1.0",
20
+ "@michaelfromyeg/loom-schema": "0.1.0",
21
+ "@michaelfromyeg/loom-eval": "0.1.0",
22
+ "@michaelfromyeg/loom-core": "0.1.0"
23
+ },
24
+ "license": "MIT",
25
+ "author": "Michael DeMarco",
26
+ "homepage": "https://github.com/michaelfromyeg/loom#readme",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/michaelfromyeg/loom.git",
30
+ "directory": "packages/index"
31
+ },
32
+ "bugs": {
33
+ "url": "https://github.com/michaelfromyeg/loom/issues"
34
+ },
35
+ "keywords": [
36
+ "loom",
37
+ "coding-agent",
38
+ "ai-agent",
39
+ "claude-code",
40
+ "codex",
41
+ "cursor",
42
+ "copilot",
43
+ "opencode",
44
+ "mcp",
45
+ "plugin",
46
+ "skill",
47
+ "compiler"
48
+ ],
49
+ "publishConfig": {
50
+ "access": "public"
51
+ },
52
+ "scripts": {
53
+ "build": "tsup"
54
+ }
55
+ }