@davesheffer/hunch 1.18.1 → 1.19.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.
@@ -4,14 +4,31 @@ import { posix } from "node:path";
4
4
  import { TextDecoder } from "node:util";
5
5
  import { compareCodeUnits } from "../core/canonicalOrder.js";
6
6
  import { resourceId, resourceRelationshipId } from "../core/ids.js";
7
+ import { parseJsonc } from "../core/jsonc.js";
8
+ import { parseSource } from "./parse.js";
7
9
  import { EdgeSchema, ResourceSchema, isCredentialFreeText, } from "../core/types.js";
8
10
  import { canonicalRemoteRepositoryIdentity, foreignRepoEnv, gitNullDevice, isGitRepo, } from "./git.js";
9
11
  export const LANDSCAPE_DISCOVERY_SCHEMA_VERSION = "hunch.landscape-discovery/1";
10
12
  export const LANDSCAPE_CANDIDATE_SCHEMA_VERSION = "hunch.landscape-candidate/1";
11
13
  const MAX_MANIFEST_BYTES = 1024 * 1024;
12
14
  const MAX_MANIFESTS = 128;
15
+ const MAX_MCP_CONFIG_BYTES = 256 * 1024;
16
+ const MAX_MCP_DECLARATIONS = 128;
17
+ const MAX_DELIVERY_DECLARATION_BYTES = 256 * 1024;
18
+ const MAX_DELIVERY_DECLARATIONS = 128;
13
19
  const ORDINARY_BLOB_MODES = new Set(["100644", "100755"]);
14
20
  const UTF8_DECODER = new TextDecoder("utf-8", { fatal: true });
21
+ const MCP_CONFIG_SPECS = [
22
+ { path: ".mcp.json", format: "jsonc", rootKey: "mcpServers" },
23
+ { path: ".agents/mcp_config.json", format: "jsonc", rootKey: "mcpServers" },
24
+ { path: ".codex/config.toml", format: "codex_toml" },
25
+ { path: ".cursor/mcp.json", format: "jsonc", rootKey: "mcpServers" },
26
+ { path: ".vscode/mcp.json", format: "jsonc", rootKey: "servers" },
27
+ { path: ".windsurf/mcp_config.json", format: "jsonc", rootKey: "mcpServers" },
28
+ { path: "plugin/.mcp.json", format: "jsonc", rootKey: "mcpServers" },
29
+ { path: "server.json", format: "registry_json" },
30
+ ];
31
+ const MCP_CONFIG_BY_PATH = new Map(MCP_CONFIG_SPECS.map((spec) => [spec.path, spec]));
15
32
  function gitEnv() {
16
33
  return {
17
34
  ...foreignRepoEnv(process.env),
@@ -218,6 +235,1062 @@ function parseManifests(blobs, issues) {
218
235
  }
219
236
  return parsed;
220
237
  }
238
+ function mcpConfigBlobs(root, revision) {
239
+ const raw = gitBuffer(root, [
240
+ "ls-tree", "--full-tree", "-z", revision, "--", ...MCP_CONFIG_SPECS.map((spec) => spec.path),
241
+ ], 4 * 1024 * 1024);
242
+ const blobs = [];
243
+ for (const record of nulRecords(raw)) {
244
+ const tab = record.indexOf(0x09);
245
+ if (tab < 0)
246
+ continue;
247
+ const head = record.subarray(0, tab).toString("ascii").match(/^([0-7]{6}) (blob|tree|commit) ([0-9a-f]{40,64})$/i);
248
+ if (!head)
249
+ continue;
250
+ let path;
251
+ try {
252
+ path = UTF8_DECODER.decode(record.subarray(tab + 1));
253
+ }
254
+ catch {
255
+ continue;
256
+ }
257
+ if (!MCP_CONFIG_BY_PATH.has(path))
258
+ continue;
259
+ const mode = head[2] === "blob" ? head[1] : `${head[2]}:${head[1]}`;
260
+ blobs.push({ path, mode, oid: head[3].toLowerCase(), bytes: null, contentHash: null });
261
+ }
262
+ return blobs.sort((left, right) => compareCodeUnits(left.path, right.path)).map((blob) => {
263
+ if (!ORDINARY_BLOB_MODES.has(blob.mode))
264
+ return blob;
265
+ const size = Number(gitText(root, ["cat-file", "-s", blob.oid], 1024 * 1024));
266
+ if (!Number.isSafeInteger(size) || size < 0 || size > MAX_MCP_CONFIG_BYTES) {
267
+ return { ...blob, contentHash: size > MAX_MCP_CONFIG_BYTES ? "oversized" : null };
268
+ }
269
+ const bytes = gitBuffer(root, ["cat-file", "blob", blob.oid], MAX_MCP_CONFIG_BYTES + 1);
270
+ return { ...blob, bytes, contentHash: sha256Bytes(bytes) };
271
+ });
272
+ }
273
+ function validMcpServerName(value) {
274
+ const name = value.trim();
275
+ return name.length > 0 && name.length <= 128
276
+ && /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name)
277
+ && isCredentialFreeText(name)
278
+ ? name
279
+ : null;
280
+ }
281
+ function safeMcpSourceField(rootKey, rawName) {
282
+ const name = validMcpServerName(rawName);
283
+ return name ? `${rootKey}.${name}` : `${rootKey}[sha256:${createHash("sha256").update(rawName).digest("hex")}]`;
284
+ }
285
+ function safeMcpUrl(value) {
286
+ if (typeof value !== "string" || value.length > 2048 || !isCredentialFreeText(value))
287
+ return null;
288
+ try {
289
+ const url = new URL(value);
290
+ if (!/^https?:$/.test(url.protocol) || url.username || url.password || url.search || url.hash)
291
+ return null;
292
+ return url.href.endsWith("/") ? url.href.slice(0, -1) : url.href;
293
+ }
294
+ catch {
295
+ return null;
296
+ }
297
+ }
298
+ function stripTomlComment(input) {
299
+ let quote = null;
300
+ let escaped = false;
301
+ for (let index = 0; index < input.length; index += 1) {
302
+ const char = input[index];
303
+ if (quote === "\"") {
304
+ if (escaped)
305
+ escaped = false;
306
+ else if (char === "\\")
307
+ escaped = true;
308
+ else if (char === quote)
309
+ quote = null;
310
+ continue;
311
+ }
312
+ if (quote === "'") {
313
+ if (char === quote)
314
+ quote = null;
315
+ continue;
316
+ }
317
+ if (char === "\"" || char === "'")
318
+ quote = char;
319
+ else if (char === "#")
320
+ return input.slice(0, index);
321
+ }
322
+ return input;
323
+ }
324
+ function tomlStructure(input) {
325
+ let quote = null;
326
+ let escaped = false;
327
+ let depth = 0;
328
+ for (const char of input) {
329
+ if (quote === "\"") {
330
+ if (escaped)
331
+ escaped = false;
332
+ else if (char === "\\")
333
+ escaped = true;
334
+ else if (char === quote)
335
+ quote = null;
336
+ continue;
337
+ }
338
+ if (quote === "'") {
339
+ if (char === quote)
340
+ quote = null;
341
+ continue;
342
+ }
343
+ if (char === "\"" || char === "'")
344
+ quote = char;
345
+ else if (char === "[" || char === "{")
346
+ depth += 1;
347
+ else if (char === "]" || char === "}")
348
+ depth -= 1;
349
+ if (depth < 0)
350
+ throw new Error("unbalanced TOML structure");
351
+ }
352
+ return { depth, closedQuote: quote === null };
353
+ }
354
+ function tomlStatements(input) {
355
+ const statements = [];
356
+ let pending = "";
357
+ for (const rawLine of input.split(/\r?\n/)) {
358
+ const line = stripTomlComment(rawLine).trim();
359
+ if (!line)
360
+ continue;
361
+ pending = pending ? `${pending}\n${line}` : line;
362
+ const state = tomlStructure(pending);
363
+ if (state.depth === 0 && state.closedQuote) {
364
+ statements.push(pending);
365
+ pending = "";
366
+ }
367
+ }
368
+ if (pending)
369
+ throw new Error("unterminated TOML statement");
370
+ return statements;
371
+ }
372
+ function parseTomlString(input) {
373
+ const value = input.trim();
374
+ if (value.length < 2)
375
+ throw new Error("TOML string expected");
376
+ if (value.startsWith("'")) {
377
+ if (!value.endsWith("'") || value.slice(1, -1).includes("'"))
378
+ throw new Error("invalid literal TOML string");
379
+ return value.slice(1, -1);
380
+ }
381
+ if (!value.startsWith("\"") || !value.endsWith("\""))
382
+ throw new Error("TOML string expected");
383
+ const parsed = JSON.parse(value);
384
+ if (typeof parsed !== "string")
385
+ throw new Error("TOML string expected");
386
+ return parsed;
387
+ }
388
+ function parseTomlKeyPath(input) {
389
+ const parts = [];
390
+ let index = 0;
391
+ const skipSpace = () => {
392
+ while (/\s/.test(input[index] ?? ""))
393
+ index += 1;
394
+ };
395
+ while (index < input.length) {
396
+ skipSpace();
397
+ const char = input[index];
398
+ if (char === "\"" || char === "'") {
399
+ const start = index;
400
+ index += 1;
401
+ let escaped = false;
402
+ for (; index < input.length; index += 1) {
403
+ const current = input[index];
404
+ if (char === "\"" && escaped)
405
+ escaped = false;
406
+ else if (char === "\"" && current === "\\")
407
+ escaped = true;
408
+ else if (current === char)
409
+ break;
410
+ }
411
+ if (index >= input.length)
412
+ throw new Error("unterminated quoted TOML key");
413
+ index += 1;
414
+ parts.push(parseTomlString(input.slice(start, index)));
415
+ }
416
+ else {
417
+ const match = input.slice(index).match(/^[A-Za-z0-9_-]+/);
418
+ if (!match)
419
+ throw new Error("invalid TOML key");
420
+ parts.push(match[0]);
421
+ index += match[0].length;
422
+ }
423
+ skipSpace();
424
+ if (index === input.length)
425
+ break;
426
+ if (input[index] !== ".")
427
+ throw new Error("invalid dotted TOML key");
428
+ index += 1;
429
+ }
430
+ if (!parts.length)
431
+ throw new Error("empty TOML key");
432
+ return parts;
433
+ }
434
+ function splitTomlAssignment(input) {
435
+ let quote = null;
436
+ let escaped = false;
437
+ for (let index = 0; index < input.length; index += 1) {
438
+ const char = input[index];
439
+ if (quote === "\"") {
440
+ if (escaped)
441
+ escaped = false;
442
+ else if (char === "\\")
443
+ escaped = true;
444
+ else if (char === quote)
445
+ quote = null;
446
+ continue;
447
+ }
448
+ if (quote === "'") {
449
+ if (char === quote)
450
+ quote = null;
451
+ continue;
452
+ }
453
+ if (char === "\"" || char === "'")
454
+ quote = char;
455
+ else if (char === "=")
456
+ return [input.slice(0, index).trim(), input.slice(index + 1).trim()];
457
+ }
458
+ throw new Error("TOML assignment expected");
459
+ }
460
+ function parseTomlStringArray(input) {
461
+ const value = input.trim();
462
+ if (!value.startsWith("[") || !value.endsWith("]"))
463
+ throw new Error("TOML string array expected");
464
+ const body = value.slice(1, -1);
465
+ const items = [];
466
+ let start = 0;
467
+ let quote = null;
468
+ let escaped = false;
469
+ for (let index = 0; index <= body.length; index += 1) {
470
+ const char = body[index];
471
+ if (quote === "\"") {
472
+ if (escaped)
473
+ escaped = false;
474
+ else if (char === "\\")
475
+ escaped = true;
476
+ else if (char === quote)
477
+ quote = null;
478
+ continue;
479
+ }
480
+ if (quote === "'") {
481
+ if (char === quote)
482
+ quote = null;
483
+ continue;
484
+ }
485
+ if (char === "\"" || char === "'") {
486
+ quote = char;
487
+ continue;
488
+ }
489
+ if (char === "," || index === body.length) {
490
+ const item = body.slice(start, index).trim();
491
+ if (item)
492
+ items.push(parseTomlString(item));
493
+ start = index + 1;
494
+ }
495
+ }
496
+ if (quote)
497
+ throw new Error("unterminated TOML array string");
498
+ return items;
499
+ }
500
+ function parseCodexMcpServers(input) {
501
+ const servers = {};
502
+ let current = null;
503
+ for (const statement of tomlStatements(input)) {
504
+ if (statement.startsWith("[")) {
505
+ if (!statement.endsWith("]") || statement.startsWith("[[") || statement.endsWith("]]")) {
506
+ throw new Error("unsupported TOML table");
507
+ }
508
+ const path = parseTomlKeyPath(statement.slice(1, -1));
509
+ if (path[0] !== "mcp_servers") {
510
+ current = null;
511
+ continue;
512
+ }
513
+ if (path.length !== 2 || Object.hasOwn(servers, path[1]))
514
+ throw new Error("invalid MCP TOML table");
515
+ current = path[1];
516
+ servers[current] = {};
517
+ continue;
518
+ }
519
+ if (!current)
520
+ continue;
521
+ const [rawKey, rawValue] = splitTomlAssignment(statement);
522
+ const keyPath = parseTomlKeyPath(rawKey);
523
+ if (keyPath.length !== 1)
524
+ throw new Error("invalid MCP TOML key");
525
+ const key = keyPath[0];
526
+ if (!new Set(["command", "url", "args"]).has(key))
527
+ continue;
528
+ if (Object.hasOwn(servers[current], key))
529
+ throw new Error("duplicate MCP TOML key");
530
+ servers[current][key] = key === "args" ? parseTomlStringArray(rawValue) : parseTomlString(rawValue);
531
+ }
532
+ return servers;
533
+ }
534
+ function validMcpRegistryName(value) {
535
+ if (typeof value !== "string")
536
+ return null;
537
+ const name = value.trim();
538
+ return name.length > 0 && name.length <= 256
539
+ && /^[A-Za-z0-9][A-Za-z0-9._-]*(?:\/[A-Za-z0-9][A-Za-z0-9._-]*)+$/.test(name)
540
+ && isCredentialFreeText(name)
541
+ ? name
542
+ : null;
543
+ }
544
+ function safeRegistryText(value, maxLength) {
545
+ if (typeof value !== "string")
546
+ return null;
547
+ const text = value.trim();
548
+ return text && text.length <= maxLength && !/[\u0000-\u001f\u007f]/.test(text) && isCredentialFreeText(text)
549
+ ? text
550
+ : null;
551
+ }
552
+ function registryMcpDeclarations(parsed, blob, revision, issues) {
553
+ const invalid = () => {
554
+ issues.push({
555
+ code: "mcp_declaration_invalid",
556
+ sourcePath: blob.path,
557
+ sourceField: "name",
558
+ detail: "registry MCP data must declare one bounded package or credential-free HTTP remote transport",
559
+ });
560
+ return [];
561
+ };
562
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
563
+ return invalid();
564
+ const manifest = parsed;
565
+ if (typeof manifest.$schema !== "string"
566
+ || !/^https:\/\/static\.modelcontextprotocol\.io\/schemas\/.+\/server\.schema\.json$/.test(manifest.$schema)) {
567
+ return [];
568
+ }
569
+ const name = validMcpRegistryName(manifest.name);
570
+ if (!name) {
571
+ issues.push({
572
+ code: "mcp_server_name_invalid",
573
+ sourcePath: blob.path,
574
+ sourceField: "name",
575
+ detail: "the registry manifest uses an invalid or credential-bearing MCP server name",
576
+ });
577
+ return [];
578
+ }
579
+ const evidence = {
580
+ kind: "mcp_declaration",
581
+ sourcePath: blob.path,
582
+ sourceField: "name",
583
+ sourceRevision: revision,
584
+ sourceContentHash: blob.contentHash,
585
+ };
586
+ const packages = manifest.packages === undefined ? [] : manifest.packages;
587
+ const remotes = manifest.remotes === undefined ? [] : manifest.remotes;
588
+ if (!Array.isArray(packages) || !Array.isArray(remotes) || packages.length > 32 || remotes.length > 32)
589
+ return invalid();
590
+ const packageDescriptors = [];
591
+ for (const value of packages) {
592
+ if (!value || typeof value !== "object" || Array.isArray(value))
593
+ return invalid();
594
+ const item = value;
595
+ const transport = item.transport;
596
+ if (!transport || typeof transport !== "object" || Array.isArray(transport)
597
+ || transport.type !== "stdio")
598
+ return invalid();
599
+ const registryType = safeRegistryText(item.registryType, 64);
600
+ const identifier = safeRegistryText(item.identifier, 256);
601
+ const version = safeRegistryText(item.version, 128);
602
+ if (!registryType || !identifier || !version)
603
+ return invalid();
604
+ packageDescriptors.push({ registryType, identifier, version });
605
+ }
606
+ const remoteLocators = [];
607
+ for (const value of remotes) {
608
+ if (!value || typeof value !== "object" || Array.isArray(value))
609
+ return invalid();
610
+ const item = value;
611
+ const type = item.type;
612
+ const locator = safeMcpUrl(item.url);
613
+ if ((type !== "sse" && type !== "streamable-http") || !locator)
614
+ return invalid();
615
+ remoteLocators.push(locator);
616
+ }
617
+ const declarations = [];
618
+ if (packageDescriptors.length) {
619
+ const descriptors = packageDescriptors.sort((left, right) => compareCodeUnits(`${left.registryType}:${left.identifier}:${left.version}`, `${right.registryType}:${right.identifier}:${right.version}`));
620
+ declarations.push({
621
+ key: name.toLowerCase(),
622
+ name,
623
+ transport: "stdio",
624
+ relationship: "provides",
625
+ locator: null,
626
+ descriptorHash: contentHash({ transport: "stdio", packages: descriptors }),
627
+ evidence,
628
+ });
629
+ }
630
+ for (const locator of [...new Set(remoteLocators)].sort(compareCodeUnits)) {
631
+ declarations.push({
632
+ key: name.toLowerCase(),
633
+ name,
634
+ transport: "http",
635
+ relationship: "provides",
636
+ locator,
637
+ descriptorHash: contentHash({ transport: "http", locator }),
638
+ evidence,
639
+ });
640
+ }
641
+ return declarations.length ? declarations : invalid();
642
+ }
643
+ function mcpDeclaration(rawName, rawEntry, blob, rootKey, revision, issues) {
644
+ const sourceField = safeMcpSourceField(rootKey, rawName);
645
+ const name = validMcpServerName(rawName);
646
+ if (!name) {
647
+ issues.push({
648
+ code: "mcp_server_name_invalid",
649
+ sourcePath: blob.path,
650
+ sourceField,
651
+ detail: "an MCP declaration uses an invalid or credential-bearing server name",
652
+ });
653
+ return null;
654
+ }
655
+ if (!rawEntry || typeof rawEntry !== "object" || Array.isArray(rawEntry)) {
656
+ issues.push({ code: "mcp_declaration_invalid", sourcePath: blob.path, sourceField, detail: `MCP server ${name} must be an object declaration` });
657
+ return null;
658
+ }
659
+ const entry = rawEntry;
660
+ const rawUrl = entry.url ?? entry.serverUrl;
661
+ const locator = rawUrl === undefined ? null : safeMcpUrl(rawUrl);
662
+ const command = typeof entry.command === "string" ? entry.command.trim() : null;
663
+ const args = entry.args === undefined
664
+ ? []
665
+ : Array.isArray(entry.args) && entry.args.length <= 64 && entry.args.every((arg) => typeof arg === "string" && arg.length <= 1024)
666
+ ? entry.args
667
+ : null;
668
+ const validCommand = command !== null && command.length > 0 && command.length <= 1024
669
+ && !/[\u0000-\u001f\u007f]/.test(command) && isCredentialFreeText(command);
670
+ if ((rawUrl !== undefined && locator === null) || args === null
671
+ || (rawUrl === undefined && !validCommand) || (rawUrl !== undefined && command !== null)) {
672
+ issues.push({
673
+ code: "mcp_declaration_invalid",
674
+ sourcePath: blob.path,
675
+ sourceField,
676
+ detail: `MCP server ${name} must declare one credential-free HTTP URL or one bounded stdio command`,
677
+ });
678
+ return null;
679
+ }
680
+ const transport = locator ? "http" : "stdio";
681
+ const descriptorHash = locator
682
+ ? contentHash({ transport, locator })
683
+ : contentHash({ transport, command, args });
684
+ return {
685
+ key: name.toLowerCase(),
686
+ name,
687
+ transport,
688
+ relationship: "depends_on",
689
+ locator,
690
+ descriptorHash,
691
+ evidence: {
692
+ kind: "mcp_declaration",
693
+ sourcePath: blob.path,
694
+ sourceField,
695
+ sourceRevision: revision,
696
+ sourceContentHash: blob.contentHash,
697
+ },
698
+ };
699
+ }
700
+ function mcpDeclarations(root, revision, issues) {
701
+ const declarations = [];
702
+ let considered = 0;
703
+ for (const blob of mcpConfigBlobs(root, revision)) {
704
+ if (!blob.bytes) {
705
+ issues.push({
706
+ code: blob.contentHash === "oversized" ? "mcp_config_oversized" : "mcp_config_mode",
707
+ sourcePath: blob.path,
708
+ sourceField: "",
709
+ detail: blob.contentHash === "oversized"
710
+ ? `${blob.path} exceeds the ${MAX_MCP_CONFIG_BYTES}-byte MCP configuration limit`
711
+ : `${blob.path} uses unsupported Git mode ${blob.mode}`,
712
+ });
713
+ continue;
714
+ }
715
+ const spec = MCP_CONFIG_BY_PATH.get(blob.path);
716
+ let parsed;
717
+ try {
718
+ parsed = spec.format === "codex_toml"
719
+ ? parseCodexMcpServers(blob.bytes.toString("utf8"))
720
+ : spec.format === "registry_json"
721
+ ? JSON.parse(blob.bytes.toString("utf8"))
722
+ : parseJsonc(blob.bytes.toString("utf8"));
723
+ }
724
+ catch {
725
+ issues.push({
726
+ code: "mcp_config_invalid",
727
+ sourcePath: blob.path,
728
+ sourceField: "",
729
+ detail: `${blob.path} is not valid ${spec.format === "codex_toml" ? "bounded MCP TOML" : spec.format === "registry_json" ? "JSON" : "JSON/JSONC"}`,
730
+ });
731
+ continue;
732
+ }
733
+ if (spec.format === "registry_json") {
734
+ const registryDeclarations = registryMcpDeclarations(parsed, blob, revision, issues);
735
+ if (!registryDeclarations.length)
736
+ continue;
737
+ const logicalCount = registryDeclarations.length;
738
+ if (considered + logicalCount > MAX_MCP_DECLARATIONS) {
739
+ issues.push({
740
+ code: "mcp_declaration_limit",
741
+ sourcePath: blob.path,
742
+ sourceField: "name",
743
+ detail: `bounded discovery accepts at most ${MAX_MCP_DECLARATIONS} MCP declarations`,
744
+ });
745
+ continue;
746
+ }
747
+ considered += logicalCount;
748
+ declarations.push(...registryDeclarations);
749
+ continue;
750
+ }
751
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
752
+ issues.push({ code: "mcp_config_invalid", sourcePath: blob.path, sourceField: "", detail: `${blob.path} must contain MCP table data` });
753
+ continue;
754
+ }
755
+ const rootKey = spec.format === "codex_toml" ? "mcp_servers" : spec.rootKey;
756
+ const servers = spec.format === "codex_toml" ? parsed : parsed[rootKey];
757
+ if (!servers || typeof servers !== "object" || Array.isArray(servers)) {
758
+ if (spec.format !== "codex_toml") {
759
+ issues.push({ code: "mcp_config_invalid", sourcePath: blob.path, sourceField: rootKey, detail: `${blob.path} must contain an object at ${rootKey}` });
760
+ }
761
+ continue;
762
+ }
763
+ const entries = Object.entries(servers).sort(([left], [right]) => compareCodeUnits(left, right));
764
+ if (considered + entries.length > MAX_MCP_DECLARATIONS) {
765
+ issues.push({
766
+ code: "mcp_declaration_limit",
767
+ sourcePath: blob.path,
768
+ sourceField: rootKey,
769
+ detail: `bounded discovery accepts at most ${MAX_MCP_DECLARATIONS} MCP declarations`,
770
+ });
771
+ }
772
+ const remaining = Math.max(0, MAX_MCP_DECLARATIONS - considered);
773
+ const selectedEntries = entries.slice(0, remaining);
774
+ considered += selectedEntries.length;
775
+ for (const [name, entry] of selectedEntries) {
776
+ const declaration = mcpDeclaration(name, entry, blob, rootKey, revision, issues);
777
+ if (declaration)
778
+ declarations.push(declaration);
779
+ }
780
+ }
781
+ return declarations.sort((left, right) => compareCodeUnits(`${left.key}:${left.descriptorHash}:${left.evidence.sourcePath}`, `${right.key}:${right.descriptorHash}:${right.evidence.sourcePath}`));
782
+ }
783
+ function deliveryDeclarationSpec(path) {
784
+ if (/^\.github\/workflows\/[^/]+\.ya?ml$/.test(path)) {
785
+ return {
786
+ evidenceKind: "ci_declaration",
787
+ resourceKind: "pipeline",
788
+ provider: "github_actions",
789
+ format: "yaml",
790
+ sourceField: "jobs",
791
+ relationship: "contains",
792
+ };
793
+ }
794
+ if (path === ".gitlab-ci.yml") {
795
+ return {
796
+ evidenceKind: "ci_declaration",
797
+ resourceKind: "pipeline",
798
+ provider: "gitlab_ci",
799
+ format: "yaml",
800
+ sourceField: "$",
801
+ relationship: "contains",
802
+ };
803
+ }
804
+ if (path === ".circleci/config.yml") {
805
+ return {
806
+ evidenceKind: "ci_declaration",
807
+ resourceKind: "pipeline",
808
+ provider: "circleci",
809
+ format: "yaml",
810
+ sourceField: "jobs",
811
+ relationship: "contains",
812
+ };
813
+ }
814
+ if (/^\.buildkite\/pipeline\.ya?ml$/.test(path)) {
815
+ return {
816
+ evidenceKind: "ci_declaration",
817
+ resourceKind: "pipeline",
818
+ provider: "buildkite",
819
+ format: "yaml",
820
+ sourceField: "steps",
821
+ relationship: "contains",
822
+ };
823
+ }
824
+ if (path === "Jenkinsfile") {
825
+ return {
826
+ evidenceKind: "ci_declaration",
827
+ resourceKind: "pipeline",
828
+ provider: "jenkins",
829
+ format: "jenkinsfile",
830
+ sourceField: "pipeline",
831
+ relationship: "contains",
832
+ };
833
+ }
834
+ if (/(^|\/)Dockerfile(?:\.[A-Za-z0-9][A-Za-z0-9._-]{0,127})?$/.test(path)) {
835
+ return {
836
+ evidenceKind: "deployment_declaration",
837
+ resourceKind: "artifact",
838
+ provider: "docker",
839
+ format: "dockerfile",
840
+ sourceField: "FROM",
841
+ relationship: "builds",
842
+ };
843
+ }
844
+ if (/(^|\/)(?:compose|docker-compose)\.ya?ml$/.test(path)) {
845
+ return {
846
+ evidenceKind: "deployment_declaration",
847
+ resourceKind: "deployment_target",
848
+ provider: "docker_compose",
849
+ format: "yaml",
850
+ sourceField: "services",
851
+ relationship: "deploys",
852
+ };
853
+ }
854
+ if (/(^|\/)(?:k8s|kubernetes|manifests|deploy)\/.+\.ya?ml$/.test(path)) {
855
+ return {
856
+ evidenceKind: "deployment_declaration",
857
+ resourceKind: "deployment_target",
858
+ provider: "kubernetes",
859
+ format: "kubernetes_yaml",
860
+ sourceField: "metadata.name",
861
+ relationship: "deploys",
862
+ };
863
+ }
864
+ if (/(^|\/)[^/]+\.service$/.test(path)) {
865
+ return {
866
+ evidenceKind: "deployment_declaration",
867
+ resourceKind: "deployment_target",
868
+ provider: "systemd",
869
+ format: "systemd_unit",
870
+ sourceField: "[Service]",
871
+ relationship: "deploys",
872
+ };
873
+ }
874
+ return null;
875
+ }
876
+ function safeDeclarationPath(path) {
877
+ const segments = path.split("/");
878
+ return path.length > 0
879
+ && path.length <= 1024
880
+ && !path.startsWith("/")
881
+ && !path.includes("\\")
882
+ && !/^[A-Za-z]:/.test(path)
883
+ && !/[\u0000-\u001f\u007f]/.test(path)
884
+ && !/(^|[._/-])(authorization|bearer|credential|password|passwd|private[_-]?key|secret|token|api[_-]?key)\s*[:=]/i.test(path)
885
+ && segments.every((segment) => !!segment && segment !== "." && segment !== "..")
886
+ && isCredentialFreeText(path);
887
+ }
888
+ function deliveryDeclarationBlobs(root, revision) {
889
+ const raw = gitBuffer(root, ["ls-tree", "--full-tree", "-r", "-z", revision], 64 * 1024 * 1024);
890
+ const discovered = [];
891
+ for (const record of nulRecords(raw)) {
892
+ const tab = record.indexOf(0x09);
893
+ if (tab < 0)
894
+ continue;
895
+ const head = record.subarray(0, tab).toString("ascii").match(/^([0-7]{6}) (blob|tree|commit) ([0-9a-f]{40,64})$/i);
896
+ if (!head)
897
+ continue;
898
+ const pathBytes = record.subarray(tab + 1);
899
+ let path;
900
+ try {
901
+ path = UTF8_DECODER.decode(pathBytes);
902
+ }
903
+ catch {
904
+ const approximate = pathBytes.toString("latin1");
905
+ if (deliveryDeclarationSpec(approximate)) {
906
+ discovered.push({
907
+ path: `<unsafe-delivery-declaration:sha256:${createHash("sha256").update(pathBytes).digest("hex")}>`,
908
+ mode: "unsafe-path",
909
+ oid: head[3].toLowerCase(),
910
+ bytes: null,
911
+ contentHash: null,
912
+ spec: null,
913
+ });
914
+ }
915
+ continue;
916
+ }
917
+ const spec = deliveryDeclarationSpec(path);
918
+ if (!spec)
919
+ continue;
920
+ if (!safeDeclarationPath(path)) {
921
+ discovered.push({
922
+ path: `<unsafe-delivery-declaration:sha256:${createHash("sha256").update(pathBytes).digest("hex")}>`,
923
+ mode: "unsafe-path",
924
+ oid: head[3].toLowerCase(),
925
+ bytes: null,
926
+ contentHash: null,
927
+ spec: null,
928
+ });
929
+ continue;
930
+ }
931
+ discovered.push({
932
+ path,
933
+ mode: head[2] === "blob" ? head[1] : `${head[2]}:${head[1]}`,
934
+ oid: head[3].toLowerCase(),
935
+ bytes: null,
936
+ contentHash: null,
937
+ spec,
938
+ });
939
+ }
940
+ discovered.sort((left, right) => compareCodeUnits(left.path, right.path));
941
+ const total = discovered.length;
942
+ const blobs = discovered.slice(0, MAX_DELIVERY_DECLARATIONS).map((blob) => {
943
+ if (blob.mode === "unsafe-path" || !ORDINARY_BLOB_MODES.has(blob.mode))
944
+ return blob;
945
+ const size = Number(gitText(root, ["cat-file", "-s", blob.oid], 1024 * 1024));
946
+ if (!Number.isSafeInteger(size) || size < 0 || size > MAX_DELIVERY_DECLARATION_BYTES) {
947
+ return { ...blob, contentHash: size > MAX_DELIVERY_DECLARATION_BYTES ? "oversized" : null };
948
+ }
949
+ const bytes = gitBuffer(root, ["cat-file", "blob", blob.oid], MAX_DELIVERY_DECLARATION_BYTES + 1);
950
+ return { ...blob, bytes, contentHash: sha256Bytes(bytes) };
951
+ });
952
+ return { blobs, total };
953
+ }
954
+ const KUBERNETES_WORKLOAD_KINDS = new Set(["Deployment", "StatefulSet", "DaemonSet", "Job", "CronJob", "Pod"]);
955
+ function stripYamlScalarComment(input) {
956
+ let quote = null;
957
+ let escaped = false;
958
+ for (let index = 0; index < input.length; index += 1) {
959
+ const char = input[index];
960
+ if (quote === "\"") {
961
+ if (escaped)
962
+ escaped = false;
963
+ else if (char === "\\")
964
+ escaped = true;
965
+ else if (char === quote)
966
+ quote = null;
967
+ continue;
968
+ }
969
+ if (quote === "'") {
970
+ if (char === "'" && input[index + 1] === "'")
971
+ index += 1;
972
+ else if (char === "'")
973
+ quote = null;
974
+ continue;
975
+ }
976
+ if (char === "\"" || char === "'")
977
+ quote = char;
978
+ else if (char === "#" && (index === 0 || /\s/.test(input[index - 1])))
979
+ return input.slice(0, index);
980
+ }
981
+ return input;
982
+ }
983
+ function boundedYamlScalar(input) {
984
+ const value = stripYamlScalarComment(input).trim();
985
+ if (!value || value.length > 512)
986
+ return null;
987
+ let parsed;
988
+ if (value.startsWith("\"")) {
989
+ if (!value.endsWith("\""))
990
+ return null;
991
+ try {
992
+ const decoded = JSON.parse(value);
993
+ if (typeof decoded !== "string")
994
+ return null;
995
+ parsed = decoded;
996
+ }
997
+ catch {
998
+ return null;
999
+ }
1000
+ }
1001
+ else if (value.startsWith("'")) {
1002
+ if (!value.endsWith("'"))
1003
+ return null;
1004
+ const inner = value.slice(1, -1);
1005
+ let decoded = "";
1006
+ for (let index = 0; index < inner.length; index += 1) {
1007
+ const char = inner[index];
1008
+ if (char !== "'") {
1009
+ decoded += char;
1010
+ continue;
1011
+ }
1012
+ if (inner[index + 1] !== "'")
1013
+ return null;
1014
+ decoded += "'";
1015
+ index += 1;
1016
+ }
1017
+ parsed = decoded;
1018
+ }
1019
+ else {
1020
+ if (!/^[A-Za-z0-9][A-Za-z0-9._/@-]{0,255}$/.test(value))
1021
+ return null;
1022
+ parsed = value;
1023
+ }
1024
+ return parsed.length > 0 && parsed.length <= 256
1025
+ && !/[\u0000-\u001f\u007f]/.test(parsed)
1026
+ && isCredentialFreeText(parsed)
1027
+ ? parsed
1028
+ : null;
1029
+ }
1030
+ function kubernetesYamlDocuments(source) {
1031
+ return source
1032
+ .split(/^(?:---|\.\.\.)[ \t]*(?:#.*)?\r?$/m)
1033
+ .map((document, index) => ({ index, source: document }))
1034
+ .filter((document) => document.source.split(/\r?\n/)
1035
+ .some((line) => !!line.trim() && !line.trimStart().startsWith("#")));
1036
+ }
1037
+ function kubernetesDocumentHeader(source) {
1038
+ const header = {
1039
+ apiVersion: undefined,
1040
+ kind: undefined,
1041
+ name: undefined,
1042
+ namespace: undefined,
1043
+ duplicate: false,
1044
+ };
1045
+ let metadataIndent = null;
1046
+ let metadataChildIndent = null;
1047
+ const assign = (field, raw) => {
1048
+ if (header[field] !== undefined) {
1049
+ header.duplicate = true;
1050
+ return;
1051
+ }
1052
+ header[field] = boundedYamlScalar(raw);
1053
+ };
1054
+ for (const rawLine of source.split(/\r?\n/)) {
1055
+ if (!rawLine.trim() || rawLine.trimStart().startsWith("#"))
1056
+ continue;
1057
+ const indentation = rawLine.match(/^ */)[0].length;
1058
+ const mapping = rawLine.slice(indentation).match(/^([A-Za-z][A-Za-z0-9_-]*)[ \t]*:(.*)$/);
1059
+ if (indentation === 0) {
1060
+ metadataIndent = null;
1061
+ metadataChildIndent = null;
1062
+ if (!mapping)
1063
+ continue;
1064
+ const [_, key, raw] = mapping;
1065
+ if (key === "apiVersion" || key === "kind")
1066
+ assign(key, raw);
1067
+ else if (key === "metadata" && !stripYamlScalarComment(raw).trim())
1068
+ metadataIndent = 0;
1069
+ continue;
1070
+ }
1071
+ if (metadataIndent === null || indentation <= metadataIndent || !mapping)
1072
+ continue;
1073
+ if (metadataChildIndent === null)
1074
+ metadataChildIndent = indentation;
1075
+ if (indentation !== metadataChildIndent)
1076
+ continue;
1077
+ const [_, key, raw] = mapping;
1078
+ if (key === "name" || key === "namespace")
1079
+ assign(key, raw);
1080
+ }
1081
+ return header;
1082
+ }
1083
+ function validKubernetesApiVersion(value) {
1084
+ return value.length <= 128 && /^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?\/v[0-9][a-z0-9]*$|^v[0-9][a-z0-9]*$/i.test(value);
1085
+ }
1086
+ function validKubernetesName(value) {
1087
+ return value.length <= 253 && /^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/.test(value);
1088
+ }
1089
+ function validKubernetesNamespace(value) {
1090
+ return value.length <= 63 && /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(value);
1091
+ }
1092
+ function kubernetesDeclarations(blob, source, issues) {
1093
+ const candidates = [];
1094
+ for (const document of kubernetesYamlDocuments(source)) {
1095
+ const header = kubernetesDocumentHeader(document.source);
1096
+ if (header.duplicate) {
1097
+ issues.push({
1098
+ code: "delivery_declaration_invalid",
1099
+ sourcePath: blob.path,
1100
+ sourceField: `documents[${document.index}]`,
1101
+ detail: `${blob.path} document ${document.index} repeats a Kubernetes identity field`,
1102
+ });
1103
+ continue;
1104
+ }
1105
+ if (typeof header.kind !== "string" || !KUBERNETES_WORKLOAD_KINDS.has(header.kind))
1106
+ continue;
1107
+ const namespace = header.namespace === undefined ? "default" : header.namespace;
1108
+ if (typeof header.apiVersion !== "string" || !validKubernetesApiVersion(header.apiVersion)
1109
+ || typeof header.name !== "string" || !validKubernetesName(header.name)
1110
+ || typeof namespace !== "string" || !validKubernetesNamespace(namespace)) {
1111
+ issues.push({
1112
+ code: "delivery_declaration_invalid",
1113
+ sourcePath: blob.path,
1114
+ sourceField: `documents[${document.index}].metadata.name`,
1115
+ detail: `${blob.path} document ${document.index} has an incomplete or unsafe Kubernetes workload identity`,
1116
+ });
1117
+ continue;
1118
+ }
1119
+ const identity = `${header.apiVersion.toLowerCase()}/${header.kind.toLowerCase()}/${namespace}/${header.name}`;
1120
+ candidates.push({
1121
+ path: blob.path,
1122
+ contentHash: blob.contentHash,
1123
+ spec: blob.spec,
1124
+ sourceField: `documents[${document.index}].metadata.name`,
1125
+ identitySuffix: identity,
1126
+ displayName: `Kubernetes ${header.kind}: ${namespace}/${header.name}`,
1127
+ locatorSuffix: `#document=${document.index}`,
1128
+ contractVersion: header.apiVersion,
1129
+ metadata: {
1130
+ document_index: document.index,
1131
+ kubernetes_kind: header.kind,
1132
+ kubernetes_namespace: namespace,
1133
+ },
1134
+ identity,
1135
+ documentIndex: document.index,
1136
+ });
1137
+ }
1138
+ const byIdentity = new Map();
1139
+ for (const declaration of candidates) {
1140
+ const group = byIdentity.get(declaration.identity) ?? [];
1141
+ group.push(declaration);
1142
+ byIdentity.set(declaration.identity, group);
1143
+ }
1144
+ const declarations = [];
1145
+ for (const identity of [...byIdentity.keys()].sort(compareCodeUnits)) {
1146
+ const group = byIdentity.get(identity);
1147
+ if (group.length > 1) {
1148
+ issues.push({
1149
+ code: "delivery_declaration_conflict",
1150
+ sourcePath: blob.path,
1151
+ sourceField: "documents.metadata.name",
1152
+ detail: `${blob.path} repeats one Kubernetes workload identity across ${group.length} documents; identity remains unresolved`,
1153
+ });
1154
+ continue;
1155
+ }
1156
+ const { identity: _, documentIndex: __, ...declaration } = group[0];
1157
+ declarations.push(declaration);
1158
+ }
1159
+ return declarations;
1160
+ }
1161
+ function validDeliveryDeclaration(path, spec, source) {
1162
+ if (spec.format === "dockerfile") {
1163
+ return /^\s*FROM(?:\s+--platform=(?:"[^"]*"|'[^']*'|\S+))?\s+\S+/im.test(source);
1164
+ }
1165
+ if (spec.format === "jenkinsfile") {
1166
+ return /^\s*(?:pipeline|node)\s*\{/m.test(source);
1167
+ }
1168
+ if (spec.format === "systemd_unit") {
1169
+ return source.split(/\r?\n/).some((line) => /^\s*\[Service\]\s*$/.test(line));
1170
+ }
1171
+ const parsed = parseSource(path, source);
1172
+ if (!parsed?.parseable)
1173
+ return false;
1174
+ if (spec.provider === "github_actions" || spec.provider === "circleci")
1175
+ return /^jobs\s*:/m.test(source);
1176
+ if (spec.provider === "buildkite")
1177
+ return /^steps\s*:/m.test(source);
1178
+ if (spec.provider === "docker_compose")
1179
+ return /^services\s*:/m.test(source);
1180
+ return source.trim().length > 0;
1181
+ }
1182
+ function deliveryDeclarations(root, revision, issues) {
1183
+ const discovered = deliveryDeclarationBlobs(root, revision);
1184
+ let limitReported = false;
1185
+ const reportLimit = (sourcePath) => {
1186
+ if (limitReported)
1187
+ return;
1188
+ limitReported = true;
1189
+ issues.push({
1190
+ code: "delivery_declaration_limit",
1191
+ sourcePath,
1192
+ sourceField: "delivery",
1193
+ detail: `bounded discovery accepts at most ${MAX_DELIVERY_DECLARATIONS} delivery declarations`,
1194
+ });
1195
+ };
1196
+ if (discovered.total > MAX_DELIVERY_DECLARATIONS) {
1197
+ reportLimit(".");
1198
+ }
1199
+ const declarations = [];
1200
+ for (const blob of discovered.blobs) {
1201
+ if (!blob.bytes || !blob.spec) {
1202
+ const code = blob.contentHash === "oversized"
1203
+ ? "delivery_declaration_oversized"
1204
+ : blob.mode === "unsafe-path"
1205
+ ? "delivery_declaration_path"
1206
+ : "delivery_declaration_mode";
1207
+ issues.push({
1208
+ code,
1209
+ sourcePath: blob.path,
1210
+ sourceField: "",
1211
+ detail: blob.contentHash === "oversized"
1212
+ ? `${blob.path} exceeds the ${MAX_DELIVERY_DECLARATION_BYTES}-byte delivery declaration limit`
1213
+ : blob.mode === "unsafe-path"
1214
+ ? "a delivery declaration uses an unsafe path"
1215
+ : `${blob.path} uses unsupported Git mode ${blob.mode}`,
1216
+ });
1217
+ continue;
1218
+ }
1219
+ if (declarations.length >= MAX_DELIVERY_DECLARATIONS) {
1220
+ reportLimit(blob.path);
1221
+ continue;
1222
+ }
1223
+ let source;
1224
+ try {
1225
+ source = UTF8_DECODER.decode(blob.bytes);
1226
+ }
1227
+ catch {
1228
+ issues.push({
1229
+ code: "delivery_declaration_invalid",
1230
+ sourcePath: blob.path,
1231
+ sourceField: blob.spec.sourceField,
1232
+ detail: `${blob.path} is not valid UTF-8 declaration data`,
1233
+ });
1234
+ continue;
1235
+ }
1236
+ if (blob.spec.format === "kubernetes_yaml") {
1237
+ const parsed = parseSource(blob.path, source);
1238
+ if (!parsed?.parseable) {
1239
+ issues.push({
1240
+ code: "delivery_declaration_invalid",
1241
+ sourcePath: blob.path,
1242
+ sourceField: blob.spec.sourceField,
1243
+ detail: `${blob.path} is not structurally valid Kubernetes YAML`,
1244
+ });
1245
+ continue;
1246
+ }
1247
+ const workloads = kubernetesDeclarations(blob, source, issues);
1248
+ const remaining = MAX_DELIVERY_DECLARATIONS - declarations.length;
1249
+ if (workloads.length > remaining)
1250
+ reportLimit(blob.path);
1251
+ declarations.push(...workloads.slice(0, remaining));
1252
+ continue;
1253
+ }
1254
+ if (!validDeliveryDeclaration(blob.path, blob.spec, source)) {
1255
+ issues.push({
1256
+ code: "delivery_declaration_invalid",
1257
+ sourcePath: blob.path,
1258
+ sourceField: blob.spec.sourceField,
1259
+ detail: `${blob.path} is not a structurally valid ${blob.spec.provider} declaration`,
1260
+ });
1261
+ continue;
1262
+ }
1263
+ declarations.push({
1264
+ path: blob.path,
1265
+ contentHash: blob.contentHash,
1266
+ spec: blob.spec,
1267
+ metadata: blob.spec.provider === "systemd" ? { unit_name: posix.basename(blob.path) } : undefined,
1268
+ });
1269
+ }
1270
+ return declarations;
1271
+ }
1272
+ function deliveryResourceKey(declaration) {
1273
+ const prefix = declaration.spec.provider === "docker"
1274
+ ? "container-image"
1275
+ : declaration.spec.provider === "docker_compose"
1276
+ ? "docker-compose"
1277
+ : declaration.spec.provider.replaceAll("_", "-");
1278
+ const pathKey = `${prefix}/${declaration.path}`;
1279
+ return declaration.identitySuffix ? `${pathKey}#${declaration.identitySuffix}` : pathKey;
1280
+ }
1281
+ function deliveryResourceName(declaration) {
1282
+ if (declaration.displayName)
1283
+ return declaration.displayName.slice(0, 256);
1284
+ const base = posix.basename(declaration.path).replace(/\.ya?ml$/, "");
1285
+ const label = declaration.spec.resourceKind === "pipeline"
1286
+ ? `${declaration.spec.provider.replaceAll("_", " ")} pipeline: ${base}`
1287
+ : declaration.spec.resourceKind === "artifact"
1288
+ ? `container image declared by ${base}`
1289
+ : declaration.spec.provider === "systemd"
1290
+ ? `systemd service: ${base}`
1291
+ : `Docker Compose deployment: ${base}`;
1292
+ return label.slice(0, 256);
1293
+ }
221
1294
  function repositoryKey(identity) {
222
1295
  const safe = (key, locator) => {
223
1296
  if (key.length > 1900 || /[\u0000-\u001f\u007f]/.test(key) || !isCredentialFreeText(key)) {
@@ -376,6 +1449,8 @@ export function discoverRepositoryLandscape(root, ref = "HEAD") {
376
1449
  const patterns = workspacePatterns(rootManifest?.value.workspaces, issues);
377
1450
  const manifests = parsed.filter((manifest) => isWorkspaceManifest(manifest.path, patterns));
378
1451
  const declarations = repositoryDeclarations(root, revision, rootManifest);
1452
+ const discoveredMcp = mcpDeclarations(root, revision, issues);
1453
+ const discoveredDelivery = deliveryDeclarations(root, revision, issues);
379
1454
  const identities = [...new Set(declarations.map((declaration) => declaration.identity))].sort(compareCodeUnits);
380
1455
  let selected = null;
381
1456
  if (identities.length > 1) {
@@ -420,6 +1495,136 @@ export function discoverRepositoryLandscape(root, ref = "HEAD") {
420
1495
  });
421
1496
  resources.push(candidate(repositoryRecord, repositoryEvidence));
422
1497
  }
1498
+ const mcpByKey = new Map();
1499
+ for (const declaration of discoveredMcp) {
1500
+ const group = mcpByKey.get(declaration.key) ?? [];
1501
+ group.push(declaration);
1502
+ mcpByKey.set(declaration.key, group);
1503
+ }
1504
+ for (const key of [...mcpByKey.keys()].sort(compareCodeUnits)) {
1505
+ const group = mcpByKey.get(key);
1506
+ const descriptorHashes = [...new Set(group.map((item) => item.descriptorHash))].sort(compareCodeUnits);
1507
+ if (descriptorHashes.length > 1) {
1508
+ const first = group[0];
1509
+ issues.push({
1510
+ code: "mcp_declaration_conflict",
1511
+ sourcePath: first.evidence.sourcePath,
1512
+ sourceField: first.evidence.sourceField,
1513
+ detail: `MCP server ${first.name} has conflicting committed declarations; identity remains unresolved`,
1514
+ });
1515
+ continue;
1516
+ }
1517
+ const first = group[0];
1518
+ const evidence = group.map((item) => item.evidence);
1519
+ const declarationPaths = [...new Set(evidence.map((item) => item.sourcePath))].sort(compareCodeUnits);
1520
+ const mcpRecord = ResourceSchema.parse({
1521
+ schema: "hunch.resource/1",
1522
+ id: resourceId("mcp_server", `declared/${key}`),
1523
+ kind: "mcp_server",
1524
+ name: first.name,
1525
+ scope: repositoryRecord ? [repositoryRecord.id] : [],
1526
+ locator: first.locator,
1527
+ lifecycle: "active",
1528
+ provenance: {
1529
+ source: "extracted:mcp-declaration",
1530
+ confidence: 0.8,
1531
+ evidence: evidence.map(provenanceEvidence),
1532
+ },
1533
+ currentness: resourceCurrentness(revision, evidence.map((item) => item.sourceContentHash)),
1534
+ metadata: {
1535
+ discovery_authority: "candidate",
1536
+ transport: first.transport,
1537
+ declaration_paths: declarationPaths,
1538
+ },
1539
+ created_at: timestamp,
1540
+ updated_at: timestamp,
1541
+ });
1542
+ resources.push(candidate(mcpRecord, evidence));
1543
+ if (!repositoryRecord)
1544
+ continue;
1545
+ for (const relationshipType of ["depends_on", "provides"]) {
1546
+ const relationshipDeclarations = group.filter((item) => item.relationship === relationshipType);
1547
+ if (!relationshipDeclarations.length)
1548
+ continue;
1549
+ const relationshipEvidence = relationshipDeclarations.map((item) => item.evidence);
1550
+ const relationshipPaths = [...new Set(relationshipEvidence.map((item) => item.sourcePath))].sort(compareCodeUnits);
1551
+ const relationship = EdgeSchema.parse({
1552
+ schema: "hunch.resource-relationship/1",
1553
+ id: resourceRelationshipId(repositoryRecord.id, mcpRecord.id, relationshipType),
1554
+ from: repositoryRecord.id,
1555
+ to: mcpRecord.id,
1556
+ type: relationshipType,
1557
+ reason: relationshipType === "provides"
1558
+ ? `committed registry configuration declares repository-provided MCP server ${first.name}`
1559
+ : `committed project configuration declares MCP server dependency ${first.name}`,
1560
+ strength: 0.8,
1561
+ provenance: {
1562
+ source: "extracted:mcp-declaration",
1563
+ confidence: 0.8,
1564
+ evidence: relationshipEvidence.map(provenanceEvidence),
1565
+ },
1566
+ currentness: resourceCurrentness(revision, relationshipEvidence.map((item) => item.sourceContentHash)),
1567
+ environment: null,
1568
+ metadata: { discovery_authority: "candidate", declaration_paths: relationshipPaths },
1569
+ });
1570
+ relationships.push(candidate(relationship, relationshipEvidence));
1571
+ }
1572
+ }
1573
+ for (const declaration of discoveredDelivery) {
1574
+ const evidence = {
1575
+ kind: declaration.spec.evidenceKind,
1576
+ sourcePath: declaration.path,
1577
+ sourceField: declaration.sourceField ?? declaration.spec.sourceField,
1578
+ sourceRevision: revision,
1579
+ sourceContentHash: declaration.contentHash,
1580
+ };
1581
+ const deliveryRecord = ResourceSchema.parse({
1582
+ schema: "hunch.resource/1",
1583
+ id: resourceId(declaration.spec.resourceKind, deliveryResourceKey(declaration)),
1584
+ kind: declaration.spec.resourceKind,
1585
+ name: deliveryResourceName(declaration),
1586
+ scope: repositoryRecord ? [repositoryRecord.id] : [],
1587
+ locator: `${declaration.path}${declaration.locatorSuffix ?? ""}`,
1588
+ lifecycle: "active",
1589
+ contract_version: declaration.contractVersion,
1590
+ provenance: {
1591
+ source: `extracted:${declaration.spec.evidenceKind}`,
1592
+ confidence: 0.8,
1593
+ evidence: [provenanceEvidence(evidence)],
1594
+ },
1595
+ currentness: resourceCurrentness(revision, [declaration.contentHash]),
1596
+ metadata: {
1597
+ discovery_authority: "candidate",
1598
+ declaration_path: declaration.path,
1599
+ declaration_format: declaration.spec.format,
1600
+ provider: declaration.spec.provider,
1601
+ ...declaration.metadata,
1602
+ },
1603
+ created_at: timestamp,
1604
+ updated_at: timestamp,
1605
+ });
1606
+ resources.push(candidate(deliveryRecord, [evidence]));
1607
+ if (!repositoryRecord)
1608
+ continue;
1609
+ const relationship = EdgeSchema.parse({
1610
+ schema: "hunch.resource-relationship/1",
1611
+ id: resourceRelationshipId(repositoryRecord.id, deliveryRecord.id, declaration.spec.relationship),
1612
+ from: repositoryRecord.id,
1613
+ to: deliveryRecord.id,
1614
+ type: declaration.spec.relationship,
1615
+ reason: `${declaration.path} declares ${declaration.spec.provider} ${declaration.spec.resourceKind}`,
1616
+ strength: 0.8,
1617
+ provenance: {
1618
+ source: `extracted:${declaration.spec.evidenceKind}`,
1619
+ confidence: 0.8,
1620
+ evidence: [provenanceEvidence(evidence)],
1621
+ },
1622
+ currentness: resourceCurrentness(revision, [declaration.contentHash]),
1623
+ environment: null,
1624
+ metadata: { discovery_authority: "candidate", declaration_path: declaration.path },
1625
+ });
1626
+ relationships.push(candidate(relationship, [evidence]));
1627
+ }
423
1628
  for (const manifest of manifests) {
424
1629
  const rawName = typeof manifest.value.name === "string" ? manifest.value.name.trim() : "";
425
1630
  if (!rawName) {