{"version":3,"file":"repository.d.ts","sourceRoot":"","sources":["../../../src/core/mission/repository.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EACX,sBAAsB,EACtB,kBAAkB,EAClB,wBAAwB,EACxB,kBAAkB,EAClB,MAAM,YAAY,CAAC;AAMpB;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,GAAE,wBAAoC,GAAG,MAAM,CAwBvG;AAED,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,GAAG,kBAAkB,GAAG,SAAS,CAQxF;AAED,wBAAgB,sBAAsB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAEpE;AAMD;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAQnF;AAED,uFAAuF;AACvF,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CASpF;AAED,MAAM,WAAW,uBAAuB;IACvC,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,4EAA4E;IAC5E,gBAAgB,EAAE,MAAM,CAAC;CACzB;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC/B,KAAK,EAAE,uBAAuB,EAC9B,KAAK,SAAa,GAChB,sBAAsB,CAAC,kBAAkB,CAAC,CA+B5C;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAQtF;AAED,yEAAyE;AACzE,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAElF","sourcesContent":["/**\n * Durable Mission Graph — repository identity, worktree allocation and\n * isolation (2.0.0).\n *\n * Repository identity is authoritative: allocations bind to a canonical repo\n * id, never to a label. Worktrees are allocated with explicit isolation and\n * must not escape the repository root (symlink / Windows junction escape is\n * rejected). The operator's own worktree is never allocated to a mission.\n */\n\nimport * as path from \"node:path\";\nimport type {\n\tMissionOperationResult,\n\tRepositoryIdentity,\n\tRepositoryIdentityScheme,\n\tWorktreeAllocation,\n} from \"./types.js\";\n\n// =============================================================================\n// Identity\n// =============================================================================\n\n/**\n * Normalize a raw remote/URL into a canonical repository identity.\n * Lowercases, strips credentials, trims trailing slashes and `.git`, and\n * prefers the SSH-style host key when present.\n */\nexport function canonicalRepositoryId(raw: string, scheme: RepositoryIdentityScheme = \"git-url\"): string {\n\t// Idempotent: already-canonical identities pass through unchanged so that\n\t// double-canonicalization (declared set vs observed reference) cannot corrupt\n\t// identity comparison.\n\tif (raw.startsWith(\"repo:\")) return raw;\n\tlet s = raw.trim();\n\tif (/^(https?|ssh|git):\\/\\//i.test(s)) {\n\t\ttry {\n\t\t\tconst u = new URL(s);\n\t\t\tu.username = \"\";\n\t\t\tu.password = \"\";\n\t\t\tu.hash = \"\";\n\t\t\tu.search = \"\";\n\t\t\ts = u.href.replace(/\\/$/, \"\");\n\t\t} catch {\n\t\t\t// fall through to generic normalization\n\t\t}\n\t}\n\ts = s\n\t\t.replace(/^git@/, \"\")\n\t\t.replace(/\\.git$/, \"\")\n\t\t.replace(/\\/$/, \"\")\n\t\t.toLowerCase();\n\treturn `repo:${scheme}:${s}`;\n}\n\nexport function parseRepositoryIdentity(identity: string): RepositoryIdentity | undefined {\n\tif (!identity.startsWith(\"repo:\")) return undefined;\n\tconst rest = identity.slice(5);\n\tconst sep = rest.indexOf(\":\");\n\tif (sep === -1) return undefined;\n\tconst scheme = rest.slice(0, sep) as RepositoryIdentityScheme;\n\tconst id = rest.slice(sep + 1);\n\treturn { id: identity, scheme, label: id };\n}\n\nexport function sameRepositoryIdentity(a: string, b: string): boolean {\n\treturn canonicalRepositoryId(a) === canonicalRepositoryId(b);\n}\n\n// =============================================================================\n// Worktree allocation & isolation\n// =============================================================================\n\n/**\n * Determine whether a candidate worktree path escapes a repository root via a\n * symlink or (on Windows) a junction. Because `path.resolve` is purely lexical,\n * we conservatively reject any candidate path that resolves outside the root\n * directory, and reject paths containing path separators where the \"worktree\"\n * is expected to be a direct child of a root (isolation boundary).\n */\nexport function assertIsolationBoundary(root: string, worktreePath: string): boolean {\n\tif (!root || !worktreePath) return false;\n\tconst resolvedRoot = path.resolve(root);\n\tconst resolvedWt = path.resolve(worktreePath);\n\tconst rel = path.relative(resolvedRoot, resolvedWt);\n\tif (rel === \"\") return false; // same directory is not an allocation\n\tif (rel.startsWith(\"..\") || path.isAbsolute(rel)) return false; // escapes root\n\treturn true;\n}\n\n/** Check that a worktree path does not traverse a symlink/junction segment of root. */\nexport function blocksEscalatingSegments(root: string, worktreePath: string): boolean {\n\tconst resolvedRoot = path.resolve(root);\n\tconst resolvedWt = path.resolve(worktreePath);\n\tconst rel = path.relative(resolvedRoot, resolvedWt);\n\tif (rel === \"\" || rel.startsWith(\"..\") || path.isAbsolute(rel)) return false;\n\tfor (const seg of rel.split(path.sep)) {\n\t\tif (seg === \"..\" || seg === \"\") return false;\n\t}\n\treturn true;\n}\n\nexport interface WorktreeAllocationInput {\n\trepositoryId: string;\n\troot: string;\n\tworktreePath: string;\n\t/** The operator's own worktree base path, which must never be allocated. */\n\toperatorWorktree: string;\n}\n\n/**\n * Allocate a worktree record for a repository.\n * Rejects when the path is not within the root, when it equals the operator's\n * worktree, or when it violates the isolation boundary.\n */\nexport function allocateWorktree(\n\tinput: WorktreeAllocationInput,\n\tnowMs = Date.now(),\n): MissionOperationResult<WorktreeAllocation> {\n\tif (!assertIsolationBoundary(input.root, input.worktreePath)) {\n\t\treturn {\n\t\t\tok: false,\n\t\t\tcode: \"FORBIDDEN_MUTATION\",\n\t\t\terror: `worktree path ${input.worktreePath} escapes repository root ${input.root}`,\n\t\t};\n\t}\n\tif (!blocksEscalatingSegments(input.root, input.worktreePath)) {\n\t\treturn {\n\t\t\tok: false,\n\t\t\tcode: \"FORBIDDEN_MUTATION\",\n\t\t\terror: `worktree path ${input.worktreePath} contains escalation segments`,\n\t\t};\n\t}\n\tif (path.resolve(input.worktreePath) === path.resolve(input.operatorWorktree)) {\n\t\treturn {\n\t\t\tok: false,\n\t\t\tcode: \"FORBIDDEN_MUTATION\",\n\t\t\terror: \"operator worktree cannot be allocated to a mission\",\n\t\t};\n\t}\n\tvoid nowMs;\n\treturn {\n\t\tok: true,\n\t\tvalue: {\n\t\t\tpath: path.resolve(input.worktreePath),\n\t\t\trepositoryId: input.repositoryId,\n\t\t\tisolated: true,\n\t\t},\n\t};\n}\n\n/**\n * Detect whether an environment observation deviates from the declared scope.\n * Returns the set of observed repository identities not declared in the scope.\n */\nexport function detectRepositoryDrift(declared: string[], observed: string[]): string[] {\n\tconst declaredSet = new Set(declared.map((r) => canonicalRepositoryId(r)));\n\tconst drift: string[] = [];\n\tfor (const o of observed) {\n\t\tconst c = canonicalRepositoryId(o);\n\t\tif (!declaredSet.has(c)) drift.push(c);\n\t}\n\treturn [...new Set(drift)].sort();\n}\n\n/** Whether an observed repository is permitted by the declared scope. */\nexport function isRepositoryDeclared(declared: string[], observed: string): boolean {\n\treturn declared.some((d) => sameRepositoryIdentity(d, observed));\n}\n"]}