@wrongstack/techstack 0.291.0 → 0.291.2
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/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/registry/purl.ts", "../src/discovery/workspace.ts", "../src/adapters/npm.ts", "../src/adapters/paths.ts", "../src/adapters/python.ts", "../src/adapters/rust.ts", "../src/adapters/go.ts", "../src/adapters/dotnet.ts", "../src/adapters/php.ts", "../src/adapters/dart.ts", "../src/adapters/maven.ts", "../src/adapters/ruby.ts", "../src/adapters/elixir.ts", "../src/adapters/cpp.ts", "../src/snapshot-diff.ts", "../src/sbom.ts", "../src/remediation.ts", "../src/registry/client.ts", "../src/advisory/osv.ts", "../src/advisory/native-audit.ts", "../src/policy/status.ts", "../src/service.ts", "../src/research/triage.ts", "../src/research/llm.ts", "../src/research/researcher.ts", "../src/research/search.ts", "../src/store/sqlite.ts", "../src/store/schema.ts", "../src/delivery/coordinator.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * TechStack \u2014 Package URL (PURL) construction and parsing.\n *\n * PURL spec: https://github.com/package-url/purl-spec\n *\n * Every dependency that comes from a registry gets a PURL identifier\n * so it can be matched across ecosystems, deduplicated in aggregate views,\n * and queried against OSV's /v1/querybatch endpoint.\n *\n * @see docs/specs/techstack-sdd.md \u00A74.1, \u00A75\n */\n\nimport type { EcosystemId } from '../types.js';\n\n/**\n * Parsed PURL components.\n */\nexport interface PurlParts {\n readonly type: string;\n readonly namespace?: string | undefined;\n readonly name: string;\n readonly version?: string | undefined;\n readonly qualifiers?: ReadonlyMap<string, string> | undefined;\n readonly subpath?: string | undefined;\n}\n\n/**\n * Map ecosystem ids to PURL type strings.\n * Some ecosystems have different PURL types than their internal id.\n */\nconst ECOSYSTEM_TO_PURL_TYPE: Readonly<Record<EcosystemId, string>> = {\n npm: 'npm',\n python: 'pypi',\n rust: 'cargo',\n go: 'golang',\n dotnet: 'nuget',\n php: 'composer',\n dart: 'pub',\n maven: 'maven',\n gradle: 'maven',\n ruby: 'gem',\n swift: 'swift',\n elixir: 'hex',\n cpp: 'conan',\n};\n\n/**\n * Reverse map for parsing PURL type back to ecosystem id.\n */\nconst PURL_TYPE_TO_ECOSYSTEM: Readonly<Record<string, EcosystemId>> = {\n npm: 'npm',\n pypi: 'python',\n cargo: 'rust',\n golang: 'go',\n nuget: 'dotnet',\n composer: 'php',\n pub: 'dart',\n maven: 'maven',\n gem: 'ruby',\n swift: 'swift',\n hex: 'elixir',\n conan: 'cpp',\n};\n\n/**\n * Build a PURL string from components.\n *\n * @example\n * buildPurl({ type: 'npm', name: 'react', version: '19.1.0' })\n * // \u2192 'pkg:npm/react@19.1.0'\n *\n * buildPurl({ type: 'npm', namespace: '@types', name: 'node', version: '22.0.0' })\n * // \u2192 'pkg:npm/%40types/node@22.0.0'\n *\n * buildPurl({ type: 'maven', namespace: 'org.springframework', name: 'spring-core', version: '6.2.7' })\n * // \u2192 'pkg:maven/org.springframework/spring-core@6.2.7'\n *\n * buildPurl({ type: 'pypi', name: 'django', version: '5.2' })\n * // \u2192 'pkg:pypi/django@5.2'\n */\nexport function buildPurl(parts: PurlParts): string {\n const segments: string[] = ['pkg:', parts.type, '/'];\n\n if (parts.namespace) {\n segments.push(encodePurlSegment(parts.namespace), '/');\n }\n\n segments.push(encodePurlSegment(parts.name));\n\n if (parts.version) {\n segments.push('@', encodePurlSegment(parts.version));\n }\n\n if (parts.qualifiers && parts.qualifiers.size > 0) {\n const qs = [...parts.qualifiers.entries()]\n .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)\n .join('&');\n segments.push('?', qs);\n }\n\n if (parts.subpath) {\n segments.push('#', encodePurlSegment(parts.subpath));\n }\n\n return segments.join('');\n}\n\n/**\n * Parse a PURL string into its components.\n * Returns undefined for malformed PURLs.\n *\n * @example\n * parsePurl('pkg:npm/react@19.1.0')\n * // \u2192 { type: 'npm', name: 'react', version: '19.1.0' }\n *\n * parsePurl('pkg:maven/org.springframework/spring-core@6.2.7')\n * // \u2192 { type: 'maven', namespace: 'org.springframework', name: 'spring-core', version: '6.2.7' }\n */\nexport function parsePurl(purl: string): PurlParts | undefined {\n if (!purl.startsWith('pkg:')) return undefined;\n\n const withoutPrefix = purl.slice(4);\n\n // Split subpath\n let main = withoutPrefix;\n let subpath: string | undefined;\n const hashIdx = main.indexOf('#');\n if (hashIdx >= 0) {\n subpath = decodePurlSegment(main.slice(hashIdx + 1));\n main = main.slice(0, hashIdx);\n }\n\n // Split qualifiers\n let qualifiers: Map<string, string> | undefined;\n const qIdx = main.indexOf('?');\n if (qIdx >= 0) {\n const qs = main.slice(qIdx + 1);\n main = main.slice(0, qIdx);\n qualifiers = new Map<string, string>();\n for (const pair of qs.split('&')) {\n const eqIdx = pair.indexOf('=');\n if (eqIdx > 0) {\n qualifiers.set(\n decodeURIComponent(pair.slice(0, eqIdx)),\n decodeURIComponent(pair.slice(eqIdx + 1)),\n );\n }\n }\n }\n\n // Split version\n let version: string | undefined;\n const atIdx = main.lastIndexOf('@');\n if (atIdx > 0) {\n // Must be after the type prefix (i.e., not @scope)\n version = decodePurlSegment(main.slice(atIdx + 1));\n main = main.slice(0, atIdx);\n }\n\n // Split type\n const slashIdx = main.indexOf('/');\n if (slashIdx < 0) return undefined;\n const type = main.slice(0, slashIdx);\n let remainder = main.slice(slashIdx + 1);\n\n // Check for namespace (second /)\n let namespace: string | undefined;\n let name: string;\n const nsSlashIdx = remainder.indexOf('/');\n if (nsSlashIdx >= 0 && type !== 'npm') {\n // Most ecosystems use namespace/name; npm uses @scope/name encoded as %40scope/name\n namespace = decodePurlSegment(remainder.slice(0, nsSlashIdx));\n name = decodePurlSegment(remainder.slice(nsSlashIdx + 1));\n } else if (nsSlashIdx >= 0 && type === 'npm' && remainder.startsWith('%40')) {\n // npm scoped: %40scope/name\n namespace = decodePurlSegment(remainder.slice(0, nsSlashIdx));\n name = decodePurlSegment(remainder.slice(nsSlashIdx + 1));\n } else {\n name = decodePurlSegment(remainder);\n }\n\n if (!type || !name) return undefined;\n\n return {\n type,\n ...(namespace ? { namespace } : {}),\n name,\n ...(version ? { version } : {}),\n ...(qualifiers && qualifiers.size > 0 ? { qualifiers } : {}),\n ...(subpath ? { subpath } : {}),\n };\n}\n\n/**\n * Get the PURL type string for an ecosystem.\n */\nexport function purlTypeForEcosystem(ecosystem: EcosystemId): string {\n return ECOSYSTEM_TO_PURL_TYPE[ecosystem];\n}\n\n/**\n * Get the ecosystem id for a PURL type string.\n * Returns undefined for unknown types.\n */\nexport function ecosystemForPurlType(type: string): EcosystemId | undefined {\n return PURL_TYPE_TO_ECOSYSTEM[type];\n}\n\n/**\n * Construct a PURL string for a dependency in a given ecosystem.\n *\n * This is the spec-required convenience constructor. It composes the richer\n * `buildPurl` primitive with per-ecosystem namespace splitting so callers can\n * pass `name` in the ecosystem-native form (e.g. `\"@types/node\"` for npm\n * scoped packages, or `\"org.springframework/spring-core\"` for Maven groupId).\n *\n * Special-case: Go module paths (`github.com/gorilla/mux`) are written as a\n * single un-encoded name \u2014 the PURL spec treats `/` as the namespace\n * separator for most ecosystems but Go module paths use literal `/`s.\n *\n * @example\n * constructPurl('npm', 'react', '19.1.0') // \u2192 'pkg:npm/react@19.1.0'\n * constructPurl('npm', '@types/node', '22.0.0') // \u2192 'pkg:npm/%40types/node@22.0.0'\n * constructPurl('pypi', 'django', '5.2') // \u2192 'pkg:pypi/django@5.2'\n * constructPurl('maven', 'org.springframework/spring-core', '6.2.7')\n * // \u2192 'pkg:maven/org.springframework/spring-core@6.2.7'\n * constructPurl('go', 'github.com/gorilla/mux', '1.8.1')\n * // \u2192 'pkg:golang/github.com/gorilla/mux@1.8.1'\n */\nexport function constructPurl(\n ecosystem: EcosystemId,\n name: string,\n version?: string,\n): string {\n const type = purlTypeForEcosystem(ecosystem);\n // Go module paths contain literal slashes that are part of the name, not\n // a namespace separator. `buildPurl` encodes `/` in segments per the PURL\n // spec, but the Go ecosystem is the documented exception: module paths\n // like `github.com/gorilla/mux` stay literal. Build the Go PURL string\n // directly so no `/` encoding is applied.\n if (ecosystem === 'go') {\n const versionSuffix = version !== undefined ? `@${encodePurlSegment(version)}` : '';\n return `pkg:${type}/${name}${versionSuffix}`;\n }\n const slashIdx = name.indexOf('/');\n // npm scoped packages: '@scope/name' \u2192 namespace='@scope', name='name'.\n // Maven coordinates: 'groupId/artifactId' \u2192 namespace=groupId, name=artifactId.\n if (slashIdx > 0) {\n const namespace = name.slice(0, slashIdx);\n const pkgName = name.slice(slashIdx + 1);\n if (pkgName.length > 0) {\n return buildPurl({\n type,\n namespace,\n name: pkgName,\n ...(version !== undefined ? { version } : {}),\n });\n }\n }\n return buildPurl({\n type,\n name,\n ...(version !== undefined ? { version } : {}),\n });\n}\n\n/**\n * Parse a PURL string back into the ecosystem-shaped parts used by the\n * TechStack dependency model: ecosystem id, package name, and optional version.\n *\n * The npm scope and Maven groupId are folded back into `name` so the returned\n * shape matches `constructPurl`'s inputs (round-trippable).\n *\n * Returns `undefined` for malformed PURLs or PURL types that don't map to a\n * TechStack ecosystem.\n *\n * @example\n * parsePurl('pkg:npm/%40types/node@22.0.0') // \u2192 { ecosystem:'npm', name:'@types/node', version:'22.0.0' }\n * parsePurl('pkg:pypi/django@5.2') // \u2192 { ecosystem:'python', name:'django', version:'5.2' }\n * parsePurl('pkg:npm/react') // \u2192 { ecosystem:'npm', name:'react' }\n * parsePurl('not-a-purl') // \u2192 undefined\n */\nexport interface ParsedEcosystemPurl {\n readonly ecosystem: EcosystemId;\n readonly name: string;\n readonly version?: string | undefined;\n}\n\nexport function parsePurlEcosystem(purl: string): ParsedEcosystemPurl | undefined {\n const parts = parsePurl(purl);\n if (!parts) return undefined;\n const ecosystem = ecosystemForPurlType(parts.type);\n if (!ecosystem) return undefined;\n const namespace = parts.namespace;\n const name = namespace ? `${namespace}/${parts.name}` : parts.name;\n return {\n ecosystem,\n name,\n ...(parts.version !== undefined ? { version: parts.version } : {}),\n };\n}\n\n/**\n * Encode a PURL path segment per the spec.\n * Percent-encode characters that are not allowed unencoded.\n */\nfunction encodePurlSegment(segment: string): string {\n // The PURL spec says the value must be percent-encoded as per RFC 3986.\n // In practice, we need to encode: @ / % and other reserved chars.\n return segment\n .replace(/%/g, '%25')\n .replace(/@/g, '%40')\n .replace(/\\//g, '%2F');\n}\n\n/**\n * Decode a PURL path segment.\n */\nfunction decodePurlSegment(segment: string): string {\n try {\n return decodeURIComponent(segment);\n } catch {\n return segment;\n }\n}\n", "/**\n * TechStack \u2014 Workspace discovery wrapper.\n *\n * Wraps `detectLanguageWorkspaces()` from `@wrongstack/tools/languages` and\n * maps each `DetectedWorkspace` into a TechStack `Workspace` with:\n * - `EcosystemId` (TechStack's package-manager classification) instead of\n * the tools-package `LanguageProfileId` (which is language-shaped, not\n * package-manager-shaped \u2014 e.g. both `typescript` and `javascript` map to\n * `npm`).\n * - `relativeRoot` (project-relative, portable across machines).\n * - `lockfiles` extracted from the detected evidence (manifest/config/lockfile\n * kinds), independent of which evidence manifests were used.\n * - `coverage` classified against the SDD \u00A76 ecosystem support matrix\n * (Tier A \u2192 `full`, Tier B \u2192 `partial`, Tier C/unsupported \u2192 `unsupported`).\n *\n * Languages with no `EcosystemId` mapping (`deno`, `shell`) are dropped from\n * the result \u2014 they cannot be inventoried by the TechStack pipeline.\n *\n * @see docs/specs/techstack-sdd.md \u00A73.2, \u00A76\n */\n\nimport { detectLanguageWorkspaces } from '@wrongstack/tools/languages';\nimport type {\n DetectLanguageOptions,\n DetectedWorkspace,\n LanguageEvidence,\n LanguageProfileId,\n} from '@wrongstack/tools/languages';\nimport type { Coverage, EcosystemId, Workspace } from '../types.js';\n\n/**\n * Map a language profile id (from `@wrongstack/tools/languages`) to a TechStack\n * ecosystem id. Returns `undefined` for languages that don't correspond to a\n * package-manager ecosystem (e.g. `deno`, `shell`).\n *\n * `java` is disambiguated by gradle-vs-maven evidence: if any lockfile\n * detector matches `gradle.lockfile` we return `gradle`, otherwise `maven`.\n */\nconst STATIC_LANGUAGE_TO_ECOSYSTEM: Readonly<Record<LanguageProfileId, EcosystemId | undefined>> = {\n typescript: 'npm',\n javascript: 'npm',\n deno: undefined,\n python: 'python',\n go: 'go',\n rust: 'rust',\n csharp: 'dotnet',\n php: 'php',\n ruby: 'ruby',\n swift: 'swift',\n dart: 'dart',\n elixir: 'elixir',\n c: 'cpp',\n cpp: 'cpp',\n java: 'maven', // overridden by `resolveJavaEcosystem` when gradle evidence is present\n shell: undefined,\n};\n\n/**\n * Tier classification per SDD \u00A76.\n *\n * Tier A (full deterministic support: inventory + registry + advisory):\n * npm, python, rust, go, dotnet, php, dart\n * Tier B (partial: best-effort inventory + OSV, may not have rich registry):\n * maven, gradle, ruby, swift, elixir\n * Tier C (best-effort only \u2014 listed here for completeness; same as\n * `unsupported` in the Coverage union because the SDD defines coverage as\n * \"full / partial / unsupported\"):\n * cpp\n */\nconst ECOSYSTEM_TIER: Readonly<Record<EcosystemId, Coverage>> = {\n npm: 'full',\n python: 'full',\n rust: 'full',\n go: 'full',\n dotnet: 'full',\n php: 'full',\n dart: 'full',\n maven: 'partial',\n gradle: 'partial',\n ruby: 'partial',\n swift: 'partial',\n elixir: 'partial',\n cpp: 'unsupported',\n};\n\nfunction resolveJavaEcosystem(evidence: readonly LanguageEvidence[]): 'maven' | 'gradle' {\n for (const item of evidence) {\n if (item.kind === 'lockfile' && item.value === 'gradle.lockfile') return 'gradle';\n if (item.kind === 'manifest' && (item.value === 'build.gradle' || item.value === 'build.gradle.kts' || item.value === 'settings.gradle')) {\n return 'gradle';\n }\n }\n return 'maven';\n}\n\nfunction ecosystemForWorkspace(detected: DetectedWorkspace): EcosystemId | undefined {\n if (detected.language === 'java') {\n return resolveJavaEcosystem(detected.evidence);\n }\n return STATIC_LANGUAGE_TO_ECOSYSTEM[detected.language];\n}\n\nfunction extractLockfiles(evidence: readonly LanguageEvidence[]): string[] {\n const seen = new Set<string>();\n const out: string[] = [];\n for (const item of evidence) {\n if (item.kind !== 'lockfile') continue;\n if (seen.has(item.path)) continue;\n seen.add(item.path);\n out.push(item.path);\n }\n return out.sort();\n}\n\n/**\n * Map a single `DetectedWorkspace` to a TechStack `Workspace`.\n * Returns `undefined` when the language has no TechStack ecosystem mapping.\n */\nexport function mapDetectedWorkspace(\n detected: DetectedWorkspace,\n projectRoot: string,\n): Workspace | undefined {\n const ecosystem = ecosystemForWorkspace(detected);\n if (!ecosystem) return undefined;\n const relativeRoot =\n detected.root === projectRoot\n ? '.'\n : detected.root.startsWith(`${projectRoot}/`) || detected.root.startsWith(`${projectRoot}\\\\`)\n ? detected.root.slice(projectRoot.length + 1)\n : detected.root;\n const lockfiles = extractLockfiles(detected.evidence);\n return {\n id: detected.id,\n relativeRoot,\n ecosystem,\n ...(detected.packageManager ? { packageManager: detected.packageManager } : {}),\n manifests: [...detected.manifests].sort(),\n lockfiles,\n confidence: Math.max(0, Math.min(1, detected.confidence)),\n coverage: ECOSYSTEM_TIER[ecosystem],\n };\n}\n\n/**\n * Directory names whose manifests are test scaffolding, not real project\n * dependencies. A `package.json` under `tests/fixtures/` describes a fake\n * project used to exercise a scanner \u2014 inventorying it would surface\n * deliberately-outdated pins (e.g. a fixture pinning `zod@^3`) as findings\n * against the real project. Matched by basename anywhere in the tree;\n * merged with any caller-provided `ignoredDirectories`.\n */\nconst TEST_FIXTURE_DIRECTORIES: readonly string[] = [\n 'fixtures',\n '__fixtures__',\n 'test-fixtures',\n 'testdata',\n '__mocks__',\n];\n\n/**\n * Discover all TechStack workspaces under `projectRoot` by delegating to\n * `detectLanguageWorkspaces` and mapping each detected workspace.\n *\n * Workspaces whose language does not map to a TechStack ecosystem are\n * silently dropped (e.g. `deno`, `shell`) \u2014 they cannot be inventoried and\n * would only inflate coverage counts with `unsupported` entries that add no\n * value at the inventory-engine boundary.\n *\n * Test-fixture directories (`fixtures`, `testdata`, \u2026) are skipped so fake\n * fixture manifests never enter the inventory; the project root itself is\n * never skipped, so scanning a fixture directly (as tests do) still works.\n *\n * Results are sorted by `(ecosystem, relativeRoot, id)` for stable display.\n */\nexport async function discoverWorkspaces(\n projectRoot: string,\n options?: Omit<DetectLanguageOptions, 'projectRoot'>,\n): Promise<Workspace[]> {\n const result = await detectLanguageWorkspaces({\n ...(options ?? {}),\n projectRoot,\n ignoredDirectories: [...TEST_FIXTURE_DIRECTORIES, ...(options?.ignoredDirectories ?? [])],\n });\n const mapped: Workspace[] = [];\n for (const detected of result.workspaces) {\n const workspace = mapDetectedWorkspace(detected, result.projectRoot);\n if (workspace) mapped.push(workspace);\n }\n mapped.sort((a, b) => {\n return (\n a.ecosystem.localeCompare(b.ecosystem) ||\n a.relativeRoot.localeCompare(b.relativeRoot) ||\n a.id.localeCompare(b.id)\n );\n });\n return mapped;\n}\n\n/**\n * Coverage for a workspace's ecosystem \u2014 the per-workspace view of the\n * SDD \u00A76 tier matrix.\n */\nexport function coverageForEcosystem(ecosystem: EcosystemId): Coverage {\n return ECOSYSTEM_TIER[ecosystem];\n}", "/**\n * TechStack \u2014 npm ecosystem adapter.\n *\n * Parses package.json manifests and pnpm-lock.yaml (or package-lock.json /\n * yarn.lock) to produce DependencyObservation[] for Node.js workspaces.\n *\n * Supports: pnpm, npm, yarn, bun \u2014 determined by lockfile presence.\n *\n * @see docs/specs/techstack-sdd.md \u00A76 Tier A\n */\n\nimport { existsSync, readFileSync } from 'node:fs';\nimport { dirname, join, relative, resolve } from 'node:path';\nimport type {\n DependencyObservation,\n DependencyScope,\n DependencyStatus,\n Evidence,\n EcosystemId,\n Workspace,\n} from '../types.js';\nimport type {\n EcosystemAdapter,\n InventoryOptions,\n} from './interface.js';\nimport { resolveIn, workspaceRoot } from './paths.js';\nimport { buildPurl } from '../registry/purl.js';\n\n// \u2500\u2500 Lockfile types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ntype LockfileKind = 'pnpm' | 'npm' | 'yarn' | 'bun' | 'none';\n\ninterface LockfileInfo {\n readonly kind: LockfileKind;\n readonly path: string;\n}\n\n// \u2500\u2500 package.json shape \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ninterface PackageJsonDeps {\n readonly [packageName: string]: string;\n}\n\ninterface PackageJson {\n readonly name?: string | undefined;\n readonly dependencies?: PackageJsonDeps | undefined;\n readonly devDependencies?: PackageJsonDeps | undefined;\n readonly peerDependencies?: PackageJsonDeps | undefined;\n readonly optionalDependencies?: PackageJsonDeps | undefined;\n}\n\n// \u2500\u2500 Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Locate the lockfile that governs a workspace.\n *\n * Walks up from the workspace to `stopAt` (the project root). In a pnpm or npm\n * workspace only the repo root holds a lockfile \u2014 `packages/cli` has none \u2014 so\n * looking only in the workspace directory finds nothing for every package but\n * the root, and every dependency ends up with no resolved version.\n */\nfunction detectLockfile(workspaceDir: string, stopAt?: string): LockfileInfo {\n const candidates: Array<{ file: string; kind: LockfileKind }> = [\n { file: 'pnpm-lock.yaml', kind: 'pnpm' },\n { file: 'package-lock.json', kind: 'npm' },\n { file: 'yarn.lock', kind: 'yarn' },\n { file: 'bun.lockb', kind: 'bun' },\n ];\n\n const ceiling = stopAt ? resolve(stopAt) : undefined;\n let dir = resolve(workspaceDir);\n\n for (;;) {\n for (const c of candidates) {\n const candidate = join(dir, c.file);\n if (existsSync(candidate)) return { kind: c.kind, path: candidate };\n }\n if (ceiling && dir === ceiling) break;\n const parent = dirname(dir);\n if (parent === dir) break; // filesystem root\n // Without a ceiling, don't wander above the workspace at all.\n if (!ceiling) break;\n dir = parent;\n }\n return { kind: 'none', path: '' };\n}\n\n/** Strip pnpm's peer-dependency suffix: `19.1.0(react@19.1.0)` \u2192 `19.1.0`. */\nfunction stripPeerSuffix(version: string): string {\n const paren = version.indexOf('(');\n return (paren === -1 ? version : version.slice(0, paren)).trim();\n}\n\n/**\n * Extract the resolved versions pnpm recorded for one importer (workspace).\n *\n * pnpm's `importers:` section maps each workspace to the exact version it\n * resolved for every declared dependency \u2014 which is precisely the per-workspace\n * question this adapter asks, and it stays correct when two workspaces pin\n * different versions of the same package.\n *\n * Line-based on purpose: the lockfile is machine-generated with a stable\n * 2-space indent, and pulling in a YAML parser for four fields isn't worth the\n * dependency.\n *\n * ```yaml\n * importers:\n * packages/cli: # 2 spaces \u2014 importer\n * dependencies: # 4 spaces \u2014 section\n * react: # 6 spaces \u2014 package\n * specifier: ^19.0.0 # 8 spaces \u2014 fields\n * version: 19.1.0\n * ```\n */\nfunction parsePnpmImporterVersions(lockContent: string, importerPath: string): Map<string, string> {\n const versions = new Map<string, string>();\n const lines = lockContent.split(/\\r?\\n/);\n\n let inImporters = false;\n let inTargetImporter = false;\n let currentPackage: string | undefined;\n\n for (const raw of lines) {\n if (raw.trim() === '' || raw.trimStart().startsWith('#')) continue;\n\n // Top-level key ends the importers block.\n if (!/^\\s/.test(raw)) {\n if (inImporters) break;\n inImporters = raw.startsWith('importers:');\n continue;\n }\n if (!inImporters) continue;\n\n const indent = raw.length - raw.trimStart().length;\n const line = raw.trim();\n\n if (indent === 2) {\n // New importer \u2014 `packages/cli:` or `.:`\n const key = line.endsWith(':') ? unquote(line.slice(0, -1)) : undefined;\n inTargetImporter = key === importerPath;\n currentPackage = undefined;\n continue;\n }\n if (!inTargetImporter) continue;\n\n if (indent === 4) {\n currentPackage = undefined; // dependencies: / devDependencies: / \u2026\n continue;\n }\n if (indent === 6 && line.endsWith(':')) {\n currentPackage = unquote(line.slice(0, -1));\n continue;\n }\n if (indent >= 8 && currentPackage && line.startsWith('version:')) {\n const version = stripPeerSuffix(unquote(line.slice('version:'.length).trim()));\n // `link:../core` is a workspace link, not a released version \u2014 recording\n // it as `locked` would make a local package look like a registry one.\n if (version && !version.startsWith('link:') && !version.startsWith('file:')) {\n versions.set(currentPackage, version);\n }\n currentPackage = undefined;\n }\n }\n\n return versions;\n}\n\nfunction unquote(value: string): string {\n const trimmed = value.trim();\n if (\n (trimmed.startsWith(\"'\") && trimmed.endsWith(\"'\")) ||\n (trimmed.startsWith('\"') && trimmed.endsWith('\"'))\n ) {\n return trimmed.slice(1, -1);\n }\n return trimmed;\n}\n\n/**\n * Parse package-lock.json (npm) to extract resolved versions.\n */\nfunction parseNpmLockVersions(lockContent: string): Map<string, string> {\n const versions = new Map<string, string>();\n try {\n const lock = JSON.parse(lockContent);\n // npm v3: lock.dependencies\n const deps = lock.dependencies ?? {};\n for (const [name, info] of Object.entries(deps)) {\n const depInfo = info as { version?: string };\n if (depInfo.version) {\n // Strip version prefixes like ^, ~, >=\n const cleanVersion = depInfo.version.replace(/^[^0-9]+/, '');\n versions.set(name, cleanVersion);\n }\n }\n // npm v2 (lockfileVersion 2+): lock.packages\n const packages = lock.packages ?? {};\n for (const key of Object.keys(packages)) {\n const pkgInfo = packages[key] as { version?: string };\n if (pkgInfo.version) {\n // Key format: \"node_modules/package-name\" or \"node_modules/@scope/package-name\"\n const name = key.replace(/^node_modules\\//, '');\n if (!versions.has(name)) {\n versions.set(name, pkgInfo.version);\n }\n }\n }\n } catch {\n // Malformed lockfile \u2014 return empty map\n }\n return versions;\n}\n\n/**\n * Create a manifest evidence entry.\n */\nfunction manifestEvidence(path: string): Evidence {\n return {\n kind: 'manifest',\n source: path,\n retrievedAt: new Date().toISOString(),\n };\n}\n\n/**\n * Create a lockfile evidence entry.\n */\nfunction lockfileEvidence(path: string): Evidence {\n return {\n kind: 'lockfile',\n source: path,\n retrievedAt: new Date().toISOString(),\n };\n}\n\n/**\n * Determine the dependency scope from the manifest section it appears in.\n */\nfunction scopeForSection(section: string): DependencyScope {\n switch (section) {\n case 'dependencies':\n return 'runtime';\n case 'devDependencies':\n return 'development';\n case 'peerDependencies':\n return 'peer';\n case 'optionalDependencies':\n return 'optional';\n default:\n return 'runtime';\n }\n}\n\n/**\n * Determine status: local_path for file: / link: / workspace:,\n * git_dependency for git+ / github: / git:, registry otherwise.\n */\nfunction statusForSpec(spec: string): DependencyStatus {\n if (spec.startsWith('file:') || spec.startsWith('link:') || spec.startsWith('workspace:')) {\n return 'local_path';\n }\n if (spec.startsWith('git+') || spec.startsWith('github:') || spec.startsWith('git:')) {\n return 'git_dependency';\n }\n return 'current';\n}\n\n/**\n * Check if a spec is a local/git reference (not resolvable to a registry version).\n */\nfunction isRegistrySpec(spec: string): boolean {\n return (\n !spec.startsWith('file:') &&\n !spec.startsWith('link:') &&\n !spec.startsWith('workspace:') &&\n !spec.startsWith('git+') &&\n !spec.startsWith('github:') &&\n !spec.startsWith('git:')\n );\n}\n\n// \u2500\u2500 Adapter \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport class NpmAdapter implements EcosystemAdapter {\n readonly ecosystem: EcosystemId = 'npm';\n\n async inventory(\n workspace: Workspace,\n options: InventoryOptions,\n ): Promise<readonly DependencyObservation[]> {\n const observations: DependencyObservation[] = [];\n const root = workspaceRoot(workspace, options);\n // Pick the actual package.json rather than `manifests[0]` \u2014 discovery also\n // reports tsconfig.json as a manifest, and sort order is not a contract.\n const manifestPath = resolveIn(\n root,\n workspace.manifests.find((m) => m.endsWith('package.json')) ?? 'package.json',\n );\n\n // Read package.json\n let pkg: PackageJson;\n let manifestContent: string;\n try {\n manifestContent = readFileSync(manifestPath, 'utf-8');\n pkg = JSON.parse(manifestContent) as PackageJson;\n } catch {\n return []; // Can't read manifest \u2014 no dependencies\n }\n\n const manifestEv = manifestEvidence(manifestPath);\n\n // Read lockfile for resolved versions\n const lockInfo = detectLockfile(root, options.projectRoot);\n const resolvedVersions = new Map<string, string>();\n let lockEv: Evidence | undefined;\n if (lockInfo.kind === 'pnpm') {\n try {\n const lockContent = readFileSync(lockInfo.path, 'utf-8');\n // The importer key is this workspace's path relative to the lockfile,\n // POSIX-style; the root workspace is `.`.\n const importerPath =\n relative(dirname(lockInfo.path), root).split(/[/\\\\]/).filter(Boolean).join('/') || '.';\n const parsed = parsePnpmImporterVersions(lockContent, importerPath);\n for (const [k, v] of parsed) resolvedVersions.set(k, v);\n if (parsed.size > 0) lockEv = lockfileEvidence(lockInfo.path);\n } catch {\n // ignore\n }\n } else if (lockInfo.kind === 'npm') {\n try {\n const lockContent = readFileSync(lockInfo.path, 'utf-8');\n const parsed = parseNpmLockVersions(lockContent);\n for (const [k, v] of parsed) resolvedVersions.set(k, v);\n lockEv = lockfileEvidence(lockInfo.path);\n } catch {\n // ignore\n }\n }\n\n // Process each dependency section\n const sections: Array<{ name: string; deps: PackageJsonDeps | undefined }> = [\n { name: 'dependencies', deps: pkg.dependencies },\n { name: 'devDependencies', deps: pkg.devDependencies },\n { name: 'peerDependencies', deps: pkg.peerDependencies },\n { name: 'optionalDependencies', deps: pkg.optionalDependencies },\n ];\n\n const seen = new Set<string>(); // dedup within workspace\n\n for (const section of sections) {\n if (!section.deps) continue;\n const scope = scopeForSection(section.name);\n\n for (const [name, requested] of Object.entries(section.deps)) {\n const dedupKey = `${name}`;\n if (seen.has(dedupKey)) continue;\n seen.add(dedupKey);\n\n const isRegistry = isRegistrySpec(requested);\n const status = statusForSpec(requested);\n\n // Resolve locked version from lockfile\n const locked = resolvedVersions.get(name);\n\n // Build PURL for registry deps\n const purl = isRegistry && locked\n ? buildPurl({ type: 'npm', name, version: locked })\n : isRegistry\n ? buildPurl({ type: 'npm', name })\n : undefined;\n\n const evidence: Evidence[] = [manifestEv];\n if (lockEv && locked) evidence.push(lockEv);\n\n observations.push({\n id: `dep-${workspace.id}-${name}`,\n workspaceId: workspace.id,\n ...(purl ? { purl } : {}),\n ecosystem: 'npm' as const,\n name,\n sourceType: isRegistry ? 'registry' : status === 'local_path' ? 'path' : 'git',\n direct: true,\n scope,\n requested,\n ...(locked ? { locked } : {}),\n status,\n evidence,\n });\n }\n }\n\n // Parse transitive dependencies from lockfile if requested\n // (Phase 1 enhancement: includeTransitive option)\n\n return observations;\n }\n}\n\n/**\n * Default singleton instance.\n */\nexport const npmAdapter = new NpmAdapter();\n", "/**\n * TechStack \u2014 adapter path resolution.\n *\n * `Workspace.relativeRoot` is deliberately project-relative: snapshots are\n * persisted and shipped to the browser, so they must stay portable across\n * machines. Adapters, however, have to actually open files, and a relative\n * root resolves against `process.cwd()` \u2014 which is only the project root by\n * coincidence. The server can be started from anywhere, and switching projects\n * mid-session doesn't move `cwd` at all.\n *\n * So the absolute base travels alongside the workspace, via\n * `InventoryOptions.projectRoot`, instead of being baked into the persisted\n * type.\n *\n * @see docs/specs/techstack-sdd.md \u00A73.2\n */\n\nimport { resolve } from 'node:path';\nimport type { Workspace } from '../types.js';\nimport type { InventoryOptions } from './interface.js';\n\n/**\n * Absolute filesystem root of a workspace.\n *\n * Falls back to resolving against `cwd` when no `projectRoot` is supplied, to\n * keep older direct callers working.\n */\nexport function workspaceRoot(workspace: Workspace, options: InventoryOptions): string {\n const relative = workspace.relativeRoot || '.';\n return options.projectRoot ? resolve(options.projectRoot, relative) : resolve(relative);\n}\n\n/**\n * Resolve a manifest/lockfile path that may arrive either absolute (as\n * `Workspace.manifests` entries do, straight from discovery) or as a bare\n * filename (as adapter fallbacks use).\n *\n * Use this instead of `join`: `join('/a/b', '/a/b/package.json')` yields\n * `/a/b/a/b/package.json`, which is exactly the bug that silently emptied the\n * npm, Go, and Rust inventories \u2014 every workspace threw on read and the\n * engine's `catch { deps = [] }` swallowed it.\n */\nexport function resolveIn(root: string, candidate: string): string {\n return resolve(root, candidate);\n}\n", "/**\r\n * TechStack \u2014 Python ecosystem adapter.\r\n *\r\n * Parses pyproject.toml (PEP 621), requirements.txt, and Pipfile to produce\r\n * DependencyObservation[] for Python workspaces.\r\n *\r\n * Supports: pip, pipenv, poetry, uv \u2014 determined by manifest/lockfile presence.\r\n */\r\n\r\nimport { readFileSync } from 'node:fs';\r\nimport { join } from 'node:path';\r\nimport type {\r\n DependencyObservation,\r\n DependencyScope,\r\n Evidence,\r\n EcosystemId,\r\n Workspace,\r\n} from '../types.js';\r\nimport type {\r\n EcosystemAdapter,\r\n InventoryOptions,\r\n} from './interface.js';\r\nimport { workspaceRoot } from './paths.js';\r\nimport { buildPurl } from '../registry/purl.js';\r\n\r\n// \u2500\u2500 Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nfunction manifestEvidence(path: string): Evidence {\r\n return { kind: 'manifest', source: path, retrievedAt: new Date().toISOString() };\r\n}\r\n\r\nfunction lockfileEvidence(path: string): Evidence {\r\n return { kind: 'lockfile', source: path, retrievedAt: new Date().toISOString() };\r\n}\r\n\r\n// \u2500\u2500 Minimal TOML parser (line-based, sufficient for pyproject.toml) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\ninterface TomlSection {\r\n readonly name: string;\r\n readonly lines: string[];\r\n}\r\n\r\nfunction parseTomlSections(content: string): TomlSection[] {\r\n const sections: TomlSection[] = [];\r\n let currentSection = '__header__';\r\n let currentLines: string[] = [];\r\n for (const raw of content.split('\\n')) {\r\n const line = raw.trim();\r\n if (line.startsWith('#') || line === '') continue;\r\n const sectionMatch = line.match(/^\\[([^\\]]+)\\]$/);\r\n if (sectionMatch) {\r\n if (currentLines.length > 0) sections.push({ name: currentSection, lines: currentLines });\r\n currentSection = sectionMatch[1]!;\r\n currentLines = [];\r\n } else {\r\n currentLines.push(raw);\r\n }\r\n }\r\n if (currentLines.length > 0) sections.push({ name: currentSection, lines: currentLines });\r\n return sections;\r\n}\r\n\r\nfunction extractTomlArray(sectionLines: string[], key: string): string[] {\r\n const result: string[] = [];\r\n let inArray = false;\r\n for (const line of sectionLines) {\r\n const trimmed = line.trim();\r\n if (!inArray) {\r\n const match = trimmed.match(new RegExp(`^${key}\\\\s*=\\\\s*\\\\[`));\r\n if (match) {\r\n inArray = true;\r\n const rest = trimmed.slice(match[0].length);\r\n if (rest.includes(']')) {\r\n const items = rest.replace(/\\]\\s*,?\\s*$/, '').trim();\r\n for (const item of items.split(',')) {\r\n const cleaned = item.trim().replace(/^\"|\"$/g, '').trim();\r\n if (cleaned) result.push(cleaned);\r\n }\r\n inArray = false;\r\n }\r\n }\r\n } else {\r\n const closeIdx = trimmed.indexOf(']');\r\n if (closeIdx >= 0) {\r\n const items = trimmed.slice(0, closeIdx).trim();\r\n for (const item of items.split(',')) {\r\n const cleaned = item.trim().replace(/^\"|\"$/g, '').trim();\r\n if (cleaned) result.push(cleaned);\r\n }\r\n inArray = false;\r\n } else {\r\n const cleaned = trimmed.replace(/,$/, '').trim().replace(/^\"|\"$/g, '').trim();\r\n if (cleaned) result.push(cleaned);\r\n }\r\n }\r\n }\r\n return result;\r\n}\r\n\r\nfunction parsePep508(spec: string): { name: string; constraint: string | undefined } {\r\n let s = spec.trim();\r\n s = s.replace(/\\[.*?\\]/g, '');\r\n const match = s.match(/^([a-zA-Z0-9][a-zA-Z0-9._-]*)\\s*(.*)$/);\r\n if (!match) return { name: s, constraint: undefined };\r\n return { name: match[1]!, constraint: match[2]?.trim() || undefined };\r\n}\r\n\r\n// \u2500\u2500 pyproject.toml parser (PEP 621) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nfunction parsePyprojectDeps(content: string): Array<{ name: string; constraint: string | undefined; scope: DependencyScope }> {\r\n const deps: Array<{ name: string; constraint: string | undefined; scope: DependencyScope }> = [];\r\n const sections = parseTomlSections(content);\r\n\r\n const projectSection = sections.find((s) => s.name === 'project');\r\n if (projectSection) {\r\n const depSpecs = extractTomlArray(projectSection.lines, 'dependencies');\r\n for (const spec of depSpecs) {\r\n const { name, constraint } = parsePep508(spec);\r\n if (name) deps.push({ name, constraint, scope: 'runtime' });\r\n }\r\n }\r\n\r\n for (const section of sections) {\r\n if (section.name === 'project.optional-dependencies') {\r\n // Each line is: group_name = [\"dep1\", \"dep2\", ...]\r\n for (const line of section.lines) {\r\n const trimmed = line.trim();\r\n const groupMatch = trimmed.match(/^([a-zA-Z0-9_-]+)\\s*=\\s*\\[/);\r\n if (groupMatch) {\r\n const depSpecs = extractTomlArray(section.lines, groupMatch[1]!);\r\n for (const spec of depSpecs) {\r\n const { name, constraint } = parsePep508(spec);\r\n if (name) deps.push({ name, constraint, scope: 'optional' });\r\n }\r\n }\r\n }\r\n }\r\n }\r\n\r\n return deps;\r\n}\r\n\r\n// \u2500\u2500 requirements.txt parser \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nfunction parseRequirementsTxt(content: string): Array<{ name: string; constraint: string | undefined }> {\r\n const deps: Array<{ name: string; constraint: string | undefined }> = [];\r\n for (const raw of content.split('\\n')) {\r\n const line = raw.trim();\r\n if (!line || line.startsWith('#') || line.startsWith('-')) continue;\r\n const { name, constraint } = parsePep508(line);\r\n if (name) deps.push({ name, constraint });\r\n }\r\n return deps;\r\n}\r\n\r\n// \u2500\u2500 Pipfile parser \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nfunction parsePipfileDeps(content: string): Array<{ name: string; constraint: string | undefined; scope: DependencyScope }> {\r\n const deps: Array<{ name: string; constraint: string | undefined; scope: DependencyScope }> = [];\r\n const sections = parseTomlSections(content);\r\n for (const section of sections) {\r\n const scope: DependencyScope = section.name === 'dev-packages' ? 'development' : 'runtime';\r\n for (const line of section.lines) {\r\n const trimmed = line.trim();\r\n if (trimmed.startsWith('#')) continue;\r\n const match = trimmed.match(/^([a-zA-Z0-9][a-zA-Z0-9._-]*)\\s*=\\s*\"([^\"]*)\"$/);\r\n if (match) {\r\n const constraint = match[2]! === '*' ? undefined : match[2]!;\r\n deps.push({ name: match[1]!, constraint, scope });\r\n }\r\n }\r\n }\r\n return deps;\r\n}\r\n\r\nfunction parseRequirementsLockVersions(content: string): Map<string, string> {\r\n const versions = new Map<string, string>();\r\n for (const raw of content.split('\\n')) {\r\n const line = raw.trim();\r\n if (!line || line.startsWith('#') || line.startsWith('-')) continue;\r\n const match = line.match(/^([a-zA-Z0-9][a-zA-Z0-9._-]*)\\s*==\\s*([^\\s;]+)/);\r\n if (match) versions.set(match[1]!, match[2]!);\r\n }\r\n return versions;\r\n}\r\n\r\n/**\r\n * Parse poetry.lock to extract resolved versions.\r\n * Format:\r\n * [[package]]\r\n * name = \"flask\"\r\n * version = \"3.0.3\"\r\n */\r\nexport function parsePoetryLock(content: string): Map<string, string> {\r\n const versions = new Map<string, string>();\r\n let currentName: string | undefined;\r\n for (const raw of content.split('\\n')) {\r\n const line = raw.trim();\r\n const nameMatch = line.match(/^name\\s*=\\s*\"([^\"]+)\"/);\r\n if (nameMatch) {\r\n currentName = nameMatch[1]!;\r\n continue;\r\n }\r\n const versionMatch = line.match(/^version\\s*=\\s*\"([^\"]+)\"/);\r\n if (versionMatch && currentName) {\r\n versions.set(normalizePkgName(currentName), versionMatch[1]!);\r\n currentName = undefined;\r\n }\r\n }\r\n return versions;\r\n}\r\n\r\n/**\r\n * Normalize Python package names per PEP 503:\r\n * underscores and hyphens are equivalent, all lowercase.\r\n */\r\nfunction normalizePkgName(name: string): string {\r\n return name.toLowerCase().replace(/_/g, '-');\r\n}\r\n\r\n// \u2500\u2500 Adapter \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nexport class PythonAdapter implements EcosystemAdapter {\r\n readonly ecosystem: EcosystemId = 'python';\r\n\r\n async inventory(workspace: Workspace, options: InventoryOptions): Promise<readonly DependencyObservation[]> {\r\n const observations: DependencyObservation[] = [];\r\n const root = workspaceRoot(workspace, options);\r\n const seen = new Set<string>();\r\n\r\n const hasPyproject = workspace.manifests.some((m) => m.includes('pyproject.toml')) || this.fileExists(join(root, 'pyproject.toml'));\r\n const hasRequirements = workspace.manifests.some((m) => m.includes('requirements.txt')) || this.fileExists(join(root, 'requirements.txt'));\r\n const hasPipfile = workspace.manifests.some((m) => m.includes('Pipfile')) || this.fileExists(join(root, 'Pipfile'));\r\n\r\n const lockfilePath = this.detectLockfile(root);\r\n\r\n let allDeps: Array<{ name: string; constraint: string | undefined; scope: DependencyScope; source: string }> = [];\r\n let pyprojectEv: Evidence | undefined;\r\n\r\n if (hasPyproject) {\r\n try {\r\n const content = readFileSync(join(root, 'pyproject.toml'), 'utf-8');\r\n pyprojectEv = manifestEvidence(join(root, 'pyproject.toml'));\r\n const parsed = parsePyprojectDeps(content);\r\n for (const d of parsed) allDeps.push({ ...d, source: 'pyproject.toml' });\r\n } catch { /* ignore */ }\r\n }\r\n\r\n let reqLockVersions = new Map<string, string>();\r\n let requirementsEv: Evidence | undefined;\r\n\r\n if (hasRequirements) {\r\n try {\r\n const content = readFileSync(join(root, 'requirements.txt'), 'utf-8');\r\n requirementsEv = manifestEvidence(join(root, 'requirements.txt'));\r\n const parsed = parseRequirementsTxt(content);\r\n for (const d of parsed) {\r\n if (!allDeps.some((existing) => existing.name === d.name)) {\r\n allDeps.push({ ...d, scope: 'runtime', source: 'requirements.txt' });\r\n }\r\n }\r\n reqLockVersions = parseRequirementsLockVersions(content);\r\n } catch { /* ignore */ }\r\n }\r\n\r\n if (hasPipfile) {\r\n try {\r\n const content = readFileSync(join(root, 'Pipfile'), 'utf-8');\r\n if (!pyprojectEv) pyprojectEv = manifestEvidence(join(root, 'Pipfile'));\r\n const parsed = parsePipfileDeps(content);\r\n for (const d of parsed) {\r\n if (!allDeps.some((existing) => existing.name === d.name)) {\r\n allDeps.push({ ...d, source: 'Pipfile' });\r\n }\r\n }\r\n } catch { /* ignore */ }\r\n }\r\n\r\n let lockEv: Evidence | undefined;\r\n if (lockfilePath) {\r\n try {\r\n readFileSync(lockfilePath, 'utf-8');\r\n lockEv = lockfileEvidence(lockfilePath);\r\n } catch { /* ignore */ }\r\n }\r\n\r\n const manifestEv = pyprojectEv || requirementsEv;\r\n\r\n for (const dep of allDeps) {\r\n if (seen.has(dep.name)) continue;\r\n seen.add(dep.name);\r\n\r\n const locked = reqLockVersions.get(dep.name) || undefined;\r\n const isRegistry = !dep.constraint || (!dep.constraint.startsWith('file:') && !dep.constraint.startsWith('git+') && !dep.constraint.startsWith('-e'));\r\n\r\n const purl = isRegistry && locked ? buildPurl({ type: 'python', name: dep.name, version: locked })\r\n : isRegistry ? buildPurl({ type: 'python', name: dep.name }) : undefined;\r\n\r\n const evidence: Evidence[] = [];\r\n if (manifestEv) evidence.push(manifestEv);\r\n if (lockEv && locked) evidence.push(lockEv);\r\n if (evidence.length === 0) {\r\n evidence.push({ kind: 'manifest', source: dep.source, retrievedAt: new Date().toISOString() });\r\n }\r\n\r\n const status: DependencyObservation['status'] =\r\n dep.constraint && (dep.constraint.startsWith('file:') || dep.constraint.startsWith('-e')) ? 'local_path'\r\n : dep.constraint?.startsWith('git+') ? 'git_dependency' : 'current';\r\n\r\n observations.push({\r\n id: `dep-${workspace.id}-${dep.name}`,\r\n workspaceId: workspace.id,\r\n ...(purl ? { purl } : {}),\r\n ecosystem: 'python',\r\n name: dep.name,\r\n sourceType: isRegistry ? 'registry' : status === 'local_path' ? 'path' : 'git',\r\n direct: true,\r\n scope: dep.scope,\r\n ...(dep.constraint ? { requested: dep.constraint } : {}),\r\n ...(locked ? { locked } : {}),\r\n status,\r\n evidence,\r\n });\r\n }\r\n\r\n return observations;\r\n }\r\n\r\n private fileExists(filePath: string): boolean {\r\n try { readFileSync(filePath, 'utf-8'); return true; } catch { return false; }\r\n }\r\n\r\n private detectLockfile(workspaceRoot: string): string | undefined {\r\n for (const file of ['Pipfile.lock', 'poetry.lock', 'uv.lock']) {\r\n try { readFileSync(join(workspaceRoot, file), 'utf-8'); return join(workspaceRoot, file); } catch { /* not found */ }\r\n }\r\n return undefined;\r\n }\r\n}\r\n\r\nexport const pythonAdapter = new PythonAdapter();\r\n", "/**\r\n * TechStack \u2014 Rust ecosystem adapter.\r\n *\r\n * Parses Cargo.toml manifests and Cargo.lock lockfiles to produce\r\n * DependencyObservation[] for Rust workspaces.\r\n *\r\n * @see docs/specs/techstack-sdd.md \u00A76 Tier A\r\n */\r\n\r\nimport { readFileSync } from 'node:fs';\r\nimport type {\r\n DependencyObservation,\r\n DependencyScope,\r\n Evidence,\r\n EcosystemId,\r\n Workspace,\r\n} from '../types.js';\r\nimport type {\r\n EcosystemAdapter,\r\n InventoryOptions,\r\n} from './interface.js';\r\nimport { resolveIn, workspaceRoot } from './paths.js';\r\nimport { buildPurl } from '../registry/purl.js';\r\n\r\n// \u2500\u2500 Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nfunction manifestEvidence(path: string): Evidence {\r\n return { kind: 'manifest', source: path, retrievedAt: new Date().toISOString() };\r\n}\r\n\r\nfunction lockfileEvidence(path: string): Evidence {\r\n return { kind: 'lockfile', source: path, retrievedAt: new Date().toISOString() };\r\n}\r\n\r\n// \u2500\u2500 Minimal TOML parser (line-based, sufficient for Cargo.toml + Cargo.lock) \u2500\u2500\r\n\r\ninterface TomlSection {\r\n readonly name: string;\r\n readonly lines: string[];\r\n}\r\n\r\nfunction parseTomlSections(content: string): TomlSection[] {\r\n const sections: TomlSection[] = [];\r\n let currentSection = '__header__';\r\n let currentLines: string[] = [];\r\n for (const raw of content.split('\\n')) {\r\n const line = raw.trim();\r\n if (line.startsWith('#') || line === '') continue;\r\n const sectionMatch = line.match(/^\\[([^\\]]+)\\]$/);\r\n if (sectionMatch) {\r\n if (currentLines.length > 0) sections.push({ name: currentSection, lines: currentLines });\r\n currentSection = sectionMatch[1]!;\r\n currentLines = [];\r\n } else {\r\n currentLines.push(raw);\r\n }\r\n }\r\n if (currentLines.length > 0) sections.push({ name: currentSection, lines: currentLines });\r\n return sections;\r\n}\r\n\r\n/**\r\n * Parse a key = value pair from a TOML line.\r\n * Handles: key = \"string\", key = { inline = \"table\" }\r\n * Returns undefined for lines that are continuations of inline tables.\r\n */\r\nfunction parseTomlKeyValue(line: string): { key: string; value: string } | undefined {\r\n const trimmed = line.trim();\r\n // Skip inline table continuation lines that start with a key\r\n if (trimmed.startsWith('#')) return undefined;\r\n const match = trimmed.match(/^([a-zA-Z0-9_-]+)\\s*=\\s*(.+)$/);\r\n if (!match) return undefined;\r\n return { key: match[1]!, value: match[2]!.trim() };\r\n}\r\n\r\n/**\r\n * Extract simple string key-value pairs from a TOML section.\r\n * Returns { name, version } for entries like `serde = \"1.0\"` or `serde = { version = \"1.0\", ... }`.\r\n * Handles both simple and inline-table formats.\r\n */\r\nfunction extractTomlDeps(sectionLines: string[]): Array<{ name: string; version: string | undefined }> {\r\n const deps: Array<{ name: string; version: string | undefined }> = [];\r\n for (const raw of sectionLines) {\r\n const line = raw.trim();\r\n if (line.startsWith('#') || line === '') continue;\r\n\r\n // Check if it's an inline table: serde = { version = \"1.0\", features = [...] }\r\n const tableMatch = line.match(/^([a-zA-Z0-9_-]+)\\s*=\\s*\\{\\s*(.*?)\\s*\\}$/);\r\n if (tableMatch) {\r\n const name = tableMatch[1]!;\r\n const inner = tableMatch[2]!;\r\n const versionMatch = inner.match(/version\\s*=\\s*\"([^\"]+)\"/);\r\n deps.push({ name, version: versionMatch ? versionMatch[1]! : undefined });\r\n continue;\r\n }\r\n\r\n // Simple key = \"value\"\r\n const simpleMatch = line.match(/^([a-zA-Z0-9_-]+)\\s*=\\s*\"([^\"]*)\"$/);\r\n if (simpleMatch) {\r\n deps.push({ name: simpleMatch[1]!, version: simpleMatch[2]! || undefined });\r\n continue;\r\n }\r\n\r\n // Try partial inline table (may span lines)\r\n const partialMatch = parseTomlKeyValue(line);\r\n if (partialMatch && !partialMatch.value.startsWith('{') && !partialMatch.value.startsWith('\"')) {\r\n // Might be a path or git dep: serde = { path = \"../foo\" }\r\n // Ignore these for now\r\n }\r\n }\r\n return deps;\r\n}\r\n\r\n/**\r\n * Parse Cargo.lock format for package entries.\r\n * Cargo.lock uses TOML format with [[package]] array entries.\r\n */\r\nfunction parseCargoLock(content: string): Map<string, string> {\r\n const versions = new Map<string, string>();\r\n const lines = content.split('\\n');\r\n let currentName: string | undefined;\r\n let currentVersion: string | undefined;\r\n let inPackage = false;\r\n\r\n for (const raw of lines) {\r\n const line = raw.trim();\r\n if (line.startsWith('#') || line === '') continue;\r\n\r\n if (line.startsWith('[[') && line.includes('package')) {\r\n // Save previous\r\n if (inPackage && currentName && currentVersion) {\r\n versions.set(currentName, currentVersion);\r\n }\r\n currentName = undefined;\r\n currentVersion = undefined;\r\n inPackage = true;\r\n continue;\r\n }\r\n\r\n if (inPackage) {\r\n if (line.startsWith('name')) {\r\n const m = line.match(/^name\\s*=\\s*\"([^\"]+)\"/);\r\n if (m) currentName = m[1]!;\r\n } else if (line.startsWith('version')) {\r\n const m = line.match(/^version\\s*=\\s*\"([^\"]+)\"/);\r\n if (m) currentVersion = m[1]!;\r\n }\r\n }\r\n }\r\n\r\n // Save last\r\n if (inPackage && currentName && currentVersion) {\r\n versions.set(currentName, currentVersion);\r\n }\r\n\r\n return versions;\r\n}\r\n\r\n// \u2500\u2500 Scope mapping \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nfunction scopeForCargoSection(section: string): DependencyScope {\r\n switch (section) {\r\n case 'dependencies':\r\n return 'runtime';\r\n case 'dev-dependencies':\r\n return 'development';\r\n case 'build-dependencies':\r\n return 'build';\r\n default:\r\n return 'runtime';\r\n }\r\n}\r\n\r\n// \u2500\u2500 Adapter \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nexport class RustAdapter implements EcosystemAdapter {\r\n readonly ecosystem: EcosystemId = 'rust';\r\n\r\n async inventory(\r\n workspace: Workspace,\r\n options: InventoryOptions,\r\n ): Promise<readonly DependencyObservation[]> {\r\n const observations: DependencyObservation[] = [];\r\n const root = workspaceRoot(workspace, options);\r\n const seen = new Set<string>();\r\n\r\n // Find manifests\r\n const cargoTomlPath = workspace.manifests.find((m) => m.includes('Cargo.toml'))\r\n || (this.fileExists(resolveIn(root, 'Cargo.toml')) ? 'Cargo.toml' : undefined);\r\n\r\n if (!cargoTomlPath) return [];\r\n\r\n const fullManifestPath = resolveIn(root, cargoTomlPath);\r\n let cargoContent: string;\r\n try {\r\n cargoContent = readFileSync(fullManifestPath, 'utf-8');\r\n } catch {\r\n return [];\r\n }\r\n\r\n const manifestEv = manifestEvidence(fullManifestPath);\r\n\r\n // Find lockfile\r\n const cargoLockPath = resolveIn(root, 'Cargo.lock');\r\n let lockVersions = new Map<string, string>();\r\n let lockEv: Evidence | undefined;\r\n try {\r\n const lockContent = readFileSync(cargoLockPath, 'utf-8');\r\n lockVersions = parseCargoLock(lockContent);\r\n lockEv = lockfileEvidence(cargoLockPath);\r\n } catch {\r\n // No lockfile \u2014 that's OK\r\n }\r\n\r\n // Parse dependency sections from Cargo.toml\r\n const sections = parseTomlSections(cargoContent);\r\n const depSections = ['dependencies', 'dev-dependencies', 'build-dependencies'];\r\n\r\n for (const section of sections) {\r\n // Cargo.toml has [dependencies], [dev-dependencies], [build-dependencies]\r\n // Also [target.'cfg(...)'.dependencies] patterns\r\n const sectionName = section.name;\r\n let matchedScope: string | undefined;\r\n\r\n for (const depSec of depSections) {\r\n if (sectionName === depSec || sectionName.endsWith(`.${depSec}`)) {\r\n matchedScope = depSec;\r\n break;\r\n }\r\n }\r\n\r\n if (!matchedScope) continue;\r\n\r\n const scope = scopeForCargoSection(matchedScope);\r\n const deps = extractTomlDeps(section.lines);\r\n\r\n for (const dep of deps) {\r\n if (seen.has(dep.name)) continue;\r\n seen.add(dep.name);\r\n\r\n const locked = lockVersions.get(dep.name) || dep.version;\r\n const isRegistry = !dep.version || (\r\n !dep.version.startsWith('path=') &&\r\n !dep.version.startsWith('git=') &&\r\n !dep.version.startsWith('../')\r\n );\r\n\r\n const purl = isRegistry && locked\r\n ? buildPurl({ type: 'rust', name: dep.name, version: locked })\r\n : isRegistry\r\n ? buildPurl({ type: 'rust', name: dep.name })\r\n : undefined;\r\n\r\n const evidence: Evidence[] = [manifestEv];\r\n if (lockEv && locked && lockVersions.has(dep.name)) evidence.push(lockEv);\r\n\r\n const status: DependencyObservation['status'] =\r\n dep.version && (dep.version.startsWith('path=') || dep.version.startsWith('git='))\r\n ? dep.version.startsWith('git=')\r\n ? 'git_dependency'\r\n : 'local_path'\r\n : 'current';\r\n\r\n observations.push({\r\n id: `dep-${workspace.id}-${dep.name}`,\r\n workspaceId: workspace.id,\r\n ...(purl ? { purl } : {}),\r\n ecosystem: 'rust',\r\n name: dep.name,\r\n sourceType: isRegistry ? 'registry' : status === 'local_path' ? 'path' : 'git',\r\n direct: true,\r\n scope,\r\n ...(dep.version ? { requested: dep.version } : {}),\r\n ...(locked ? { locked } : {}),\r\n status,\r\n evidence,\r\n });\r\n }\r\n }\r\n\r\n return observations;\r\n }\r\n\r\n private fileExists(filePath: string): boolean {\r\n try {\r\n readFileSync(filePath, 'utf-8');\r\n return true;\r\n } catch {\r\n return false;\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * Default singleton instance.\r\n */\r\nexport const rustAdapter = new RustAdapter();\r\n", "/**\r\n * TechStack \u2014 Go ecosystem adapter.\r\n *\r\n * Parses go.mod manifests and go.sum to produce DependencyObservation[]\r\n * for Go workspaces.\r\n *\r\n * @see docs/specs/techstack-sdd.md \u00A76 Tier A\r\n */\r\n\r\nimport { readFileSync } from 'node:fs';\r\nimport type {\r\n DependencyObservation,\r\n DependencyScope,\r\n Evidence,\r\n EcosystemId,\r\n Workspace,\r\n} from '../types.js';\r\nimport type {\r\n EcosystemAdapter,\r\n InventoryOptions,\r\n} from './interface.js';\r\nimport { resolveIn, workspaceRoot } from './paths.js';\r\nimport { buildPurl } from '../registry/purl.js';\r\n\r\n// \u2500\u2500 Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nfunction manifestEvidence(path: string): Evidence {\r\n return { kind: 'manifest', source: path, retrievedAt: new Date().toISOString() };\r\n}\r\n\r\nfunction lockfileEvidence(path: string): Evidence {\r\n return { kind: 'lockfile', source: path, retrievedAt: new Date().toISOString() };\r\n}\r\n\r\nfunction cleanGoVersion(v: string): string {\r\n return v.replace(/^v/i, '');\r\n}\r\n\r\n// \u2500\u2500 go.mod parser\r\n\r\ninterface GoRequireStmt {\r\n readonly modulePath: string;\r\n readonly version: string;\r\n /** Indirect dependencies (// indirect comment) */\r\n readonly indirect?: boolean;\r\n}\r\n\r\n/**\r\n * Parse a go.mod file to extract require statements.\r\n * Handles:\r\n * require module/path v1.2.3\r\n * require (\r\n * module/path v1.2.3\r\n * module/other v0.5.0 // indirect\r\n * )\r\n * exclude, replace, retract are ignored.\r\n */\r\nfunction parseGoMod(content: string): GoRequireStmt[] {\r\n const deps: GoRequireStmt[] = [];\r\n const lines = content.split('\\n');\r\n let inRequireBlock = false;\r\n\r\n for (const raw of lines) {\r\n const line = raw.trim();\r\n\r\n // Skip comments and empty lines\r\n if (line === '' || line.startsWith('//')) continue;\r\n\r\n // Track require blocks\r\n if (line.startsWith('require (') && line.endsWith('(')) {\r\n inRequireBlock = true;\r\n continue;\r\n }\r\n if (line.startsWith('require ') && !line.includes('(')) {\r\n // Single-line require\r\n const m = line.match(/^require\\s+(\\S+)\\s+(\\S+)/);\r\n if (m) {\r\n const indirect = raw.includes('// indirect');\r\n deps.push({ modulePath: m[1]!, version: cleanGoVersion(m[2]!), indirect });\r\n }\r\n continue;\r\n }\r\n\r\n if (inRequireBlock) {\r\n if (line === ')') {\r\n inRequireBlock = false;\r\n continue;\r\n }\r\n // Module path v1.2.3 // indirect\r\n const m = line.match(/^(\\S+)\\s+(\\S+)/);\r\n if (m) {\r\n const indirect = raw.includes('// indirect');\r\n deps.push({ modulePath: m[1]!, version: cleanGoVersion(m[2]!), indirect });\r\n }\r\n continue;\r\n }\r\n\r\n // Skip exclude/replace/retract blocks\r\n if (line.startsWith('exclude') || line.startsWith('replace') || line.startsWith('retract')) {\r\n continue;\r\n }\r\n }\r\n\r\n return deps;\r\n}\r\n\r\n/**\r\n * Parse go.sum to extract resolved versions.\r\n * Format: module_path version h1:hash\r\n * module_path version/go.mod h1:hash\r\n */\r\nfunction parseGoSum(content: string): Map<string, string> {\r\n const versions = new Map<string, string>();\r\n for (const raw of content.split('\\n')) {\r\n const line = raw.trim();\r\n if (!line) continue;\r\n // module_path version hash\r\n const m = line.match(/^(\\S+)\\s+(\\S+)\\s+\\S+/);\r\n if (m) {\r\n const modulePath = m[1]!;\r\n const version = cleanGoVersion(m[2]!);\r\n // Only set if not already set (first occurrence wins)\r\n if (!versions.has(modulePath)) {\r\n // Skip pseudo-versions like v0.0.0-20240701012345-abcdef\r\n versions.set(modulePath, version);\r\n }\r\n }\r\n }\r\n return versions;\r\n}\r\n\r\n/**\r\n * Extract the module name from go.mod (go module statement).\r\n */\r\nfunction parseGoModuleName(content: string): string | undefined {\r\n for (const raw of content.split('\\n')) {\r\n const line = raw.trim();\r\n const m = line.match(/^module\\s+(\\S+)/);\r\n if (m) return m[1]!;\r\n }\r\n return undefined;\r\n}\r\n\r\n// \u2500\u2500 Adapter \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nexport class GoAdapter implements EcosystemAdapter {\r\n readonly ecosystem: EcosystemId = 'go';\r\n\r\n async inventory(\r\n workspace: Workspace,\r\n options: InventoryOptions,\r\n ): Promise<readonly DependencyObservation[]> {\r\n const observations: DependencyObservation[] = [];\r\n const root = workspaceRoot(workspace, options);\r\n const seen = new Set<string>();\r\n\r\n // Find go.mod\r\n const goModPath = workspace.manifests.find((m) => m.includes('go.mod'))\r\n || (this.fileExists(resolveIn(root, 'go.mod')) ? 'go.mod' : undefined);\r\n if (!goModPath) return [];\r\n\r\n const fullManifestPath = resolveIn(root, goModPath);\r\n let goModContent: string;\r\n try {\r\n goModContent = readFileSync(fullManifestPath, 'utf-8');\r\n } catch {\r\n return [];\r\n }\r\n\r\n const manifestEv = manifestEvidence(fullManifestPath);\r\n\r\n // Parse go.mod\r\n const requires = parseGoMod(goModContent);\r\n const modName = parseGoModuleName(goModContent);\r\n\r\n // Parse go.sum for locked versions\r\n const goSumPath = resolveIn(root, 'go.sum');\r\n let lockVersions = new Map<string, string>();\r\n let lockEv: Evidence | undefined;\r\n try {\r\n const sumContent = readFileSync(goSumPath, 'utf-8');\r\n lockVersions = parseGoSum(sumContent);\r\n lockEv = lockfileEvidence(goSumPath);\r\n } catch {\r\n // No go.sum\r\n }\r\n\r\n for (const req of requires) {\r\n if (seen.has(req.modulePath)) continue;\r\n seen.add(req.modulePath);\r\n\r\n // Skip the module itself if it appears (rare but possible)\r\n if (req.modulePath === modName) continue;\r\n\r\n // Determine scope\r\n // Go has no dev/prod distinction in go.mod \u2014 everything is runtime\r\n // unless marked indirect (which Go treats as transitive)\r\n const scope: DependencyScope = req.indirect ? 'transitive' : 'runtime';\r\n const direct = !req.indirect;\r\n\r\n // Resolve locked version\r\n const locked = lockVersions.get(req.modulePath) || req.version;\r\n\r\n // Go module paths work like: github.com/gorilla/mux\r\n // Build PURL with full module path as name\r\n const purl = buildPurl({ type: 'go', name: req.modulePath, version: locked });\r\n\r\n const evidence: Evidence[] = [manifestEv];\r\n if (lockEv && lockVersions.has(req.modulePath)) evidence.push(lockEv);\r\n\r\n observations.push({\r\n id: `dep-${workspace.id}-${req.modulePath}`,\r\n workspaceId: workspace.id,\r\n purl,\r\n ecosystem: 'go',\r\n name: req.modulePath,\r\n sourceType: 'registry',\r\n direct,\r\n scope,\r\n requested: req.version,\r\n ...(locked ? { locked } : {}),\r\n status: 'current',\r\n evidence,\r\n });\r\n }\r\n\r\n return observations;\r\n }\r\n\r\n private fileExists(filePath: string): boolean {\r\n try {\r\n readFileSync(filePath, 'utf-8');\r\n return true;\r\n } catch {\r\n return false;\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * Default singleton instance.\r\n */\r\nexport const goAdapter = new GoAdapter();\r\n", "/**\r\n * TechStack \u2014 .NET ecosystem adapter.\r\n *\r\n * Parses .csproj files and project.assets.json to produce\r\n * DependencyObservation[] for .NET workspaces.\r\n *\r\n * @see docs/specs/techstack-sdd.md \u00A76 Tier A\r\n */\r\n\r\nimport { readFileSync, readdirSync } from 'node:fs';\r\nimport { join } from 'node:path';\r\nimport type {\r\n DependencyObservation,\r\n Evidence,\r\n EcosystemId,\r\n Workspace,\r\n} from '../types.js';\r\nimport type {\r\n EcosystemAdapter,\r\n InventoryOptions,\r\n} from './interface.js';\r\nimport { workspaceRoot } from './paths.js';\r\nimport { buildPurl } from '../registry/purl.js';\r\n\r\n// \u2500\u2500 Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nfunction manifestEvidence(path: string): Evidence {\r\n return { kind: 'manifest', source: path, retrievedAt: new Date().toISOString() };\r\n}\r\n\r\nfunction lockfileEvidence(path: string): Evidence {\r\n return { kind: 'lockfile', source: path, retrievedAt: new Date().toISOString() };\r\n}\r\n\r\n// \u2500\u2500 Minimal XML parser for .csproj \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\ninterface CsprojPackageRef {\r\n readonly name: string;\r\n readonly version: string | undefined;\r\n}\r\n\r\n/**\r\n * Parse a .csproj file to extract PackageReference items.\r\n * Handles:\r\n * <PackageReference Include=\"Newtonsoft.Json\" Version=\"13.0.3\" />\r\n * <PackageReference Include=\"Serilog\" Version=\"4.2.0\">\r\n * <PrivateAssets>all</PrivateAssets>\r\n * </PackageReference>\r\n * <PackageReference Include=\"Microsoft.AspNetCore.App\" />\r\n * Condition attributes are ignored.\r\n */\r\nfunction parseCsproj(content: string): CsprojPackageRef[] {\r\n const refs: CsprojPackageRef[] = [];\r\n // Match: <PackageReference Include=\"Name\" Version=\"ver\" ... />\r\n const regex = /<PackageReference\\s+Include\\s*=\\s*\"([^\"]+)\"\\s*(?:Version\\s*=\\s*\"([^\"]*)\")?\\s*\\/?\\s*>/g;\r\n let match: RegExpExecArray | null;\r\n while ((match = regex.exec(content)) !== null) {\r\n const name = match[1]!;\r\n const version = match[2] || undefined;\r\n refs.push({ name, version });\r\n }\r\n return refs;\r\n}\r\n\r\n/**\r\n * Parse project.assets.json for resolved dependency versions.\r\n * Format: {\r\n * \"libraries\": {\r\n * \"Newtonsoft.Json/13.0.3\": { ... },\r\n * \"Serilog/4.2.0\": { ... }\r\n * }\r\n * }\r\n */\r\nfunction parseProjectAssetsJson(content: string): Map<string, string> {\r\n const versions = new Map<string, string>();\r\n try {\r\n const json = JSON.parse(content) as {\r\n libraries?: Record<string, { type?: string }>;\r\n };\r\n if (json.libraries) {\r\n for (const key of Object.keys(json.libraries)) {\r\n // Format: \"PackageName/Version\"\r\n const sepIndex = key.lastIndexOf('/');\r\n if (sepIndex >= 0) {\r\n const name = key.slice(0, sepIndex);\r\n const version = key.slice(sepIndex + 1);\r\n if (name && version) {\r\n versions.set(name, version);\r\n }\r\n }\r\n }\r\n }\r\n } catch {\r\n // Malformed JSON\r\n }\r\n return versions;\r\n}\r\n\r\n// \u2500\u2500 Adapter \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nexport class DotNetAdapter implements EcosystemAdapter {\r\n readonly ecosystem: EcosystemId = 'dotnet';\r\n\r\n async inventory(\r\n workspace: Workspace,\r\n options: InventoryOptions,\r\n ): Promise<readonly DependencyObservation[]> {\r\n const observations: DependencyObservation[] = [];\r\n const root = workspaceRoot(workspace, options);\r\n const seen = new Set<string>();\r\n\r\n // Find .csproj file via readdirSync\r\n let csprojPath: string | undefined;\r\n try {\r\n const files = readdirSync(root);\r\n const csproj = files.find((f: string) => f.endsWith('.csproj'));\r\n if (csproj) csprojPath = join(root, csproj);\r\n } catch {\r\n // Can't read directory\r\n }\r\n\r\n if (!csprojPath) return [];\r\n\r\n let csprojContent: string;\r\n try {\r\n csprojContent = readFileSync(csprojPath, 'utf-8');\r\n } catch {\r\n return [];\r\n }\r\n\r\n const manifestEv = manifestEvidence(csprojPath);\r\n\r\n // Parse PackageReferences\r\n const refs = parseCsproj(csprojContent);\r\n\r\n // Read project.assets.json for locked versions\r\n const assetsPath = join(root, 'project.assets.json');\r\n let lockVersions = new Map<string, string>();\r\n let lockEv: Evidence | undefined;\r\n try {\r\n const assetsContent = readFileSync(assetsPath, 'utf-8');\r\n lockVersions = parseProjectAssetsJson(assetsContent);\r\n lockEv = lockfileEvidence(assetsPath);\r\n } catch {\r\n // No assets file\r\n }\r\n\r\n for (const ref of refs) {\r\n if (seen.has(ref.name)) continue;\r\n seen.add(ref.name);\r\n\r\n const locked = lockVersions.get(ref.name) || ref.version;\r\n\r\n // .NET PackageReferences are always registry (NuGet)\r\n const purl = locked\r\n ? buildPurl({ type: 'dotnet', name: ref.name, version: locked })\r\n : buildPurl({ type: 'dotnet', name: ref.name });\r\n\r\n const evidence: Evidence[] = [manifestEv];\r\n if (lockEv && lockVersions.has(ref.name)) evidence.push(lockEv);\r\n\r\n observations.push({\r\n id: `dep-${workspace.id}-${ref.name}`,\r\n workspaceId: workspace.id,\r\n purl,\r\n ecosystem: 'dotnet',\r\n name: ref.name,\r\n sourceType: 'registry',\r\n direct: true,\r\n scope: 'runtime',\r\n ...(ref.version ? { requested: ref.version } : {}),\r\n ...(locked ? { locked } : {}),\r\n status: 'current',\r\n evidence,\r\n });\r\n }\r\n\r\n return observations;\r\n }\r\n}\r\n\r\n/**\r\n * Default singleton instance.\r\n */\r\nexport const dotNetAdapter = new DotNetAdapter();\r\n", "/**\r\n * TechStack \u2014 PHP ecosystem adapter.\r\n *\r\n * Parses composer.json and composer.lock to produce\r\n * DependencyObservation[] for PHP workspaces.\r\n *\r\n * @see docs/specs/techstack-sdd.md \u00A76 Tier A\r\n */\r\n\r\nimport { readFileSync } from 'node:fs';\r\nimport { join } from 'node:path';\r\nimport type {\r\n DependencyObservation,\r\n DependencyScope,\r\n Evidence,\r\n EcosystemId,\r\n Workspace,\r\n} from '../types.js';\r\nimport type {\r\n EcosystemAdapter,\r\n InventoryOptions,\r\n} from './interface.js';\r\nimport { workspaceRoot } from './paths.js';\r\nimport { buildPurl } from '../registry/purl.js';\r\n\r\n// \u2500\u2500 Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nfunction manifestEvidence(path: string): Evidence {\r\n return { kind: 'manifest', source: path, retrievedAt: new Date().toISOString() };\r\n}\r\n\r\nfunction lockfileEvidence(path: string): Evidence {\r\n return { kind: 'lockfile', source: path, retrievedAt: new Date().toISOString() };\r\n}\r\n\r\n// \u2500\u2500 composer.json types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\ninterface ComposerJson {\r\n readonly require?: Record<string, string>;\r\n readonly 'require-dev'?: Record<string, string>;\r\n}\r\n\r\n// \u2500\u2500 composer.lock types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\ninterface ComposerLockPackage {\r\n readonly name: string;\r\n readonly version: string;\r\n readonly type?: string;\r\n readonly 'require'?: Record<string, string>;\r\n readonly 'require-dev'?: Record<string, string>;\r\n}\r\n\r\ninterface ComposerLock {\r\n readonly packages?: ComposerLockPackage[];\r\n readonly 'packages-dev'?: ComposerLockPackage[];\r\n}\r\n\r\n/**\r\n * Parse a composer.lock file to extract resolved versions.\r\n */\r\nfunction parseComposerLock(content: string): Map<string, string> {\r\n const versions = new Map<string, string>();\r\n try {\r\n const lock = JSON.parse(content) as ComposerLock;\r\n for (const pkg of [...(lock.packages ?? []), ...(lock['packages-dev'] ?? [])]) {\r\n versions.set(pkg.name, pkg.version);\r\n }\r\n } catch {\r\n // Malformed lockfile\r\n }\r\n return versions;\r\n}\r\n\r\n/**\r\n * Determine status from version constraint.\r\n */\r\nfunction statusForComposerSpec(spec: string): DependencyObservation['status'] {\r\n if (spec.startsWith('file:') || spec.startsWith('path:')) return 'local_path';\r\n if (spec.startsWith('git@') || spec.startsWith('git:') || spec.startsWith('http')) return 'git_dependency';\r\n return 'current';\r\n}\r\n\r\n/**\r\n * Determine sourceType from version constraint.\r\n */\r\nfunction sourceTypeForComposerSpec(spec: string): Exclude<DependencyObservation['sourceType'], undefined> {\r\n if (spec.startsWith('file:') || spec.startsWith('path:')) return 'path';\r\n if (spec.startsWith('git@') || spec.startsWith('git:') || spec.startsWith('http')) return 'git';\r\n return 'registry';\r\n}\r\n\r\n// \u2500\u2500 Adapter \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nexport class PhpAdapter implements EcosystemAdapter {\r\n readonly ecosystem: EcosystemId = 'php';\r\n\r\n async inventory(\r\n workspace: Workspace,\r\n options: InventoryOptions,\r\n ): Promise<readonly DependencyObservation[]> {\r\n const observations: DependencyObservation[] = [];\r\n const root = workspaceRoot(workspace, options);\r\n const seen = new Set<string>();\r\n\r\n // Find composer.json\r\n const composerJsonPath = workspace.manifests.find((m) => m.includes('composer.json'))\r\n || (this.fileExists(join(root, 'composer.json')) ? join(root, 'composer.json') : undefined);\r\n if (!composerJsonPath) return [];\r\n\r\n let content: string;\r\n try {\r\n content = readFileSync(composerJsonPath, 'utf-8');\r\n } catch {\r\n return [];\r\n }\r\n\r\n const manifestEv = manifestEvidence(composerJsonPath);\r\n\r\n // Parse composer.json\r\n let composerJson: ComposerJson;\r\n try {\r\n composerJson = JSON.parse(content) as ComposerJson;\r\n } catch {\r\n return [];\r\n }\r\n\r\n // Parse composer.lock for resolved versions\r\n const lockPath = join(root, 'composer.lock');\r\n let lockVersions = new Map<string, string>();\r\n let lockEv: Evidence | undefined;\r\n try {\r\n const lockContent = readFileSync(lockPath, 'utf-8');\r\n lockVersions = parseComposerLock(lockContent);\r\n lockEv = lockfileEvidence(lockPath);\r\n } catch {\r\n // No lockfile\r\n }\r\n\r\n // Process require (runtime deps) and require-dev (dev deps)\r\n const sections: Array<{ deps: Record<string, string> | undefined; scope: DependencyScope }> = [\r\n { deps: composerJson.require, scope: 'runtime' },\r\n { deps: composerJson['require-dev'], scope: 'development' },\r\n ];\r\n\r\n for (const { deps, scope } of sections) {\r\n if (!deps) continue;\r\n for (const [name, constraint] of Object.entries(deps)) {\r\n if (seen.has(name)) continue;\r\n seen.add(name);\r\n\r\n const locked = lockVersions.get(name);\r\n const status = statusForComposerSpec(constraint);\r\n const sourceType = sourceTypeForComposerSpec(constraint);\r\n const isRegistry = sourceType === 'registry';\r\n\r\n const purl = isRegistry && locked\r\n ? buildPurl({ type: 'php', name, version: locked })\r\n : isRegistry\r\n ? buildPurl({ type: 'php', name })\r\n : undefined;\r\n\r\n const evidence: Evidence[] = [manifestEv];\r\n if (lockEv && locked) evidence.push(lockEv);\r\n\r\n observations.push({\r\n id: `dep-${workspace.id}-${name}`,\r\n workspaceId: workspace.id,\r\n ...(purl ? { purl } : {}),\r\n ecosystem: 'php',\r\n name,\r\n sourceType,\r\n direct: true,\r\n scope,\r\n requested: constraint,\r\n ...(locked ? { locked } : {}),\r\n status,\r\n evidence,\r\n });\r\n }\r\n }\r\n\r\n return observations;\r\n }\r\n\r\n private fileExists(filePath: string): boolean {\r\n try {\r\n readFileSync(filePath, 'utf-8');\r\n return true;\r\n } catch {\r\n return false;\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * Default singleton instance.\r\n */\r\nexport const phpAdapter = new PhpAdapter();\r\n", "/**\r\n * TechStack \u2014 Dart ecosystem adapter.\r\n *\r\n * Parses pubspec.yaml and pubspec.lock to produce\r\n * DependencyObservation[] for Dart/Flutter workspaces.\r\n *\r\n * @see docs/specs/techstack-sdd.md \u00A76 Tier A\r\n */\r\n\r\nimport { readFileSync } from 'node:fs';\r\nimport { join } from 'node:path';\r\nimport type {\r\n DependencyObservation,\r\n DependencyScope,\r\n Evidence,\r\n EcosystemId,\r\n Workspace,\r\n} from '../types.js';\r\nimport type {\r\n EcosystemAdapter,\r\n InventoryOptions,\r\n} from './interface.js';\r\nimport { workspaceRoot } from './paths.js';\r\nimport { buildPurl } from '../registry/purl.js';\r\n\r\n// \u2500\u2500 Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nfunction manifestEvidence(path: string): Evidence {\r\n return { kind: 'manifest', source: path, retrievedAt: new Date().toISOString() };\r\n}\r\n\r\nfunction lockfileEvidence(path: string): Evidence {\r\n return { kind: 'lockfile', source: path, retrievedAt: new Date().toISOString() };\r\n}\r\n\r\n// \u2500\u2500 Minimal YAML parser (line-based, sufficient for pubspec.yaml) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Parse a pubspec.yaml to extract dependencies sections.\r\n * Returns a map of section name \u2192 Map of dependency name \u2192 constraint.\r\n *\r\n * Handles:\r\n * dependencies:\r\n * flutter:\r\n * sdk: flutter\r\n * http: ^1.2.0\r\n * dev_dependencies:\r\n * test: ^1.24.0\r\n */\r\nfunction parsePubspecYaml(content: string): Map<string, Map<string, string>> {\r\n const sections = new Map<string, Map<string, string>>();\r\n let currentSection: string | undefined;\r\n let currentName: string | undefined;\r\n\r\n for (const raw of content.split('\\n')) {\r\n const line = raw.trimEnd();\r\n const trimmed = line.trim();\r\n if (trimmed === '' || trimmed.startsWith('#')) continue;\r\n\r\n // Section header (no indent): `dependencies:`\r\n const sectionMatch = trimmed.match(/^(\\w[\\w-]*):\\s*$/);\r\n if (sectionMatch && line.startsWith(sectionMatch[1]!)) {\r\n currentSection = sectionMatch[1]!;\r\n currentName = undefined;\r\n if (!sections.has(currentSection)) {\r\n sections.set(currentSection, new Map());\r\n }\r\n continue;\r\n }\r\n\r\n if (!currentSection) continue;\r\n\r\n // Dependency definition: ` package_name: ^1.0.0`\r\n // Or sub-properties: ` sdk: flutter` \u2014 skip these\r\n const depMatch = trimmed.match(/^(\\S[^:]*?):\\s*(.*)$/);\r\n if (depMatch && line.startsWith(' ') && !line.startsWith(' ')) {\r\n currentName = depMatch[1]!.trim();\r\n let constraint = depMatch[2]!.trim();\r\n // Handle empty constraints (sdk: flutter has constraint as sub-props)\r\n if (!constraint || constraint.startsWith('{')) {\r\n constraint = '*';\r\n }\r\n const sec = sections.get(currentSection)!;\r\n sec.set(currentName, constraint);\r\n }\r\n }\r\n\r\n return sections;\r\n}\r\n\r\n/**\r\n * Parse pubspec.lock to extract resolved versions.\r\n * pubspec.lock uses YAML format with packages as a map.\r\n *\r\n * packages:\r\n * http:\r\n * version: \"1.2.0\"\r\n * path:\r\n * version: \"2.0.0\"\r\n */\r\nfunction parsePubspecLock(content: string): Map<string, string> {\r\n const versions = new Map<string, string>();\r\n const lines = content.split('\\n');\r\n let currentPackage: string | undefined;\r\n let inPackages = false;\r\n\r\n for (const raw of lines) {\r\n const trimmed = raw.trim();\r\n if (trimmed === '') continue;\r\n\r\n if (trimmed === 'packages:') {\r\n inPackages = true;\r\n continue;\r\n }\r\n\r\n if (!inPackages) continue;\r\n\r\n // Package name: ` package_name:`\r\n const pkgMatch = trimmed.match(/^(\\S[^:]*):\\s*$/);\r\n if (pkgMatch && raw.startsWith(' ') && !raw.startsWith(' ')) {\r\n currentPackage = pkgMatch[1]!.trim();\r\n continue;\r\n }\r\n\r\n // Version: ` version: \"1.2.0\"`\r\n if (currentPackage) {\r\n const verMatch = trimmed.match(/^version:\\s*\"?([^\"\\s]+)\"?\\s*$/);\r\n if (verMatch && raw.startsWith(' ')) {\r\n versions.set(currentPackage, verMatch[1]!);\r\n currentPackage = undefined;\r\n }\r\n }\r\n }\r\n\r\n return versions;\r\n}\r\n\r\n// \u2500\u2500 Adapter \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nexport class DartAdapter implements EcosystemAdapter {\r\n readonly ecosystem: EcosystemId = 'dart';\r\n\r\n async inventory(\r\n workspace: Workspace,\r\n options: InventoryOptions,\r\n ): Promise<readonly DependencyObservation[]> {\r\n const observations: DependencyObservation[] = [];\r\n const root = workspaceRoot(workspace, options);\r\n const seen = new Set<string>();\r\n\r\n // Find pubspec.yaml\r\n const pubspecPath = workspace.manifests.find((m) => m.includes('pubspec.yaml'))\r\n || (this.fileExists(join(root, 'pubspec.yaml')) ? join(root, 'pubspec.yaml') : undefined);\r\n if (!pubspecPath) return [];\r\n\r\n let content: string;\r\n try {\r\n content = readFileSync(pubspecPath, 'utf-8');\r\n } catch {\r\n return [];\r\n }\r\n\r\n const manifestEv = manifestEvidence(pubspecPath);\r\n\r\n // Parse pubspec.yaml\r\n const sections = parsePubspecYaml(content);\r\n\r\n // Parse pubspec.lock\r\n const lockPath = join(root, 'pubspec.lock');\r\n let lockVersions = new Map<string, string>();\r\n let lockEv: Evidence | undefined;\r\n try {\r\n const lockContent = readFileSync(lockPath, 'utf-8');\r\n lockVersions = parsePubspecLock(lockContent);\r\n lockEv = lockfileEvidence(lockPath);\r\n } catch {\r\n // No lockfile\r\n }\r\n\r\n // Process sections\r\n const sectionMapping: Array<{ yamlSection: string; scope: DependencyScope }> = [\r\n { yamlSection: 'dependencies', scope: 'runtime' },\r\n { yamlSection: 'dev_dependencies', scope: 'development' },\r\n { yamlSection: 'dependency_overrides', scope: 'runtime' },\r\n ];\r\n\r\n for (const { yamlSection, scope } of sectionMapping) {\r\n const deps = sections.get(yamlSection);\r\n if (!deps) continue;\r\n\r\n for (const [name, constraint] of deps) {\r\n if (seen.has(name)) continue;\r\n seen.add(name);\r\n\r\n // Skip sdk dependencies (they are the Dart SDK itself)\r\n if (constraint === '*' || constraint.startsWith('{')) continue;\r\n\r\n const locked = lockVersions.get(name);\r\n\r\n // Determine status\r\n let status: DependencyObservation['status'] = 'current';\r\n let sourceType: Exclude<DependencyObservation['sourceType'], undefined> = 'registry';\r\n\r\n if (constraint.startsWith('path:')) {\r\n status = 'local_path';\r\n sourceType = 'path';\r\n } else if (constraint.startsWith('git:')) {\r\n status = 'git_dependency';\r\n sourceType = 'git';\r\n } else if (constraint.startsWith('{')) {\r\n // Inline map: e.g. {sdk: flutter}\r\n status = 'local_path';\r\n sourceType = 'path';\r\n }\r\n\r\n const isRegistry = sourceType === 'registry';\r\n // Strip caret/tilde/>= for PURL \u2014 use locked if available\r\n const purl = isRegistry && (locked || constraint)\r\n ? buildPurl({ type: 'dart', name, version: locked || constraint.replace(/^[\\^~>=<\\s]+/, '') })\r\n : isRegistry\r\n ? buildPurl({ type: 'dart', name })\r\n : undefined;\r\n\r\n const evidence: Evidence[] = [manifestEv];\r\n if (lockEv && locked) evidence.push(lockEv);\r\n\r\n observations.push({\r\n id: `dep-${workspace.id}-${name}`,\r\n workspaceId: workspace.id,\r\n ...(purl ? { purl } : {}),\r\n ecosystem: 'dart',\r\n name,\r\n sourceType,\r\n direct: true,\r\n scope,\r\n ...(constraint && constraint !== '*' ? { requested: constraint } : {}),\r\n ...(locked ? { locked } : {}),\r\n status,\r\n evidence,\r\n });\r\n }\r\n }\r\n\r\n return observations;\r\n }\r\n\r\n private fileExists(filePath: string): boolean {\r\n try {\r\n readFileSync(filePath, 'utf-8');\r\n return true;\r\n } catch {\r\n return false;\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * Default singleton instance.\r\n */\r\nexport const dartAdapter = new DartAdapter();\r\n", "/**\n * TechStack \u2014 Maven ecosystem adapter (Tier B).\n *\n * Parses pom.xml for direct dependencies. Partial support \u2014 no lockfile\n * parsing (Maven has no standardized lockfile); version resolution\n * requires `mvn dependency:tree` which is not invoked here.\n *\n * @see docs/specs/techstack-sdd.md \u00A76 Tier B\n */\n\nimport { readFileSync } from 'node:fs';\nimport type {\n DependencyObservation,\n DependencyScope,\n Evidence,\n EcosystemId,\n Workspace,\n} from '../types.js';\nimport type { EcosystemAdapter, InventoryOptions } from './interface.js';\nimport { buildPurl } from '../registry/purl.js';\n\ninterface MavenDependency {\n readonly groupId: string;\n readonly artifactId: string;\n readonly version?: string | undefined;\n readonly scope?: string | undefined;\n}\n\nfunction manifestEvidence(path: string): Evidence {\n return { kind: 'manifest', source: path, retrievedAt: new Date().toISOString() };\n}\n\n/**\n * Minimal XML parser for `<dependency>` blocks inside pom.xml.\n * Does not handle inheritance/dependencyManagement \u2014 this is Tier B partial.\n */\nfunction parsePomDependencies(xml: string): MavenDependency[] {\n const deps: MavenDependency[] = [];\n const depRegex = /<dependency>\\s*([\\s\\S]*?)<\\/dependency>/g;\n let match: RegExpExecArray | null;\n while ((match = depRegex.exec(xml)) !== null) {\n const block = match[1]!;\n const groupId = block.match(/<groupId>([^<]+)<\\/groupId>/)?.[1]?.trim();\n const artifactId = block.match(/<artifactId>([^<]+)<\\/artifactId>/)?.[1]?.trim();\n const version = block.match(/<version>([^<]+)<\\/version>/)?.[1]?.trim();\n const scope = block.match(/<scope>([^<]+)<\\/scope>/)?.[1]?.trim();\n if (groupId && artifactId) {\n deps.push({ groupId, artifactId, version, scope });\n }\n }\n return deps;\n}\n\nfunction mavenScopeToScope(scope: string | undefined): DependencyScope {\n switch (scope) {\n case 'test': return 'development';\n case 'provided': return 'optional';\n case 'runtime': return 'runtime';\n case 'compile': return 'runtime';\n default: return 'runtime';\n }\n}\n\nexport class MavenAdapter implements EcosystemAdapter {\n readonly ecosystem: EcosystemId = 'maven';\n\n async inventory(\n workspace: Workspace,\n _options: InventoryOptions,\n ): Promise<readonly DependencyObservation[]> {\n const observations: DependencyObservation[] = [];\n const pomPath = workspace.manifests.find((m) => m.includes('pom.xml'));\n if (!pomPath) return [];\n\n let content: string;\n try {\n content = readFileSync(pomPath, 'utf-8');\n } catch {\n return [];\n }\n\n const manifestEv = manifestEvidence(pomPath);\n const deps = parsePomDependencies(content);\n const seen = new Set<string>();\n\n for (const dep of deps) {\n const name = `${dep.groupId}:${dep.artifactId}`;\n if (seen.has(name)) continue;\n seen.add(name);\n\n const purl = dep.version\n ? buildPurl({ type: 'maven', name, version: dep.version })\n : buildPurl({ type: 'maven', name });\n\n observations.push({\n id: `dep-${workspace.id}-${name}`,\n workspaceId: workspace.id,\n purl,\n ecosystem: 'maven',\n name,\n sourceType: 'registry',\n direct: true,\n scope: mavenScopeToScope(dep.scope),\n ...(dep.version ? { requested: dep.version } : {}),\n status: 'current',\n evidence: [manifestEv],\n });\n }\n\n return observations;\n }\n}\n\nexport const mavenAdapter = new MavenAdapter();\n", "/**\n * TechStack \u2014 Ruby/Bundler ecosystem adapter (Tier B).\n *\n * Parses Gemfile and Gemfile.lock for direct and transitive dependencies.\n * Partial support \u2014 no registry API; OSV-only advisory enrichment.\n *\n * @see docs/specs/techstack-sdd.md \u00A76 Tier B\n */\n\nimport { readFileSync } from 'node:fs';\nimport type {\n DependencyObservation,\n DependencyScope,\n Evidence,\n EcosystemId,\n Workspace,\n} from '../types.js';\nimport type { EcosystemAdapter, InventoryOptions } from './interface.js';\nimport { buildPurl } from '../registry/purl.js';\n\nfunction manifestEvidence(path: string): Evidence {\n return { kind: 'manifest', source: path, retrievedAt: new Date().toISOString() };\n}\n\nfunction lockfileEvidence(path: string): Evidence {\n return { kind: 'lockfile', source: path, retrievedAt: new Date().toISOString() };\n}\n\n/**\n * Parse Gemfile for direct `gem 'name'` and `gem 'name', 'version'` calls.\n */\nfunction parseGemfile(content: string): Array<{ name: string; version?: string | undefined }> {\n const gems: Array<{ name: string; version?: string | undefined }> = [];\n const gemRegex = /gem\\s+['\"]([^'\"]+)['\"](?:\\s*,\\s*['\"]([^'\"]+)['\"])?/g;\n let match: RegExpExecArray | null;\n while ((match = gemRegex.exec(content)) !== null) {\n const name = match[1]!;\n // Skip gems that are clearly comments or block-evaluated\n if (name === 'rails' || name === 'ruby') continue;\n gems.push({ name, version: match[2] });\n }\n return gems;\n}\n\n/**\n * Parse Gemfile.lock `GEM` section for resolved versions.\n * Format: ` name (version)`.\n */\nfunction parseGemfileLock(content: string): Map<string, string> {\n const versions = new Map<string, string>();\n const lines = content.split('\\n');\n let inSpecs = false;\n for (const line of lines) {\n if (line.startsWith('GEM')) { inSpecs = true; continue; }\n if (inSpecs && /^[A-Z]/.test(line) && !line.startsWith(' ')) { inSpecs = false; continue; }\n if (!inSpecs) continue;\n const match = /^\\s{4,}([\\w-]+)\\s+\\(([^)]+)\\)/.exec(line);\n if (match) {\n const version = match[2]!.split(' ')[0] ?? match[2]!;\n versions.set(match[1]!, version);\n }\n }\n return versions;\n}\n\nexport class RubyAdapter implements EcosystemAdapter {\n readonly ecosystem: EcosystemId = 'ruby';\n\n async inventory(\n workspace: Workspace,\n _options: InventoryOptions,\n ): Promise<readonly DependencyObservation[]> {\n const observations: DependencyObservation[] = [];\n const gemfilePath = workspace.manifests.find((m) => m.includes('Gemfile'));\n if (!gemfilePath) return [];\n\n let content: string;\n try {\n content = readFileSync(gemfilePath, 'utf-8');\n } catch {\n return [];\n }\n\n const manifestEv = manifestEvidence(gemfilePath);\n const gems = parseGemfile(content);\n const seen = new Set<string>();\n\n // Parse lockfile\n const lockfilePath = workspace.lockfiles.find((l) => l.includes('Gemfile.lock'));\n let lockVersions = new Map<string, string>();\n let lockEv: Evidence | undefined;\n if (lockfilePath) {\n try {\n const lockContent = readFileSync(lockfilePath, 'utf-8');\n lockVersions = parseGemfileLock(lockContent);\n lockEv = lockfileEvidence(lockfilePath);\n } catch {\n // No lockfile\n }\n }\n\n for (const gem of gems) {\n if (seen.has(gem.name)) continue;\n seen.add(gem.name);\n\n const locked = lockVersions.get(gem.name);\n const version = locked ?? gem.version;\n const purl = version\n ? buildPurl({ type: 'gem', name: gem.name, version })\n : buildPurl({ type: 'gem', name: gem.name });\n\n const evidence: Evidence[] = [manifestEv];\n if (lockEv && locked) evidence.push(lockEv);\n\n observations.push({\n id: `dep-${workspace.id}-${gem.name}`,\n workspaceId: workspace.id,\n purl,\n ecosystem: 'ruby',\n name: gem.name,\n sourceType: 'registry',\n direct: true,\n scope: 'runtime' as DependencyScope,\n ...(gem.version ? { requested: gem.version } : {}),\n ...(locked ? { locked } : {}),\n status: 'current',\n evidence,\n });\n }\n\n return observations;\n }\n}\n\nexport const rubyAdapter = new RubyAdapter();\n", "/**\n * TechStack \u2014 Elixir/Hex ecosystem adapter (Tier B).\n *\n * Parses mix.exs for direct dependencies and mix.lock for resolved versions.\n * Partial support \u2014 no registry API; OSV-only advisory enrichment.\n *\n * @see docs/specs/techstack-sdd.md \u00A76 Tier B\n */\n\nimport { readFileSync } from 'node:fs';\nimport type {\n DependencyObservation,\n DependencyScope,\n Evidence,\n EcosystemId,\n Workspace,\n} from '../types.js';\nimport type { EcosystemAdapter, InventoryOptions } from './interface.js';\nimport { buildPurl } from '../registry/purl.js';\n\nfunction manifestEvidence(path: string): Evidence {\n return { kind: 'manifest', source: path, retrievedAt: new Date().toISOString() };\n}\n\nfunction lockfileEvidence(path: string): Evidence {\n return { kind: 'lockfile', source: path, retrievedAt: new Date().toISOString() };\n}\n\n/**\n * Parse mix.exs `defp deps do` block for `{:name, \"version\"}` tuples.\n */\nfunction parseMixExsDeps(content: string): Array<{ name: string; version?: string | undefined }> {\n const deps: Array<{ name: string; version?: string | undefined }> = [];\n // Match: {:name, \"version\"} or {:name, \"~> x.y\"} or {:name, github: \"...\"} or {:name, path: \"...\"}\n const depRegex = /\\{:(\\w+),\\s*[\"']([^\"']+)[\"']\\}/g;\n let match: RegExpExecArray | null;\n while ((match = depRegex.exec(content)) !== null) {\n deps.push({ name: match[1]!, version: match[2] });\n }\n return deps;\n}\n\n/**\n * Parse mix.lock for resolved hex versions.\n * Format: `{\"name\", hex: \":uuid\", \"1.2.3\"}`\n */\nfunction parseMixLock(content: string): Map<string, string> {\n const versions = new Map<string, string>();\n const lockRegex = /\\{:\"(\\w+)\",\\s*hex: \"[^\"]*\",\\s*\"([^\"]+)\"/g;\n let match: RegExpExecArray | null;\n while ((match = lockRegex.exec(content)) !== null) {\n versions.set(match[1]!, match[2]!);\n }\n return versions;\n}\n\nexport class ElixirAdapter implements EcosystemAdapter {\n readonly ecosystem: EcosystemId = 'elixir';\n\n async inventory(\n workspace: Workspace,\n _options: InventoryOptions,\n ): Promise<readonly DependencyObservation[]> {\n const observations: DependencyObservation[] = [];\n const mixExsPath = workspace.manifests.find((m) => m.includes('mix.exs'));\n if (!mixExsPath) return [];\n\n let content: string;\n try {\n content = readFileSync(mixExsPath, 'utf-8');\n } catch {\n return [];\n }\n\n const manifestEv = manifestEvidence(mixExsPath);\n const deps = parseMixExsDeps(content);\n const seen = new Set<string>();\n\n // Parse lockfile\n const lockfilePath = workspace.lockfiles.find((l) => l.includes('mix.lock'));\n let lockVersions = new Map<string, string>();\n let lockEv: Evidence | undefined;\n if (lockfilePath) {\n try {\n const lockContent = readFileSync(lockfilePath, 'utf-8');\n lockVersions = parseMixLock(lockContent);\n lockEv = lockfileEvidence(lockfilePath);\n } catch {\n // No lockfile\n }\n }\n\n for (const dep of deps) {\n if (seen.has(dep.name)) continue;\n seen.add(dep.name);\n\n const locked = lockVersions.get(dep.name);\n const version = locked ?? dep.version;\n const purl = version\n ? buildPurl({ type: 'hex', name: dep.name, version })\n : buildPurl({ type: 'hex', name: dep.name });\n\n const evidence: Evidence[] = [manifestEv];\n if (lockEv && locked) evidence.push(lockEv);\n\n observations.push({\n id: `dep-${workspace.id}-${dep.name}`,\n workspaceId: workspace.id,\n purl,\n ecosystem: 'elixir',\n name: dep.name,\n sourceType: 'registry',\n direct: true,\n scope: 'runtime' as DependencyScope,\n ...(dep.version ? { requested: dep.version } : {}),\n ...(locked ? { locked } : {}),\n status: 'current',\n evidence,\n });\n }\n\n return observations;\n }\n}\n\nexport const elixirAdapter = new ElixirAdapter();\n", "/**\n * TechStack \u2014 C/C++ ecosystem adapter (Tier C).\n *\n * Best-effort: parses conanfile.txt / conanfile.py for `[requires]` and\n * vcpkg.json for dependencies. No lockfile resolution; coverage='unsupported'.\n *\n * @see docs/specs/techstack-sdd.md \u00A76 Tier C\n */\n\nimport { readFileSync } from 'node:fs';\nimport type {\n DependencyObservation,\n Evidence,\n EcosystemId,\n Workspace,\n} from '../types.js';\nimport type { EcosystemAdapter, InventoryOptions } from './interface.js';\nimport { buildPurl } from '../registry/purl.js';\n\nfunction manifestEvidence(path: string): Evidence {\n return { kind: 'manifest', source: path, retrievedAt: new Date().toISOString() };\n}\n\n/**\n * Parse conanfile.txt `[requires]` section.\n */\nfunction parseConanTxt(content: string): Array<{ name: string; version?: string | undefined }> {\n const deps: Array<{ name: string; version?: string | undefined }> = [];\n const requiresMatch = /\\[requires\\]\\s*\\n([\\s\\S]*?)(?:\\[|$)/;\n const block = requiresMatch.exec(content)?.[1];\n if (!block) return deps;\n\n for (const line of block.split('\\n')) {\n const trimmed = line.trim();\n if (!trimmed || trimmed.startsWith('#')) continue;\n const parts = trimmed.split('/');\n if (parts.length >= 2) {\n deps.push({ name: parts[0]!, version: parts[1] });\n } else {\n deps.push({ name: trimmed });\n }\n }\n return deps;\n}\n\n/**\n * Parse vcpkg.json `dependencies` array.\n */\nfunction parseVcpkgJson(content: string): Array<{ name: string; version?: string | undefined }> {\n const deps: Array<{ name: string; version?: string | undefined }> = [];\n try {\n const json = JSON.parse(content) as { dependencies?: Array<string | { name: string; version?: string }> };\n for (const dep of json.dependencies ?? []) {\n if (typeof dep === 'string') {\n deps.push({ name: dep });\n } else {\n deps.push({ name: dep.name, version: dep.version });\n }\n }\n } catch {\n // Malformed\n }\n return deps;\n}\n\nexport class CppAdapter implements EcosystemAdapter {\n readonly ecosystem: EcosystemId = 'cpp';\n\n async inventory(\n workspace: Workspace,\n _options: InventoryOptions,\n ): Promise<readonly DependencyObservation[]> {\n const observations: DependencyObservation[] = [];\n const seen = new Set<string>();\n\n for (const manifestPath of workspace.manifests) {\n let content: string;\n try {\n content = readFileSync(manifestPath, 'utf-8');\n } catch {\n continue;\n }\n\n const manifestEv = manifestEvidence(manifestPath);\n let deps: Array<{ name: string; version?: string | undefined }> = [];\n\n if (manifestPath.includes('conanfile')) {\n deps = parseConanTxt(content);\n } else if (manifestPath.includes('vcpkg.json')) {\n deps = parseVcpkgJson(content);\n } else {\n continue;\n }\n\n for (const dep of deps) {\n if (seen.has(dep.name)) continue;\n seen.add(dep.name);\n\n const purl = dep.version\n ? buildPurl({ type: 'conan', name: dep.name, version: dep.version })\n : buildPurl({ type: 'conan', name: dep.name });\n\n observations.push({\n id: `dep-${workspace.id}-${dep.name}`,\n workspaceId: workspace.id,\n purl,\n ecosystem: 'cpp',\n name: dep.name,\n sourceType: 'registry',\n direct: true,\n scope: 'runtime',\n ...(dep.version ? { requested: dep.version } : {}),\n // Tier C \u2014 we cannot verify current/version status\n status: 'unknown',\n evidence: [manifestEv],\n });\n }\n }\n\n return observations;\n }\n}\n\nexport const cppAdapter = new CppAdapter();\n", "/**\n * TechStack \u2014 Snapshot diff utility.\n *\n * Compares two snapshots to identify added, removed, and changed dependencies.\n *\n * @see docs/specs/techstack-sdd.md \u00A79\n */\n\nimport type { DependencyObservation, Snapshot } from './types.js';\n\nexport interface SnapshotDiff {\n added: DependencyObservation[];\n removed: DependencyObservation[];\n changed: Array<{\n name: string;\n ecosystem: string;\n field: string;\n from: string;\n to: string;\n }>;\n}\n\n/**\n * Compare two snapshots by dependency name + ecosystem.\n *\n * Returns added (in new but not old), removed (in old but not new), and\n * changed (version/status differences for matching dependencies).\n */\nexport function diffSnapshots(oldSnapshot: Snapshot, newSnapshot: Snapshot): SnapshotDiff {\n const oldByKey = new Map<string, DependencyObservation>();\n for (const dep of oldSnapshot.dependencies) {\n oldByKey.set(`${dep.ecosystem}:${dep.name}`, dep);\n }\n\n const newByKey = new Map<string, DependencyObservation>();\n for (const dep of newSnapshot.dependencies) {\n newByKey.set(`${dep.ecosystem}:${dep.name}`, dep);\n }\n\n const added: DependencyObservation[] = [];\n const removed: DependencyObservation[] = [];\n const changed: SnapshotDiff['changed'] = [];\n\n // Find added + changed\n for (const [key, newDep] of newByKey) {\n const oldDep = oldByKey.get(key);\n if (!oldDep) {\n added.push(newDep);\n continue;\n }\n\n // Check version changes\n const fields: Array<keyof DependencyObservation> = ['locked', 'requested', 'status', 'latestStable'];\n for (const field of fields) {\n const oldVal = String(oldDep[field] ?? '');\n const newVal = String(newDep[field] ?? '');\n if (oldVal !== newVal) {\n changed.push({\n name: newDep.name,\n ecosystem: newDep.ecosystem,\n field: String(field),\n from: oldVal,\n to: newVal,\n });\n }\n }\n }\n\n // Find removed\n for (const [key, oldDep] of oldByKey) {\n if (!newByKey.has(key)) {\n removed.push(oldDep);\n }\n }\n\n return { added, removed, changed };\n}\n", "/**\n * TechStack \u2014 SBOM (Software Bill of Materials) export.\n *\n * Converts a Snapshot into SPDX or CycloneDX JSON format.\n *\n * @see docs/specs/techstack-sdd.md \u00A79\n */\n\nimport type { Snapshot } from './types.js';\n\n// \u2500\u2500 SPDX \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface SpdxDocument {\n spdxVersion: string;\n dataLicense: string;\n SPDXID: string;\n name: string;\n documentNamespace: string;\n creationInfo: {\n created: string;\n creators: string[];\n };\n packages: Array<{\n name: string;\n SPDXID: string;\n versionInfo?: string | undefined;\n downloadLocation?: string | undefined;\n licenseConcluded?: string | undefined;\n }>;\n}\n\nexport function toSpdx(snapshot: Snapshot): SpdxDocument {\n const created = snapshot.createdAt;\n return {\n spdxVersion: 'SPDX-2.3',\n dataLicense: 'CC0-1.0',\n SPDXID: 'SPDXRef-DOCUMENT',\n name: `TechStack-SBOM-${snapshot.projectId}`,\n documentNamespace: `https://wrongstack.dev/spdx/${snapshot.id}`,\n creationInfo: {\n created,\n creators: ['Tool: WrongStack TechStack Engine'],\n },\n packages: snapshot.dependencies.map((dep, index) => ({\n name: dep.name,\n SPDXID: `SPDXRef-Package-${index}`,\n versionInfo: dep.locked ?? dep.requested,\n downloadLocation: dep.purl ? `https://purl.io/${dep.purl}` : 'NOASSERTION',\n licenseConcluded: dep.license ?? 'NOASSERTION',\n })),\n };\n}\n\n// \u2500\u2500 CycloneDX \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface CycloneDXBom {\n bomFormat: string;\n specVersion: string;\n version: number;\n metadata: {\n timestamp: string;\n tools: Array<{ name: string; version: string }>;\n };\n components: Array<{\n type: string;\n name: string;\n version?: string | undefined;\n purl?: string | undefined;\n licenses?: Array<{ license: { id: string } }> | undefined;\n }>;\n}\n\nexport function toCycloneDX(snapshot: Snapshot): CycloneDXBom {\n return {\n bomFormat: 'CycloneDX',\n specVersion: '1.5',\n version: 1,\n metadata: {\n timestamp: snapshot.createdAt,\n tools: [{ name: 'WrongStack TechStack Engine', version: snapshot.adapterVersion }],\n },\n components: snapshot.dependencies.map((dep) => ({\n type: 'library',\n name: dep.name,\n version: dep.locked ?? dep.requested,\n ...(dep.purl ? { purl: dep.purl } : {}),\n ...(dep.license\n ? { licenses: [{ license: { id: dep.license } }] }\n : {}),\n })),\n };\n}\n", "/**\n * TechStack \u2014 Remediation planning.\n *\n * Generates dry-run upgrade plans from a snapshot's findings. NEVER mutates\n * dependency files \u2014 the plan is read-only output that the user must\n * explicitly approve before any `language_package` or `install` tool runs.\n *\n * @see docs/specs/techstack-sdd.md \u00A72 (R25), \u00A79\n */\n\nimport type { DependencyObservation, Finding, Snapshot } from './types.js';\n\nexport interface UpgradePlanItem {\n readonly dependencyName: string;\n readonly ecosystem: string;\n readonly workspaceId: string;\n readonly currentVersion: string | undefined;\n readonly targetVersion: string | undefined;\n readonly action: Finding['action'];\n readonly severity: Finding['severity'];\n readonly rationale: string;\n readonly breakingRisk: string | undefined;\n /** Command suggestion (informational only \u2014 never auto-executed). */\n readonly suggestedCommand: string | undefined;\n}\n\nexport interface UpgradePlan {\n readonly snapshotId: string;\n readonly generatedAt: string;\n readonly items: readonly UpgradePlanItem[];\n readonly summary: {\n readonly total: number;\n readonly patch: number;\n readonly minor: number;\n readonly major: number;\n readonly replace: number;\n readonly remove: number;\n readonly investigate: number;\n };\n readonly warning: string;\n}\n\n/**\n * Suggest a package-manager command for a given ecosystem + action.\n * This is purely informational \u2014 the actual execution goes through the\n * permission-gated `language_package` tool.\n */\nfunction suggestCommand(\n ecosystem: string,\n name: string,\n action: Finding['action'],\n targetVersion?: string,\n): string | undefined {\n const ver = targetVersion ? `@${targetVersion}` : '@latest';\n switch (ecosystem) {\n case 'npm':\n if (action === 'remove') return `npm uninstall ${name}`;\n return `npm install ${name}${ver}`;\n case 'python':\n if (action === 'remove') return `pip uninstall ${name}`;\n return `pip install ${name}${ver}`;\n case 'rust':\n if (action === 'remove') return `cargo remove ${name}`;\n return `cargo add ${name}@${targetVersion ?? 'latest'}`;\n case 'go':\n if (action === 'remove') return `go get ${name}@none`;\n return `go get ${name}@${targetVersion ?? 'latest'}`;\n case 'php':\n if (action === 'remove') return `composer remove ${name}`;\n return `composer require ${name}:${targetVersion ?? 'latest'}`;\n case 'dotnet':\n if (action === 'remove') return `dotnet remove package ${name}`;\n return `dotnet add package ${name}`;\n default:\n return undefined;\n }\n}\n\n/**\n * Generate a dry-run upgrade plan from a snapshot.\n *\n * This function is **read-only** \u2014 it never touches manifests, lockfiles,\n * or the filesystem. It only reads the snapshot's findings and produces\n * a structured plan the user can review.\n *\n * @param snapshot The enriched snapshot to plan from.\n * @returns A structured upgrade plan.\n */\nexport function generateUpgradePlan(snapshot: Snapshot): UpgradePlan {\n const findings = snapshot.findings as readonly Finding[];\n const items: UpgradePlanItem[] = [];\n\n for (const finding of findings) {\n if (finding.action === 'none') continue;\n\n const dep = snapshot.dependencies.find(\n (d: DependencyObservation) => d.id === finding.dependencyId,\n );\n if (!dep) continue;\n\n const targetVersion = dep.latestStable ?? dep.resolvable ?? dep.wanted;\n\n items.push({\n dependencyName: dep.name,\n ecosystem: dep.ecosystem,\n workspaceId: dep.workspaceId,\n currentVersion: dep.locked ?? dep.installed ?? dep.requested,\n targetVersion,\n action: finding.action,\n severity: finding.severity,\n rationale: finding.rationale,\n breakingRisk: finding.breakingRisk,\n suggestedCommand: suggestCommand(dep.ecosystem, dep.name, finding.action, targetVersion),\n });\n }\n\n // Sort by severity (critical first, then high, medium, low, info)\n const severityOrder = new Map([\n ['critical', 0],\n ['high', 1],\n ['medium', 2],\n ['low', 3],\n ['info', 4],\n ]);\n items.sort((a, b) => {\n const sa = severityOrder.get(a.severity) ?? 5;\n const sb = severityOrder.get(b.severity) ?? 5;\n return sa - sb;\n });\n\n const summary = {\n total: items.length,\n patch: items.filter((i) => i.action === 'upgrade_patch').length,\n minor: items.filter((i) => i.action === 'upgrade_minor').length,\n major: items.filter((i) => i.action === 'upgrade_major').length,\n replace: items.filter((i) => i.action === 'replace').length,\n remove: items.filter((i) => i.action === 'remove').length,\n investigate: items.filter((i) => i.action === 'investigate').length,\n };\n\n return {\n snapshotId: snapshot.id,\n generatedAt: new Date().toISOString(),\n items,\n summary,\n warning:\n 'This plan is read-only. No dependency files will be modified unless you explicitly approve and execute each item.',\n };\n}\n\n/**\n * Render an upgrade plan as Markdown for the report endpoint or CLI display.\n */\nexport function renderPlanMarkdown(plan: UpgradePlan): string {\n const lines: string[] = [\n '# TechStack Remediation Plan',\n '',\n `**Generated:** ${plan.generatedAt}`,\n `**Snapshot:** ${plan.snapshotId}`,\n `**Total items:** ${plan.summary.total}`,\n '',\n `> \u26A0\uFE0F ${plan.warning}`,\n '',\n ];\n\n if (plan.items.length === 0) {\n lines.push('_No remediation actions needed \u2014 all dependencies are current._');\n return lines.join('\\n');\n }\n\n // Summary table\n lines.push('## Summary', '');\n lines.push('| Action | Count |');\n lines.push('|---|---|');\n lines.push(`| Patch upgrade | ${plan.summary.patch} |`);\n lines.push(`| Minor upgrade | ${plan.summary.minor} |`);\n lines.push(`| Major upgrade | ${plan.summary.major} |`);\n lines.push(`| Replace | ${plan.summary.replace} |`);\n lines.push(`| Remove | ${plan.summary.remove} |`);\n lines.push(`| Investigate | ${plan.summary.investigate} |`);\n lines.push('');\n\n // Detail items\n lines.push('## Items', '');\n for (const item of plan.items) {\n const icon =\n item.severity === 'critical' ? '\uD83D\uDD34' :\n item.severity === 'high' ? '\uD83D\uDFE0' :\n item.severity === 'medium' ? '\uD83D\uDFE1' :\n item.severity === 'low' ? '\uD83D\uDD35' : '\u2139\uFE0F';\n\n lines.push(`### ${icon} ${item.dependencyName} (${item.ecosystem})`, '');\n lines.push(`- **Action:** ${item.action}`);\n lines.push(`- **Current:** ${item.currentVersion ?? 'unknown'}`);\n lines.push(`- **Target:** ${item.targetVersion ?? 'latest'}`);\n lines.push(`- **Severity:** ${item.severity}`);\n lines.push(`- **Rationale:** ${item.rationale}`);\n if (item.breakingRisk) lines.push(`- **Breaking risk:** ${item.breakingRisk}`);\n if (item.suggestedCommand) {\n lines.push(`- **Suggested command:** \\`${item.suggestedCommand}\\``);\n }\n lines.push('');\n }\n\n return lines.join('\\n');\n}\n", "/**\n * TechStack \u2014 Registry metadata HTTP client.\n *\n * Per-ecosystem registry API clients built on Node's built-in https module.\n * Features: in-memory cache with ETag/TTL, per-host concurrency limit (max 3),\n * exponential backoff on 429/5xx responses.\n *\n * @see docs/specs/techstack-sdd.md \u00A75, \u00A76\n */\n\nimport { get as httpsGet, type RequestOptions } from 'node:https';\nimport type { IncomingMessage } from 'node:http';\nimport { get as httpGet } from 'node:http';\n\n// \u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface RegistryEntry {\n readonly latestStable?: string | undefined;\n readonly license?: string | undefined;\n readonly deprecated?: boolean | undefined;\n readonly yanked?: boolean | undefined;\n readonly retrievedAt: string;\n readonly source: string;\n}\n\nexport interface CacheEntry {\n readonly data: RegistryEntry;\n readonly etag?: string | undefined;\n readonly expiresAt: number;\n}\n\nexport interface HostConcurrency {\n active: number;\n readonly queue: Array<() => void>;\n}\n\n// \u2500\u2500 Constants \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst DEFAULT_TTL_MS = 10 * 60 * 1000; // 10 minutes\nconst MAX_CONCURRENCY_PER_HOST = 3;\nconst MAX_RETRIES = 3;\nconst BASE_BACKOFF_MS = 1000;\n\n// \u2500\u2500 In-memory cache \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst registryCache = new Map<string, CacheEntry>();\nconst hostConcurrency = new Map<string, HostConcurrency>();\n\nfunction getCacheKey(host: string, path: string): string {\n return `${host}${path}`;\n}\n\nfunction getCached(key: string): RegistryEntry | undefined {\n const entry = registryCache.get(key);\n if (!entry) return undefined;\n if (Date.now() > entry.expiresAt) {\n registryCache.delete(key);\n return undefined;\n }\n return entry.data;\n}\n\nfunction setCache(\n key: string,\n data: RegistryEntry,\n etag?: string,\n ttlMs = DEFAULT_TTL_MS,\n): void {\n registryCache.set(key, {\n data,\n etag,\n expiresAt: Date.now() + ttlMs,\n });\n}\n\n// \u2500\u2500 Concurrency limiter \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction acquireHostSlot(host: string): Promise<void> {\n let concurrency = hostConcurrency.get(host);\n if (!concurrency) {\n concurrency = { active: 0, queue: [] };\n hostConcurrency.set(host, concurrency);\n }\n\n if (concurrency.active < MAX_CONCURRENCY_PER_HOST) {\n concurrency.active++;\n return Promise.resolve();\n }\n\n return new Promise<void>((resolve) => {\n concurrency!.queue.push(resolve);\n });\n}\n\nfunction releaseHostSlot(host: string): void {\n const concurrency = hostConcurrency.get(host);\n if (!concurrency) return;\n\n concurrency.active--;\n\n if (concurrency.queue.length > 0) {\n const next = concurrency.queue.shift();\n if (next) {\n concurrency.active++;\n next();\n }\n }\n}\n\n// \u2500\u2500 Backoff helper \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction computeBackoff(attempt: number, statusCode: number): number {\n const base = statusCode === 429 ? BASE_BACKOFF_MS * 2 : BASE_BACKOFF_MS;\n return base * Math.pow(2, attempt) + Math.random() * 500;\n}\n\n// \u2500\u2500 HTTPS/HTTP fetch wrapper \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ninterface FetchResponse {\n readonly statusCode: number;\n readonly headers: Record<string, string | string[] | undefined>;\n readonly body: string;\n readonly isFromCache: boolean;\n}\n\nfunction httpsFetch(\n hostname: string,\n path: string,\n etag?: string,\n signal?: AbortSignal,\n): Promise<FetchResponse> {\n return new Promise((resolve, reject) => {\n const options: RequestOptions = {\n hostname,\n path,\n method: 'GET',\n headers: {\n Accept: 'application/json',\n 'User-Agent': 'WrongStack-TechStack/1.0',\n ...(etag ? { 'If-None-Match': etag } : {}),\n },\n signal,\n timeout: 15000,\n };\n\n const mod = hostname === 'localhost' || hostname === '127.0.0.1' ? httpGet : httpsGet;\n\n const req = mod(options, (res: IncomingMessage) => {\n const statusCode = res.statusCode ?? 0;\n const responseHeaders = res.headers as Record<string, string | string[] | undefined>;\n\n let body = '';\n res.on('data', (chunk: string) => {\n body += chunk;\n });\n res.on('end', () => {\n resolve({\n statusCode,\n headers: responseHeaders,\n body,\n isFromCache: false,\n });\n });\n });\n\n req.on('error', (err: Error) => {\n reject(err);\n });\n\n req.on('timeout', () => {\n req.destroy();\n reject(new Error(`Request timeout for ${hostname}${path}`));\n });\n\n req.end();\n });\n}\n\n// \u2500\u2500 Registry metadata response parser \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ninterface EcosystemFetcher {\n readonly host: string;\n readonly path: (name: string) => string;\n readonly parser: (json: Record<string, unknown>, name: string, ecosystem: string) => RegistryEntry | undefined;\n}\n\n// \u2500\u2500 Per-ecosystem parsers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Parse an npm packument into a {@link RegistryEntry}.\n *\n * Exported so the deprecation rule is unit-testable without a network round\n * trip \u2014 it was untested, and drifted into flagging most of the ecosystem dead.\n */\nexport function parseNpmPackument(json: Record<string, unknown>, name: string): RegistryEntry {\n const latestVersion = (json['dist-tags'] as Record<string, string> | undefined)?.['latest'];\n\n // A package counts as deprecated only when its *latest* version is marked so\n // \u2014 that's what `npm deprecate` leaves behind for a dead package, and it's\n // the signal other tooling reads.\n //\n // Deliberately NOT \"any version in history is deprecated\": every long-lived\n // package eventually deprecates an old beta or a bad patch, so that rule\n // flags essentially the entire mature ecosystem. It marked vitest, biome and\n // cross-env dead in this very repo.\n let deprecated: boolean | undefined;\n if (latestVersion && json.versions && typeof json.versions === 'object') {\n const versions = json.versions as Record<string, Record<string, unknown>>;\n deprecated = versions[latestVersion]?.deprecated ? true : undefined;\n }\n\n // npm has no standard \"yanked\" field in registry metadata.\n return {\n latestStable: latestVersion,\n license: (json.license as string) ?? undefined,\n deprecated: deprecated ?? undefined,\n yanked: undefined,\n retrievedAt: new Date().toISOString(),\n source: `https://registry.npmjs.org/${name}`,\n };\n}\n\n// \u2500\u2500 Per-ecosystem fetcher definitions \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst ECOSYSTEM_FETCHERS: Readonly<Record<string, EcosystemFetcher>> = {\n npm: {\n host: 'registry.npmjs.org',\n path: (name: string) => {\n // Scoped packages: /@scope%2Fname\n const encoded = name.startsWith('@') ? name.replace('/', '%2F') : name;\n return `/${encoded}`;\n },\n parser: parseNpmPackument,\n },\n\n python: {\n host: 'pypi.org',\n path: (name: string) => `/pypi/${name}/json`,\n parser: (json: Record<string, unknown>): RegistryEntry => {\n const info = json.info as Record<string, unknown> | undefined;\n return {\n latestStable: (info?.version as string) ?? undefined,\n license: (info?.license as string) ?? undefined,\n deprecated: (info?.deprecated as boolean) ?? undefined,\n yanked: undefined,\n retrievedAt: new Date().toISOString(),\n source: `https://pypi.org/pypi/${info?.name ?? ''}/json`,\n };\n },\n },\n\n cargo: {\n host: 'crates.io',\n path: (name: string) => `/api/v1/crates/${name}`,\n parser: (json: Record<string, unknown>): RegistryEntry => {\n const crate = json.crate as Record<string, unknown> | undefined;\n return {\n latestStable: (crate?.max_stable_version as string) ?? (crate?.max_version as string) ?? undefined,\n license: (crate?.license as string) ?? undefined,\n deprecated: undefined, // crates.io doesn't have deprecation\n yanked: undefined,\n retrievedAt: new Date().toISOString(),\n source: `https://crates.io/api/v1/crates/${crate?.name ?? ''}`,\n };\n },\n },\n\n golang: {\n host: 'proxy.golang.org',\n path: (module: string) => `/${module}/@latest`,\n parser: (json: Record<string, unknown>, module: string): RegistryEntry => {\n return {\n latestStable: (json.Version as string) ?? undefined,\n license: undefined, // Go proxy doesn't provide license\n deprecated: undefined,\n yanked: undefined,\n retrievedAt: new Date().toISOString(),\n source: `https://proxy.golang.org/${module}/@latest`,\n };\n },\n },\n\n nuget: {\n host: 'api.nuget.org',\n path: (name: string) => {\n const lower = name.toLowerCase();\n return `/v3/registration5-semver1/${lower}/index.json`;\n },\n parser: (json: Record<string, unknown>, name: string): RegistryEntry => {\n // NuGet V3 registration index has items with catalog entries\n const items = json.items as Array<Record<string, unknown>> | undefined;\n let latestStable: string | undefined;\n\n if (items && items.length > 0) {\n // Items are ordered; look through all items for the latest stable version\n for (const item of items) {\n const itemItems = item.items as Array<Record<string, unknown>> | undefined;\n if (itemItems && Array.isArray(itemItems)) {\n for (const entry of itemItems) {\n const catalogEntry = entry.catalogEntry as Record<string, unknown> | undefined;\n if (catalogEntry?.version) {\n const ver = catalogEntry.version as string;\n // Prefer non-prerelease\n if (!latestStable || (!ver.includes('-') && latestStable.includes('-'))) {\n latestStable = ver;\n } else if (!ver.includes('-') && !latestStable.includes('-')) {\n // Both stable \u2014 take greater\n if (ver > latestStable) latestStable = ver;\n }\n }\n }\n }\n }\n }\n\n return {\n latestStable,\n license: undefined, // License requires per-version catalog entry\n deprecated: undefined,\n yanked: undefined,\n retrievedAt: new Date().toISOString(),\n source: `https://api.nuget.org/v3/registration5-semver1/${name.toLowerCase()}/index.json`,\n };\n },\n },\n\n composer: {\n host: 'repo.packagist.org',\n path: (name: string) => `/p2/${name}.json`,\n parser: (json: Record<string, unknown>, name: string): RegistryEntry => {\n const packages = json.packages as Record<string, Array<Record<string, unknown>>> | undefined;\n const versions = packages?.[name];\n if (!versions || versions.length === 0) {\n return { retrievedAt: new Date().toISOString(), source: 'packagist' };\n }\n\n // Find the latest stable version\n let latestStable: string | undefined;\n for (const ver of versions) {\n const version = ver.version as string;\n if (version && !version.includes('dev') && !version.includes('alpha') && !version.includes('beta') && !version.includes('RC') && !version.includes('rc')) {\n if (!latestStable || version > latestStable) {\n latestStable = version;\n }\n }\n }\n\n // Use the latest version entry for license info\n const latest = versions[0]!;\n\n return {\n latestStable,\n license: latest.license as string ?? undefined,\n deprecated: (latest.deprecated as boolean) ?? undefined,\n yanked: (latest.abandoned as boolean) ?? undefined,\n retrievedAt: new Date().toISOString(),\n source: `https://repo.packagist.org/p2/${name}.json`,\n };\n },\n },\n\n pub: {\n host: 'pub.dev',\n path: (name: string) => `/api/packages/${name}`,\n parser: (json: Record<string, unknown>): RegistryEntry => {\n const latest = json.latest as Record<string, unknown> | undefined;\n return {\n latestStable: (json.latestVersion as string) ?? (latest?.version as string) ?? (json.version as string) ?? undefined,\n license: (latest?.license as string) ?? undefined,\n deprecated: (json.isDiscontinued as boolean) ?? undefined,\n yanked: (json.isRetracted as boolean) ?? undefined,\n retrievedAt: new Date().toISOString(),\n source: `https://pub.dev/api/packages/${(json.name as string) ?? ''}`,\n };\n },\n },\n};\n\n// \u2500\u2500 Public API \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface RegistryLookupOptions {\n /** Abort signal for cancellation. */\n readonly signal?: AbortSignal | undefined;\n /** Bypass cache and force a fresh lookup. */\n readonly force?: boolean | undefined;\n}\n\n/**\n * Look up registry metadata for a package in a given ecosystem.\n *\n * Returns `undefined` on 404/401 (private/unresolved package).\n * Throws on network errors (timeout, DNS failure).\n */\nexport async function lookupRegistry(\n ecosystem: string,\n name: string,\n options: RegistryLookupOptions = {},\n): Promise<RegistryEntry | undefined> {\n const fetcher = ECOSYSTEM_FETCHERS[ecosystem];\n if (!fetcher) {\n throw new Error(`Unsupported ecosystem for registry lookup: ${ecosystem}`);\n }\n\n const path = fetcher.path(name);\n const cacheKey = getCacheKey(fetcher.host, path);\n\n // Check cache (unless force refresh)\n if (!options.force) {\n const cached = getCached(cacheKey);\n if (cached) return cached;\n }\n\n // Acquire concurrency slot\n await acquireHostSlot(fetcher.host);\n try {\n // Get cached ETag\n const existingEntry = registryCache.get(cacheKey);\n const etag = existingEntry?.etag;\n\n let lastError: Error | undefined;\n for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {\n try {\n const response = await httpsFetch(fetcher.host, path, etag, options.signal);\n\n // 304 Not Modified \u2014 use cached data and extend TTL\n if (response.statusCode === 304 && existingEntry) {\n setCache(cacheKey, existingEntry.data, existingEntry.etag, DEFAULT_TTL_MS);\n return existingEntry.data;\n }\n\n // 401/403/404 \u2014 private or unresolved package\n if (response.statusCode === 401 || response.statusCode === 403 || response.statusCode === 404) {\n return undefined;\n }\n\n // 429/5xx \u2014 retry with backoff\n if (response.statusCode === 429 || response.statusCode >= 500) {\n if (attempt < MAX_RETRIES - 1) {\n const backoff = computeBackoff(attempt, response.statusCode);\n await sleep(backoff);\n continue;\n }\n throw new Error(`Registry ${fetcher.host} returned ${response.statusCode} after ${MAX_RETRIES} attempts`);\n }\n\n // Success (2xx)\n let json: Record<string, unknown>;\n try {\n json = JSON.parse(response.body) as Record<string, unknown>;\n } catch {\n throw new Error(`Invalid JSON response from ${fetcher.host}${path}`);\n }\n\n const parsed = fetcher.parser(json, name, ecosystem);\n if (!parsed) {\n // Parser returned nothing \u2014 package exists but no metadata\n return undefined;\n }\n\n // Cache with ETag\n const responseEtag = response.headers['etag'] as string | undefined;\n setCache(cacheKey, parsed, responseEtag, DEFAULT_TTL_MS);\n\n return parsed;\n } catch (err) {\n lastError = err instanceof Error ? err : new Error(String(err));\n if (attempt < MAX_RETRIES - 1) {\n const isRateLimit = err instanceof Error && err.message.includes('429');\n const backoff = computeBackoff(attempt, isRateLimit ? 429 : 500);\n await sleep(backoff);\n }\n }\n }\n\n throw lastError ?? new Error(`Failed to look up ${ecosystem}:${name}`);\n } finally {\n releaseHostSlot(fetcher.host);\n }\n}\n\n/**\n * Look up registry metadata for multiple packages in the same ecosystem.\n * Uses the same per-host concurrency limit for the batch.\n */\nexport async function lookupRegistryBatch(\n ecosystem: string,\n names: readonly string[],\n options: RegistryLookupOptions = {},\n): Promise<Map<string, RegistryEntry | undefined>> {\n const results = new Map<string, RegistryEntry | undefined>();\n\n // Process in parallel respecting concurrency limits\n const entries = await Promise.all(\n names.map(async (name) => {\n try {\n const entry = await lookupRegistry(ecosystem, name, options);\n return { name, entry } as const;\n } catch {\n return { name, entry: undefined } as const;\n }\n }),\n );\n\n for (const { name, entry } of entries) {\n results.set(name, entry);\n }\n\n return results;\n}\n\n/**\n * Get the list of supported ecosystem IDs for registry lookups.\n */\nexport function supportedRegistryEcosystems(): string[] {\n return Object.keys(ECOSYSTEM_FETCHERS);\n}\n\n/**\n * Clear the in-memory registry cache.\n * Useful for testing and when force-refreshing.\n */\nexport function clearRegistryCache(): void {\n registryCache.clear();\n hostConcurrency.clear();\n}\n", "/**\n * TechStack \u2014 OSV (Open Source Vulnerabilities) advisory client.\n *\n * Uses the OSV /v1/querybatch endpoint to batch-query vulnerability\n * information for lists of PackageURLs. Chunks requests into batches\n * of at most 500 packages per the OSV API limits.\n *\n * @see https://osv.dev/docs/\n */\n\nimport { get as httpsGet, type RequestOptions } from 'node:https';\nimport type { IncomingMessage } from 'node:http';\nimport type { Evidence } from '../types.js';\n\n// \u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/** OSV query batch request shape */\ninterface OsvQueryBatchRequest {\n readonly queries: ReadonlyArray<{\n readonly package: {\n readonly purl: string;\n };\n }>;\n}\n\n/** OSV query batch response shape */\ninterface OsvQueryBatchResponse {\n readonly results: ReadonlyArray<{\n readonly vulns?: ReadonlyArray<{\n readonly id: string;\n readonly summary?: string;\n readonly details?: string;\n readonly aliases?: readonly string[];\n readonly severity?: ReadonlyArray<{\n readonly type: string;\n readonly score: string;\n }>;\n readonly database_specific?: {\n readonly severity?: string;\n };\n readonly affected?: ReadonlyArray<{\n readonly database_specific?: {\n readonly severity?: string;\n };\n }>;\n }>;\n }>;\n}\n\n/** Parsed advisory for a single package */\nexport interface OsvAdvisory {\n readonly id: string;\n readonly summary: string;\n readonly severity: 'info' | 'low' | 'medium' | 'high' | 'critical';\n readonly aliases: readonly string[];\n}\n\n/** Result of querying OSV for a batch of packages */\nexport interface OsvBatchResult {\n /** Map from PURL to advisories found for that package */\n readonly advisories: Map<string, readonly OsvAdvisory[]>;\n readonly evidence: Evidence;\n}\n\n// \u2500\u2500 Constants \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst OSV_API_BASE = 'api.osv.dev';\nconst OSV_QUERY_BATCH_PATH = '/v1/querybatch';\nconst MAX_BATCH_SIZE = 500;\nconst MAX_RETRIES = 3;\nconst BASE_BACKOFF_MS = 1000;\n\n// \u2500\u2500 Severity mapping \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction mapSeverity(\n osvSeverity?: ReadonlyArray<{ readonly type: string; readonly score: string }>,\n databaseSeverity?: string,\n): 'info' | 'low' | 'medium' | 'high' | 'critical' {\n // Check CVSS score first\n if (osvSeverity && osvSeverity.length > 0) {\n for (const s of osvSeverity) {\n if (s.type === 'CVSS_V3' || s.type === 'CVSS_V2') {\n const score = parseFloat(s.score);\n if (score >= 9.0) return 'critical';\n if (score >= 7.0) return 'high';\n if (score >= 4.0) return 'medium';\n if (score >= 0.1) return 'low';\n }\n }\n }\n\n // Check database_specific severity\n if (databaseSeverity) {\n const ds = databaseSeverity.toLowerCase();\n if (ds === 'critical') return 'critical';\n if (ds === 'high') return 'high';\n if (ds === 'medium' || ds === 'moderate') return 'medium';\n if (ds === 'low') return 'low';\n }\n\n return 'info';\n}\n\n// \u2500\u2500 HTTP client \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction osvPostRequest(body: string, signal?: AbortSignal): Promise<{ statusCode: number; body: string }> {\n return new Promise((resolve, reject) => {\n const options: RequestOptions = {\n hostname: OSV_API_BASE,\n path: OSV_QUERY_BATCH_PATH,\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Content-Length': Buffer.byteLength(body).toString(),\n 'User-Agent': 'WrongStack-TechStack/1.0',\n },\n signal,\n timeout: 30000,\n };\n\n const req = httpsGet(options, (res: IncomingMessage) => {\n const statusCode = res.statusCode ?? 0;\n let responseBody = '';\n res.on('data', (chunk: string) => {\n responseBody += chunk;\n });\n res.on('end', () => {\n resolve({ statusCode, body: responseBody });\n });\n });\n\n req.on('error', (err: Error) => {\n reject(err);\n });\n\n req.on('timeout', () => {\n req.destroy();\n reject(new Error('OSV API request timeout'));\n });\n\n req.write(body);\n req.end();\n });\n}\n\n// \u2500\u2500 Sleep helper \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n// \u2500\u2500 Core function \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Query OSV for advisories matching a list of PackageURLs.\n *\n * Chunks requests into batches of at most 500 PURLs per the OSV API limits.\n * Returns a map from each queried PURL to its list of advisories (empty array\n * means no advisories found).\n */\nexport async function queryOsvBatch(\n purls: readonly string[],\n options: { signal?: AbortSignal | undefined } = {},\n): Promise<OsvBatchResult> {\n const advisories = new Map<string, readonly OsvAdvisory[]>();\n\n // Initialize empty arrays for all PURLs\n for (const purl of purls) {\n advisories.set(purl, []);\n }\n\n // Chunk PURLs into batches\n const batches: string[][] = [];\n for (let i = 0; i < purls.length; i += MAX_BATCH_SIZE) {\n batches.push(purls.slice(i, i + MAX_BATCH_SIZE));\n }\n\n let lastError: Error | undefined;\n\n for (const batch of batches) {\n const requestBody: OsvQueryBatchRequest = {\n queries: batch.map((purl) => ({\n package: { purl },\n })),\n };\n\n const jsonBody = JSON.stringify(requestBody);\n\n let success = false;\n for (let attempt = 0; attempt < MAX_RETRIES && !success; attempt++) {\n try {\n const response = await osvPostRequest(jsonBody, options.signal);\n\n if (response.statusCode === 200) {\n const result = JSON.parse(response.body) as OsvQueryBatchResponse;\n\n if (result.results && Array.isArray(result.results)) {\n for (let i = 0; i < result.results.length; i++) {\n const purl = batch[i];\n if (!purl) continue;\n\n const vulns = result.results[i]?.vulns;\n if (!vulns || vulns.length === 0) continue;\n\n const parsed: OsvAdvisory[] = [];\n for (const vuln of vulns) {\n // Determine severity\n const dbSpecific = vuln.database_specific;\n const affectedDbSpecific = vuln.affected?.[0]?.database_specific;\n const severitySource = dbSpecific?.severity ?? affectedDbSpecific?.severity;\n\n parsed.push({\n id: vuln.id,\n summary: vuln.summary ?? vuln.details ?? 'No summary available',\n severity: mapSeverity(vuln.severity, severitySource),\n aliases: vuln.aliases ?? [],\n });\n }\n\n advisories.set(purl, parsed);\n }\n }\n\n success = true;\n } else if (response.statusCode === 429 || response.statusCode >= 500) {\n // Rate limited or server error \u2014 retry with backoff\n if (attempt < MAX_RETRIES - 1) {\n const backoff = BASE_BACKOFF_MS * Math.pow(2, attempt) + Math.random() * 500;\n await sleep(backoff);\n } else {\n throw new Error(`OSV API returned ${response.statusCode} after ${MAX_RETRIES} attempts: ${response.body}`);\n }\n } else {\n // Other error \u2014 don't retry\n throw new Error(`OSV API returned ${response.statusCode}: ${response.body}`);\n }\n } catch (err) {\n lastError = err instanceof Error ? err : new Error(String(err));\n if (attempt < MAX_RETRIES - 1) {\n const backoff = BASE_BACKOFF_MS * Math.pow(2, attempt) + Math.random() * 500;\n await sleep(backoff);\n }\n }\n }\n\n if (!success && lastError) {\n // If a batch completely fails, we still have partial results from\n // earlier batches\n throw lastError;\n }\n }\n\n const evidence: Evidence = {\n kind: 'osv',\n source: 'https://api.osv.dev/v1/querybatch',\n retrievedAt: new Date().toISOString(),\n detail: `Queried ${purls.length} packages in ${batches.length} batch(es)`,\n };\n\n return { advisories, evidence };\n}\n\n/**\n * Query OSV for a single PURL.\n * Convenience wrapper around queryOsvBatch.\n */\nexport async function queryOsvSingle(\n purl: string,\n options: { signal?: AbortSignal | undefined } = {},\n): Promise<readonly OsvAdvisory[]> {\n const result = await queryOsvBatch([purl], options);\n return result.advisories.get(purl) ?? [];\n}\n", "/**\n * TechStack \u2014 Native audit command wrappers.\n *\n * Spawns ecosystem-native audit tools (npm audit, pip-audit, cargo-audit,\n * govulncheck, composer audit, dotnet package audit) and parses their\n * output into the TechStack advisory model.\n *\n * @see docs/specs/techstack-sdd.md \u00A76, \u00A77\n */\n\nimport { spawnSync, type SpawnSyncOptions } from 'node:child_process';\nimport { existsSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type { EcosystemId, Evidence } from '../types.js';\n\n// \u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface NativeAdvisory {\n readonly id: string;\n readonly packageName: string;\n readonly severity: 'info' | 'low' | 'medium' | 'high' | 'critical';\n readonly summary: string;\n readonly fixVersion?: string | undefined;\n readonly url?: string | undefined;\n readonly aliases: readonly string[];\n}\n\nexport interface NativeAuditResult {\n readonly advisories: readonly NativeAdvisory[];\n readonly evidence: Evidence;\n}\n\n// \u2500\u2500 Parse helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/** Map npm audit severity strings */\nfunction npmSeverity(s: string): NativeAdvisory['severity'] {\n switch (s.toLowerCase()) {\n case 'critical': return 'critical';\n case 'high': return 'high';\n case 'moderate':\n case 'medium': return 'medium';\n case 'low': return 'low';\n default: return 'info';\n }\n}\n\n/** Map cargo-audit severity strings */\nfunction cargoSeverity(s: string): NativeAdvisory['severity'] {\n switch (s.toLowerCase()) {\n case 'critical': return 'critical';\n case 'high': return 'high';\n case 'medium': return 'medium';\n case 'low': return 'low';\n default: return 'info';\n }\n}\n\n// \u2500\u2500 npm audit \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Run npm audit in the given workspace directory and parse JSON output.\n */\nexport function runNpmAudit(workspaceRoot: string): NativeAuditResult {\n const result = runAuditCommand('npm', ['audit', '--json'], workspaceRoot);\n const advisories: NativeAdvisory[] = [];\n let detailLines: string[] = [];\n\n if (result.status === 0 || result.status === 1) {\n // npm audit exits 0 if no vulns, 1 if vulns found, 2 if error\n try {\n const json = JSON.parse(result.stdout || '{}');\n const vulnerabilities = json.vulnerabilities as Record<string, Record<string, unknown>> | undefined;\n\n if (vulnerabilities) {\n for (const [pkg, info] of Object.entries(vulnerabilities)) {\n const via = info.via as Array<Record<string, unknown> | string> | undefined;\n if (!via) continue;\n\n for (const advisory of via) {\n if (typeof advisory === 'string') continue;\n const source = advisory.source as number | undefined;\n const name = advisory.name as string | undefined;\n // npm uses numeric source references \u2014 skip those\n if (typeof source === 'number') continue;\n\n advisories.push({\n id: (advisory.cve as string) ?? (advisory.ghsa as string) ?? `npm-${pkg}-${name ?? 'unknown'}`,\n packageName: pkg,\n severity: npmSeverity((info.severity as string) ?? 'info'),\n summary: (advisory.title as string) ?? (name ?? 'No summary'),\n fixVersion: (info.fixAvailable as string) ?? undefined,\n url: (advisory.url as string) ?? undefined,\n aliases: (advisory.cve as string) ? [(advisory.cve as string)] : [],\n });\n }\n }\n }\n\n const metadata = json.metadata as Record<string, unknown> | undefined;\n if (metadata) {\n detailLines = [\n `Total vulnerabilities: ${metadata.vulnerabilities as string ?? 'unknown'}`,\n `Total dependencies: ${metadata.totalDependencies as string ?? 'unknown'}`,\n ];\n }\n } catch {\n detailLines = ['Failed to parse npm audit JSON output'];\n }\n } else {\n detailLines = [`npm audit exited with code ${result.status}`];\n }\n\n const evidence: Evidence = {\n kind: 'audit',\n source: 'npm audit --json',\n retrievedAt: new Date().toISOString(),\n detail: detailLines.join('\\n') || `Found ${advisories.length} advisories`,\n };\n\n return { advisories, evidence };\n}\n\n// \u2500\u2500 pip-audit \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Run pip-audit in the given workspace directory.\n * pip-audit supports --requirement, --format json flags.\n */\nexport function runPipAudit(workspaceRoot: string): NativeAuditResult {\n // Try common requirements files\n const reqFiles = ['requirements.txt', 'requirements-dev.txt'];\n let reqFlag = '';\n for (const f of reqFiles) {\n if (existsSync(join(workspaceRoot, f))) {\n reqFlag = `--requirement ${f}`;\n break;\n }\n }\n\n const args = ['audit', '--format', 'json'];\n if (reqFlag) {\n args.push(...reqFlag.split(' '));\n }\n\n const result = runAuditCommand('pip-audit' in process.env ? 'pip-audit' : 'pip-audit', args, workspaceRoot);\n return parsePipAuditOutput(result);\n}\n\nfunction parsePipAuditOutput(result: AuditCommandResult): NativeAuditResult {\n const advisories: NativeAdvisory[] = [];\n let detailLines: string[] = [];\n\n if (result.status === 0) {\n try {\n const json = JSON.parse(result.stdout || '[]') as Array<Record<string, unknown>>;\n for (const entry of json) {\n advisories.push({\n id: (entry.id as string) ?? (entry.vulnerability_id as string) ?? 'unknown',\n packageName: (entry.name as string) ?? '',\n severity: npmSeverity((entry.severity as string) ?? 'info'),\n summary: (entry.description as string) ?? (entry.vulnerability_id as string) ?? 'No summary',\n fixVersion: (entry.fix_version as string) ?? undefined,\n url: (entry.advisory_url as string) ?? undefined,\n aliases: (entry.aliases as string[]) ?? [],\n });\n }\n } catch {\n detailLines = ['Failed to parse pip-audit JSON output'];\n }\n } else {\n detailLines = [`pip-audit exited with code ${result.status}: ${result.stderr}`];\n }\n\n const evidence: Evidence = {\n kind: 'audit',\n source: 'pip-audit --format json',\n retrievedAt: new Date().toISOString(),\n detail: detailLines.join('\\n') || `Found ${advisories.length} advisories`,\n };\n\n return { advisories, evidence };\n}\n\n// \u2500\u2500 cargo-audit \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Run cargo-audit in the given workspace directory.\n */\nexport function runCargoAudit(workspaceRoot: string): NativeAuditResult {\n const result = runAuditCommand('cargo', ['audit', '--json'], workspaceRoot);\n return parseCargoAuditOutput(result);\n}\n\nfunction parseCargoAuditOutput(result: AuditCommandResult): NativeAuditResult {\n const advisories: NativeAdvisory[] = [];\n let detailLines: string[] = [];\n\n if (result.status === 0) {\n try {\n const json = JSON.parse(result.stdout || '{}');\n const vulnerabilities = json.vulnerabilities as Record<string, unknown> | undefined;\n const advisoriesList = vulnerabilities?.list as Array<Record<string, unknown>> | undefined;\n\n if (advisoriesList) {\n for (const adv of advisoriesList) {\n const advisory = adv.advisory as Record<string, unknown> | undefined;\n const pkg = adv.package as Record<string, unknown> | undefined;\n if (!advisory) continue;\n\n advisories.push({\n id: (advisory.id as string) ?? 'unknown',\n packageName: (pkg?.name as string) ?? '',\n severity: cargoSeverity((advisory.cvss as string ?? '').split('/')?.[0] ?? 'info'),\n summary: (advisory.title as string) ?? (advisory.description as string) ?? 'No summary',\n fixVersion: (advisory.patched_versions as string) ?? undefined,\n url: (advisory.url as string) ?? undefined,\n aliases: (advisory.aliases as string[]) ?? [],\n });\n }\n }\n } catch {\n detailLines = ['Failed to parse cargo audit JSON output'];\n }\n } else {\n detailLines = [`cargo audit exited with code ${result.status}: ${result.stderr}`];\n }\n\n const evidence: Evidence = {\n kind: 'audit',\n source: 'cargo audit --json',\n retrievedAt: new Date().toISOString(),\n detail: detailLines.join('\\n') || `Found ${advisories.length} advisories`,\n };\n\n return { advisories, evidence };\n}\n\n// \u2500\u2500 govulncheck \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Run govulncheck in the given workspace directory.\n * Output is JSON with vulnerabilities in the format:\n * { vulns: [{ id, details, osv, ... }] }\n */\nexport function runGoVulncheck(workspaceRoot: string): NativeAuditResult {\n const result = runAuditCommand('govulncheck', ['-json'], workspaceRoot);\n const advisories: NativeAdvisory[] = [];\n let detailLines: string[] = [];\n\n if (result.status === 0 || result.status === 3) {\n // govulncheck exits 3 when vulnerabilities found\n try {\n const json = JSON.parse(result.stdout || '{}');\n const vulns = json.vulns as Array<Record<string, unknown>> | undefined;\n\n if (vulns) {\n for (const v of vulns) {\n const osv = v.osv as string | undefined;\n\n advisories.push({\n id: (v.id as string) ?? osv ?? 'unknown',\n packageName: (v.package as string) ?? (v.module_path as string) ?? '',\n severity: 'high', // govulncheck doesn't provide CVSS \u2014 default to high\n summary: (v.details as string) ?? (v.description as string) ?? osv ?? 'No summary',\n fixVersion: (v.fixed_version as string) ?? undefined,\n url: (v.url as string) ?? undefined,\n aliases: osv ? [osv] : [],\n });\n }\n }\n } catch {\n detailLines = ['Failed to parse govulncheck JSON output'];\n }\n } else if (result.status === 1) {\n detailLines = ['govulncheck: no vulnerabilities found'];\n } else {\n detailLines = [`govulncheck exited with code ${result.status}: ${result.stderr}`];\n }\n\n const evidence: Evidence = {\n kind: 'audit',\n source: 'govulncheck -json',\n retrievedAt: new Date().toISOString(),\n detail: detailLines.join('\\n') || `Found ${advisories.length} advisories`,\n };\n\n return { advisories, evidence };\n}\n\n// \u2500\u2500 composer audit \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Run composer audit in the given workspace directory.\n */\nexport function runComposerAudit(workspaceRoot: string): NativeAuditResult {\n const result = runAuditCommand('composer', ['audit', '--format=json'], workspaceRoot);\n const advisories: NativeAdvisory[] = [];\n let detailLines: string[] = [];\n\n if (result.status === 0) {\n try {\n const json = JSON.parse(result.stdout || '{}');\n const advisoriesJson = json.advisories as Record<string, Array<Record<string, unknown>>> | undefined;\n\n if (advisoriesJson) {\n for (const [pkg, advs] of Object.entries(advisoriesJson)) {\n for (const adv of advs) {\n advisories.push({\n id: (adv.cve as string) ?? (adv.reference as string) ?? `composer-${pkg}`,\n packageName: pkg,\n severity: npmSeverity((adv.severity as string) ?? 'medium'),\n summary: (adv.title as string) ?? (adv.description as string) ?? 'No summary',\n fixVersion: adv.link ? (adv.link as string).split('/').pop() : undefined,\n url: (adv.link as string) ?? undefined,\n aliases: (adv.cve as string) ? [(adv.cve as string)] : [],\n });\n }\n }\n }\n } catch {\n detailLines = ['Failed to parse composer audit JSON output'];\n }\n } else {\n detailLines = [`composer audit exited with code ${result.status}: ${result.stderr}`];\n }\n\n const evidence: Evidence = {\n kind: 'audit',\n source: 'composer audit --format=json',\n retrievedAt: new Date().toISOString(),\n detail: detailLines.join('\\n') || `Found ${advisories.length} advisories`,\n };\n\n return { advisories, evidence };\n}\n\n// \u2500\u2500 dotnet package audit \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Run `dotnet package audit` in the given workspace directory.\n * .NET 8+ supports `dotnet package audit --format json`.\n */\nexport function runDotnetAudit(workspaceRoot: string): NativeAuditResult {\n // Try new --format first, fall back to default output\n const result = runAuditCommand('dotnet', ['package', 'audit', '--format', 'json'], workspaceRoot);\n const advisories: NativeAdvisory[] = [];\n let detailLines: string[] = [];\n\n if (result.status === 0) {\n try {\n const json = JSON.parse(result.stdout || '{}');\n const vulnerabilities = json.vulnerabilities as Record<string, unknown> | undefined;\n const packages = json.packages as Record<string, Array<Record<string, unknown>>> | undefined;\n\n // Two possible shapes (different .NET SDK versions)\n if (vulnerabilities) {\n // Flat shape: { vulnerabilities: [{ packageName, severity, advisoryUrl, ... }] }\n const vulnList = Array.isArray(vulnerabilities)\n ? vulnerabilities as Array<Record<string, unknown>>\n : [];\n for (const v of vulnList) {\n advisories.push({\n id: (v.advisoryId as string) ?? (v.id as string) ?? 'unknown',\n packageName: (v.packageName as string) ?? '',\n severity: npmSeverity((v.severity as string) ?? 'info'),\n summary: (v.description as string) ?? (v.title as string) ?? 'No summary',\n fixVersion: (v.fixedVersion as string) ?? (v.patchedVersion as string) ?? undefined,\n url: (v.advisoryUrl as string) ?? (v.url as string) ?? undefined,\n aliases: (v.aliases as string[]) ?? [],\n });\n }\n } else if (packages) {\n // Nested shape: { packages: { \"pkgName\": [{ severity, advisoryUrl, ... }] } }\n for (const [pkg, entries] of Object.entries(packages)) {\n for (const entry of entries) {\n advisories.push({\n id: (entry.advisoryId as string) ?? (entry.id as string) ?? 'unknown',\n packageName: pkg,\n severity: npmSeverity((entry.severity as string) ?? 'info'),\n summary: (entry.description as string) ?? (entry.title as string) ?? 'No summary',\n fixVersion: (entry.fixedVersion as string) ?? (entry.patchedVersion as string) ?? undefined,\n url: (entry.advisoryUrl as string) ?? (entry.url as string) ?? undefined,\n aliases: (entry.aliases as string[]) ?? [],\n });\n }\n }\n }\n } catch {\n detailLines = ['Failed to parse dotnet package audit JSON output'];\n }\n } else {\n detailLines = [`dotnet package audit exited with code ${result.status}: ${result.stderr}`];\n }\n\n const evidence: Evidence = {\n kind: 'audit',\n source: 'dotnet package audit --format json',\n retrievedAt: new Date().toISOString(),\n detail: detailLines.join('\\n') || `Found ${advisories.length} advisories`,\n };\n\n return { advisories, evidence };\n}\n\n// \u2500\u2500 Common command runner \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ninterface AuditCommandResult {\n readonly status: number | null;\n readonly stdout: string;\n readonly stderr: string;\n}\n\nfunction runAuditCommand(\n command: string,\n args: readonly string[],\n cwd: string,\n): AuditCommandResult {\n try {\n const options: SpawnSyncOptions = {\n cwd,\n encoding: 'utf-8' as const,\n timeout: 60000,\n maxBuffer: 10 * 1024 * 1024, // 10MB\n windowsHide: true,\n };\n\n const result = spawnSync(command, args as string[], options);\n\n return {\n status: result.status,\n stdout: result.stdout?.toString() ?? '',\n stderr: result.stderr?.toString() ?? '',\n };\n } catch (err) {\n return {\n status: null,\n stdout: '',\n stderr: err instanceof Error ? err.message : String(err),\n };\n }\n}\n\n// \u2500\u2500 Ecosystem dispatch \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Run the native audit command for the given ecosystem.\n * Returns advisories found by the native tool.\n */\nexport function runNativeAudit(\n ecosystem: EcosystemId,\n workspaceRoot: string,\n): NativeAuditResult {\n switch (ecosystem) {\n case 'npm':\n return runNpmAudit(workspaceRoot);\n case 'python':\n return runPipAudit(workspaceRoot);\n case 'rust':\n return runCargoAudit(workspaceRoot);\n case 'go':\n return runGoVulncheck(workspaceRoot);\n case 'php':\n return runComposerAudit(workspaceRoot);\n case 'dotnet':\n return runDotnetAudit(workspaceRoot);\n // dart/pub doesn't have a standard audit command \u2014 use OSV instead\n default:\n return {\n advisories: [],\n evidence: {\n kind: 'audit',\n source: `native-audit:${ecosystem}`,\n retrievedAt: new Date().toISOString(),\n detail: `No native audit tool for ecosystem: ${ecosystem}`,\n },\n };\n }\n}\n\n// \u2500\u2500 Availability check \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Check if the native audit tool is available for the given ecosystem.\n */\nexport function isNativeAuditAvailable(ecosystem: EcosystemId): boolean {\n const result = runAuditCommand(\n ecosystem === 'npm' ? 'npm' :\n ecosystem === 'python' ? 'pip-audit' :\n ecosystem === 'rust' ? 'cargo' :\n ecosystem === 'go' ? 'govulncheck' :\n ecosystem === 'php' ? 'composer' :\n ecosystem === 'dotnet' ? 'dotnet' : '',\n ['--version'],\n process.cwd(),\n );\n\n return result.status === 0;\n}\n", "/**\n * TechStack \u2014 Status classification policy.\n *\n * Classifies a DependencyObservation's status based on registry metadata,\n * advisory data, and version comparison rules.\n *\n * Key contracts (per SDD R8/R9):\n * - Private/unresolved (404/401) \u2192 `private_or_unresolved` (never `dead` or `deprecated`)\n * - Offline/failed lookup \u2192 `unknown` (never `current`)\n * - Registry says deprecated \u2192 `deprecated`\n * - Registry says yanked \u2192 `yanked`\n * - Advisory found \u2192 `vulnerable`\n *\n * @see docs/specs/techstack-sdd.md \u00A77, R8, R9\n */\n\nimport type { DependencyObservation, DependencyStatus, Evidence } from '../types.js';\n\n// \u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/** Registry metadata used for status classification. */\nexport interface RegistryStatusData {\n readonly latestStable?: string | undefined;\n readonly deprecated?: boolean | undefined;\n readonly yanked?: boolean | undefined;\n /** Whether the registry lookup resulted in a 404/401 (private/unresolved). */\n readonly privateOrUnresolved?: boolean | undefined;\n /** Whether the registry lookup failed due to network error / timeout / offline. */\n readonly lookupFailed?: boolean | undefined;\n /** The evidence from the registry lookup. */\n readonly evidence?: readonly Evidence[] | undefined;\n}\n\n/** Advisory data used for status classification. */\nexport interface AdvisoryStatusData {\n /** Whether any advisory was found for this dependency. */\n readonly hasAdvisory: boolean;\n /** The evidence from the advisory lookup. */\n readonly evidence?: readonly Evidence[] | undefined;\n}\n\n// \u2500\u2500 Version comparison helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Check if a version string looks like a valid semver.\n */\nfunction isValidSemver(version: string): boolean {\n return /^\\d+\\.\\d+\\.\\d+/.test(version);\n}\n\n/**\n * Compare two semver-like version strings.\n * Returns -1 if a < b, 0 if a == b, 1 if a > b.\n * Handles prerelease tags per semver: `1.0.0-alpha < 1.0.0`.\n */\nexport function compareVersions(a: string, b: string): number {\n // Split off prerelease segment(s) from each version\n // Match: numeric segment | alphanumeric prerelease segment\n const aMatch = a.match(/^([^-]+)(?:-(.+))?$/);\n const bMatch = b.match(/^([^-]+)(?:-(.+))?$/);\n const aBase = aMatch?.[1] ?? a;\n const aPre = aMatch?.[2];\n const bBase = bMatch?.[1] ?? b;\n const bPre = bMatch?.[2];\n\n // Compare base segments numerically\n const aBaseParts = aBase.split('.').map(Number);\n const bBaseParts = bBase.split('.').map(Number);\n\n for (let i = 0; i < Math.max(aBaseParts.length, bBaseParts.length); i++) {\n const aNum = aBaseParts[i] ?? 0;\n const bNum = bBaseParts[i] ?? 0;\n if (aNum > bNum) return 1;\n if (aNum < bNum) return -1;\n }\n\n // Base parts equal \u2192 prerelease handling\n if (aPre === bPre) return 0;\n if (aPre === undefined) return 1; // no prerelease > has prerelease\n if (bPre === undefined) return -1; // has prerelease < no prerelease\n // Both have prerelease \u2014 compare dot-separated identifiers\n // Numeric identifiers compare numerically; non-numeric compare lexically.\n const aPreParts = aPre.split('.');\n const bPreParts = bPre.split('.');\n for (let i = 0; i < Math.max(aPreParts.length, bPreParts.length); i++) {\n const aId = aPreParts[i] ?? '';\n const bId = bPreParts[i] ?? '';\n if (aId === bId) continue;\n const aNum = Number(aId);\n const bNum = Number(bId);\n if (!Number.isNaN(aNum) && !Number.isNaN(bNum) && Number.isFinite(aNum) && Number.isFinite(bNum)) {\n if (aNum > bNum) return 1;\n if (aNum < bNum) return -1;\n } else {\n if (aId > bId) return 1;\n if (aId < bId) return -1;\n }\n return aId > bId ? 1 : -1;\n }\n return 0;\n}\n\n/**\n * Check if a constraint string is a simple semver range (^, ~, >=, exact).\n * Returns the expression boundary characters for comparison purposes.\n */\nfunction isSimpleConstraint(constraint: string): boolean {\n return (\n constraint.startsWith('^') ||\n constraint.startsWith('~') ||\n constraint.startsWith('>=') ||\n constraint.startsWith('>') ||\n isValidSemver(constraint)\n );\n}\n\n/**\n * Check if upgrading from `locked` to `latestStable` would be breaking\n * based on the constraint.\n *\n * Simple heuristic:\n * - ^ means compatible (major must match)\n * - ~ means approximately (minor must match)\n * - >= means compatible if major matches\n * - an exact pin is a manifest constraint, not a compatibility one \u2014 still\n * only breaking on a major bump\n */\nfunction isBreakingUpgrade(locked: string, latestStable: string, constraint?: string): boolean {\n const constraintNorm = constraint?.trim() ?? '';\n\n // Get major versions\n const lockedMajor = locked.split('.')[0];\n const latestMajor = latestStable.split('.')[0];\n\n if (!lockedMajor || !latestMajor) return true;\n\n // `^` \u2014 compatible, only breaking if major changes\n if (constraintNorm.startsWith('^')) {\n return lockedMajor !== latestMajor;\n }\n\n // `~` \u2014 approximately equivalent, breaking if minor changes (and we have it)\n if (constraintNorm.startsWith('~')) {\n const lockedMinor = locked.split('.')[1];\n const latestMinor = latestStable.split('.')[1];\n if (lockedMinor && latestMinor && lockedMajor !== latestMajor) return true;\n if (lockedMinor && latestMinor && lockedMinor !== latestMinor) return true;\n return false;\n }\n\n // `>=` \u2014 compatible if major matches\n if (constraintNorm.startsWith('>=') || constraintNorm.startsWith('>')) {\n return lockedMajor !== latestMajor;\n }\n\n // Exact pin (`\"biome\": \"2.5.3\"`). The pin means \"don't move without a\n // decision\", but that is a manifest question, not a compatibility one \u2014\n // breaking is still a major bump. Treating every pinned patch release as\n // breaking flags most of a pin-heavy repo as a major upgrade.\n if (isValidSemver(constraintNorm)) {\n return lockedMajor !== latestMajor;\n }\n\n // Unknown constraint type \u2014 assume breaking\n return lockedMajor !== latestMajor;\n}\n\n// \u2500\u2500 Main classification function \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Classify a dependency's status based on its current data plus optional\n * registry metadata and advisory information.\n *\n * @param dep - The dependency observation from the inventory pass.\n * @param registryData - Optional registry metadata (from lookupRegistry).\n * @param advisoryData - Optional advisory data (from OSV or native audit).\n * @returns The classified DependencyStatus.\n */\nexport function classifyStatus(\n dep: Pick<DependencyObservation, 'name' | 'sourceType' | 'status' | 'locked' | 'requested'>,\n registryData?: RegistryStatusData,\n advisoryData?: AdvisoryStatusData,\n): DependencyStatus {\n // \u2500\u2500 Source-type based classifications \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n // Local path deps\n if (dep.sourceType === 'path') {\n return 'local_path';\n }\n\n // Git deps\n if (dep.sourceType === 'git') {\n return 'git_dependency';\n }\n\n // \u2500\u2500 Private/unresolved (404/401) \u2014 never dead, never deprecated \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n if (registryData?.privateOrUnresolved) {\n return 'private_or_unresolved';\n }\n\n // \u2500\u2500 Lookup failed \u2014 never current, never up-to-date \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n if (registryData?.lookupFailed) {\n return 'unknown';\n }\n\n // \u2500\u2500 Deprecated / yanked \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n if (registryData?.deprecated) {\n return 'deprecated';\n }\n\n if (registryData?.yanked) {\n return 'yanked';\n }\n\n // \u2500\u2500 Vulnerable \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n if (advisoryData?.hasAdvisory) {\n return 'vulnerable';\n }\n\n // \u2500\u2500 Version comparison \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n const locked = dep.locked;\n const latestStable = registryData?.latestStable;\n\n if (locked && latestStable) {\n if (locked === latestStable) {\n return 'current';\n }\n\n try {\n const cmp = compareVersions(locked, latestStable);\n if (cmp < 0) {\n // locked < latestStable\n const constraint = dep.requested;\n if (constraint && isSimpleConstraint(constraint)) {\n const breaking = isBreakingUpgrade(locked, latestStable, constraint);\n return breaking ? 'update_available_breaking' : 'update_available_safe';\n }\n // No constraint or complex constraint \u2014 conservative: assume safe\n return 'update_available_safe';\n }\n // locked > latestStable \u2014 this shouldn't normally happen for registry deps\n // but handle gracefully as current\n return 'current';\n } catch {\n // Version comparison failed \u2014 fall through to registry-based status\n }\n }\n\n // \u2500\u2500 Holdover status from inventory pass \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n // If the adapter already classified this (e.g. local_path, git_dependency),\n // respect that classification\n if (dep.status === 'local_path' || dep.status === 'git_dependency') {\n return dep.status;\n }\n\n // \u2500\u2500 Default \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n return dep.status ?? 'current';\n}\n\n/**\n * Create a registry status data object indicating a private/unresolved package.\n */\nexport function privateOrUnresolvedStatus(\n source: string,\n detail?: string,\n): RegistryStatusData {\n return {\n privateOrUnresolved: true,\n evidence: [\n {\n kind: 'registry',\n source,\n retrievedAt: new Date().toISOString(),\n detail: detail ?? 'Package returned 404/401 \u2014 private or unresolved',\n },\n ],\n };\n}\n\n/**\n * Create a registry status data object indicating a failed/offline lookup.\n */\nexport function failedLookupStatus(\n source: string,\n error?: string,\n): RegistryStatusData {\n return {\n lookupFailed: true,\n evidence: [\n {\n kind: 'registry',\n source,\n retrievedAt: new Date().toISOString(),\n detail: error ?? 'Registry lookup failed \u2014 network error or timeout',\n },\n ],\n };\n}\n", "/**\n * TechStack \u2014 Public service API.\n *\n * Wires together inventory, online enrichment (registry + OSV + native audit),\n * status classification, and persistence into a single async job flow.\n *\n * @see docs/specs/techstack-sdd.md \u00A74.2\n */\n\nimport { randomUUID } from 'node:crypto';\nimport type {\n Coverage,\n DependencyObservation,\n EcosystemId,\n Evidence,\n Finding,\n Snapshot,\n TechStackJob,\n TechStackJobProgress,\n TechStackJobStatus,\n Workspace,\n} from './types.js';\nimport type { EcosystemAdapter } from './adapters/interface.js';\nimport { lookupRegistry } from './registry/client.js';\nimport type { RegistryEntry } from './registry/client.js';\nimport { queryOsvBatch } from './advisory/osv.js';\nimport { classifyStatus } from './policy/status.js';\nimport type { RegistryStatusData, AdvisoryStatusData } from './policy/status.js';\nimport type { TechStackStore } from './store/sqlite.js';\nimport { discoverWorkspaces } from './discovery/workspace.js';\nimport { triageCandidates } from './research/triage.js';\nimport type { TechStackResearcher } from './research/types.js';\n\n// \u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface EnrichOptions {\n /** Skip network calls; use only cached/offline data. */\n readonly online?: boolean | undefined;\n /** Abort signal for cancellation. */\n readonly signal?: AbortSignal | undefined;\n /** Force re-fetch registry data (ignore cache). */\n readonly forceRegistryRefresh?: boolean | undefined;\n}\n\nexport interface AnalyzeOptions {\n /** Where to start the analysis. */\n readonly targetRoot: string;\n /** Session ID for job tracking. */\n readonly sessionId?: string | undefined;\n /** Requesting entity. */\n readonly requestedBy?: string | undefined;\n /** Enable online enrichment (registry + advisory). */\n readonly online?: boolean | undefined;\n /** Auto-deliver report when complete. */\n readonly autoDeliver?: boolean | undefined;\n /** Optional caller-assigned id so HTTP/WS clients can track the job immediately. */\n readonly jobId?: string | undefined;\n /** Abort signal used by cancel endpoints. */\n readonly signal?: AbortSignal | undefined;\n /** Progress callback for WebSocket projection. */\n readonly onProgress?: ((phase: string, completed: number, total: number) => void) | undefined;\n /**\n * LLM interpretation stage. Omit it and `analyze()` stays a purely\n * deterministic tool \u2014 research is strictly additive enrichment.\n *\n * @see docs/specs/techstack-sdd.md \u00A731\n */\n readonly researcher?: TechStackResearcher | undefined;\n /** Cap on packages sent to research. Defaults to `DEFAULT_TRIAGE_LIMIT`. */\n readonly researchLimit?: number | undefined;\n}\n\n// \u2500\u2500 Adapter registry \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nimport { npmAdapter } from './adapters/npm.js';\nimport { pythonAdapter } from './adapters/python.js';\nimport { rustAdapter } from './adapters/rust.js';\nimport { goAdapter } from './adapters/go.js';\nimport { dotNetAdapter } from './adapters/dotnet.js';\nimport { phpAdapter } from './adapters/php.js';\nimport { dartAdapter } from './adapters/dart.js';\nimport { mavenAdapter } from './adapters/maven.js';\nimport { rubyAdapter } from './adapters/ruby.js';\nimport { elixirAdapter } from './adapters/elixir.js';\nimport { cppAdapter } from './adapters/cpp.js';\n\nfunction getAdapter(ecosystem: EcosystemId): EcosystemAdapter | undefined {\n switch (ecosystem) {\n case 'npm': return npmAdapter;\n case 'python': return pythonAdapter;\n case 'rust': return rustAdapter;\n case 'go': return goAdapter;\n case 'dotnet': return dotNetAdapter;\n case 'php': return phpAdapter;\n case 'dart': return dartAdapter;\n // Tier B \u2014 partial support\n case 'maven': return mavenAdapter;\n case 'gradle': return mavenAdapter; // reuse Maven adapter (same manifest family)\n case 'ruby': return rubyAdapter;\n case 'swift': return undefined; // no adapter yet\n case 'elixir': return elixirAdapter;\n // Tier C \u2014 best-effort\n case 'cpp': return cppAdapter;\n default: return undefined;\n }\n}\n\n// \u2500\u2500 Version constant \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst ADAPTER_VERSION = '0.1.0';\n\n// \u2500\u2500 TechStack Engine \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport class TechStackEngine {\n private store: TechStackStore;\n\n constructor(store: TechStackStore) {\n this.store = store;\n }\n\n // \u2500\u2500 Inventory \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n /**\n * Run an offline inventory: discover workspaces and parse dependencies\n * from manifests and lockfiles. No network calls.\n *\n * Returns a Snapshot with workspace and dependency data but no\n * registry/advisory enrichment.\n */\n async inventory(\n projectId: string,\n targetRoot: string,\n _jobId?: string,\n onProgress?: (phase: string, completed: number, total: number) => void,\n ): Promise<Snapshot> {\n const snapshotId = randomUUID();\n\n // Phase 1: Discover workspaces\n onProgress?.('discovering', 0, 1);\n const rawWorkspaces = await discoverWorkspaces(targetRoot);\n\n // Map into our Workspace type\n const workspaces: Workspace[] = rawWorkspaces.map((w) => ({\n id: w.id,\n relativeRoot: w.relativeRoot,\n ecosystem: w.ecosystem,\n packageManager: w.packageManager,\n manifests: [...w.manifests],\n lockfiles: [...w.lockfiles],\n confidence: w.confidence,\n coverage: w.coverage as Coverage,\n }));\n\n // Phase 2: Inventory per workspace\n onProgress?.('inventorying', 0, workspaces.length);\n const allDependencies: DependencyObservation[] = [];\n let totalCoverage: Coverage = 'full';\n\n for (let i = 0; i < workspaces.length; i++) {\n const ws = workspaces[i]!;\n const adapter = getAdapter(ws.ecosystem);\n let deps: readonly DependencyObservation[] = [];\n\n if (adapter) {\n try {\n // `targetRoot` is the absolute base every adapter resolves\n // `ws.relativeRoot` against. Without it they fall back to\n // `process.cwd()` and silently inventory nothing.\n deps = await adapter.inventory(ws, { projectRoot: targetRoot });\n } catch {\n deps = [];\n }\n }\n\n // Track coverage\n if (ws.coverage === 'unsupported') {\n totalCoverage = 'partial';\n }\n\n allDependencies.push(...deps);\n onProgress?.('inventorying', i + 1, workspaces.length);\n }\n\n // Compute fingerprint\n const fingerprint = computeFingerprint(allDependencies);\n\n const snapshot: Snapshot = {\n id: snapshotId,\n projectId,\n targetRoot,\n fingerprint,\n createdAt: new Date().toISOString(),\n workspaces,\n dependencies: allDependencies,\n findings: [],\n coverage: totalCoverage,\n adapterVersion: ADAPTER_VERSION,\n };\n\n // Persist\n this.store.saveSnapshot(snapshot);\n\n return snapshot;\n }\n\n // \u2500\u2500 Enrichment \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n /**\n * Enrich a snapshot with registry metadata and advisory data.\n *\n * This is the online pass: fetches latest versions, licenses, deprecation\n * status from registries, and queries OSV for advisories.\n *\n * Key contracts:\n * - 404/401 \u2192 status `private_or_unresolved` (never `dead` or `deprecated`)\n * - Network failure \u2192 status `unknown` with evidence detail (never `current`)\n */\n async enrich(\n snapshot: Snapshot,\n options: EnrichOptions = {},\n ): Promise<Snapshot> {\n const isOnline = options.online !== false;\n if (!isOnline || options.signal?.aborted) {\n return snapshot;\n }\n\n // Group dependencies by ecosystem for batch lookups\n const byEcosystem = new Map<EcosystemId, DependencyObservation[]>();\n for (const dep of snapshot.dependencies) {\n // Skip local/git deps \u2014 they have no registry metadata\n if (dep.sourceType === 'path' || dep.sourceType === 'git') continue;\n if (!dep.purl) continue;\n\n const list = byEcosystem.get(dep.ecosystem);\n if (list) {\n list.push(dep);\n } else {\n byEcosystem.set(dep.ecosystem, [dep]);\n }\n }\n\n // Enrich each ecosystem\n const enrichedDeps = new Map<string, DependencyObservation>();\n const allFindings: Finding[] = [...snapshot.findings];\n\n for (const [ecosystem, deps] of byEcosystem) {\n // \u2500\u2500 Registry lookup \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n const names = [...new Set(deps.map((d) => d.name))];\n\n for (const name of names) {\n const lookupOpts: { signal?: AbortSignal; force?: boolean } = {};\n if (options.signal) lookupOpts.signal = options.signal;\n if (options.forceRegistryRefresh) lookupOpts.force = true;\n\n let registryEntry: RegistryEntry | undefined;\n let registryStatus: RegistryStatusData | undefined;\n\n try {\n registryEntry = await lookupRegistry(ecosystem, name, lookupOpts);\n\n if (registryEntry) {\n // Successful lookup\n registryStatus = {\n latestStable: registryEntry.latestStable,\n deprecated: registryEntry.deprecated,\n yanked: registryEntry.yanked,\n evidence: [\n {\n kind: 'registry',\n source: registryEntry.source,\n retrievedAt: registryEntry.retrievedAt,\n detail: `latestStable: ${registryEntry.latestStable ?? 'N/A'}, license: ${registryEntry.license ?? 'N/A'}`,\n },\n ],\n };\n } else {\n // 401/403/404 \u2014 private or unresolved\n registryStatus = {\n privateOrUnresolved: true,\n evidence: [\n {\n kind: 'registry',\n source: `${ecosystem} registry for ${name}`,\n retrievedAt: new Date().toISOString(),\n detail: 'Package returned 404/401 \u2014 private or unresolved',\n },\n ],\n };\n }\n } catch (err) {\n // Network error / timeout / offline\n registryStatus = {\n lookupFailed: true,\n evidence: [\n {\n kind: 'registry',\n source: `${ecosystem} registry for ${name}`,\n retrievedAt: new Date().toISOString(),\n detail: err instanceof Error ? err.message : 'Registry lookup failed',\n },\n ],\n };\n }\n\n // \u2500\u2500 OSV advisory \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n let advisoryStatus: AdvisoryStatusData | undefined;\n\n try {\n // Build PURL for OSV query\n const depList = deps.filter((d) => d.name === name);\n const purls = depList\n .map((d) => d.purl)\n .filter((p): p is string => !!p);\n\n if (purls.length > 0) {\n const osvResult = await queryOsvBatch(purls, { signal: options.signal });\n\n const hasAdvisory = [...osvResult.advisories.values()].some(\n (advisories) => advisories.length > 0,\n );\n\n if (hasAdvisory) {\n advisoryStatus = {\n hasAdvisory: true,\n };\n }\n }\n } catch {\n // OSV failure \u2014 don't block enrichment, just skip advisory\n }\n\n // \u2500\u2500 Apply status classification to matching deps \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n for (const dep of deps) {\n if (dep.name !== name) continue;\n\n const newStatus = classifyStatus(dep, registryStatus, advisoryStatus);\n const newEvidence: Evidence[] = [\n ...dep.evidence,\n ...(registryStatus?.evidence ?? []),\n ...(advisoryStatus?.evidence ?? []),\n ];\n\n enrichedDeps.set(dep.id, {\n ...dep,\n latestStable: registryEntry?.latestStable ?? dep.latestStable,\n license: registryEntry?.license ?? dep.license,\n deprecated: registryEntry?.deprecated ?? dep.deprecated,\n yanked: registryEntry?.yanked ?? dep.yanked,\n status: newStatus,\n evidence: newEvidence,\n });\n\n // Generate findings for non-current statuses\n if (newStatus !== 'current' && newStatus !== 'local_path' && newStatus !== 'git_dependency') {\n allFindings.push(\n createFindingForStatus(dep.id, newStatus, registryEntry?.license),\n );\n }\n }\n }\n }\n\n // Build enriched dependency list, preserving un-enriched deps\n const finalDependencies = snapshot.dependencies.map(\n (dep) => enrichedDeps.get(dep.id) ?? dep,\n );\n\n return {\n ...snapshot,\n dependencies: finalDependencies,\n findings: allFindings,\n };\n }\n\n // \u2500\u2500 Research \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n /**\n * Interpret an enriched snapshot with the LLM: triage the problem cases,\n * research them, and append the resulting findings.\n *\n * Additive by construction. Two invariants hold here, and they are the whole\n * reason this is a separate pass rather than part of `enrich()`:\n *\n * 1. **`snapshot.dependencies` is returned untouched.** Version facts come\n * only from registry evidence \u2014 the LLM cannot fabricate a `latestStable`\n * because `Finding` has nowhere to put one (SDD \u00A7472).\n * 2. **Failure is not fatal.** No researcher, no candidates, a provider\n * outage, a dry web search \u2014 every one of them returns the input snapshot\n * unchanged. A deterministic report is the floor, never a casualty of the\n * optional stage above it.\n *\n * @see docs/specs/techstack-sdd.md \u00A731, \u00A7472\n */\n async research(\n snapshot: Snapshot,\n options: {\n researcher?: TechStackResearcher | undefined;\n researchLimit?: number | undefined;\n signal?: AbortSignal | undefined;\n /** Only ever emits the two research phases \u2014 narrow so callers can feed\n * `updateJob` without casting. */\n onProgress?:\n | ((phase: 'researching' | 'synthesizing', completed: number, total: number) => void)\n | undefined;\n } = {},\n ): Promise<Snapshot> {\n if (!options.researcher || options.signal?.aborted) return snapshot;\n\n const candidates = triageCandidates(snapshot.dependencies, {\n limit: options.researchLimit,\n });\n if (candidates.length === 0) return snapshot;\n\n options.onProgress?.('researching', 0, candidates.length);\n\n let findings: readonly Finding[];\n try {\n findings = await options.researcher.research(candidates, {\n signal: options.signal,\n onProgress: (completed, total) => {\n // Cluster-level progress, reported against the phase the UI shows.\n options.onProgress?.('researching', completed, total);\n },\n });\n } catch {\n // The deterministic snapshot stands on its own.\n return snapshot;\n }\n\n options.onProgress?.('synthesizing', 1, 1);\n if (findings.length === 0) return snapshot;\n\n // Deterministic findings first \u2014 facts outrank interpretations in the\n // order the report and the UI render them.\n return { ...snapshot, findings: [...snapshot.findings, ...findings] };\n }\n\n // \u2500\u2500 Analyze \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n /**\n * Run a full analysis: inventory + enrich + research + persist.\n * This is the main entry point for the analyze job flow.\n */\n async analyze(\n projectId: string,\n options: AnalyzeOptions,\n ): Promise<{ snapshot: Snapshot; job: TechStackJob }> {\n const jobId = options.jobId ?? randomUUID();\n const requestedBy = options.requestedBy ?? 'system';\n\n // Create job\n const job: TechStackJob = {\n id: jobId,\n projectId,\n targetRoot: options.targetRoot,\n kind: 'analyze',\n status: 'queued',\n fingerprint: '',\n requestedBy,\n sessionId: options.sessionId,\n createdAt: new Date().toISOString(),\n progress: { phase: 'queued', completed: 0, total: 0 },\n };\n this.store.saveJob(job);\n\n const updateJob = (status: TechStackJobStatus, progress?: TechStackJobProgress) => {\n this.store.updateJobStatus(jobId, status, progress);\n if (progress) options.onProgress?.(progress.phase, progress.completed, progress.total);\n };\n const throwIfAborted = (): void => {\n if (options.signal?.aborted) throw new DOMException('TechStack job cancelled', 'AbortError');\n };\n\n try {\n throwIfAborted();\n // Phase 1: Inventory (offline)\n updateJob('discovering', { phase: 'discovering', completed: 0, total: 1 });\n const snapshot = await this.inventory(\n projectId,\n options.targetRoot,\n jobId,\n (phase, completed, total) => {\n throwIfAborted();\n updateJob(phase as TechStackJobStatus, { phase, completed, total });\n },\n );\n throwIfAborted();\n\n // Phase 2: Enrich (online, if enabled)\n const isOnline = options.online !== false;\n if (isOnline) {\n updateJob('enriching', { phase: 'enriching', completed: 0, total: 1 });\n const enriched = await this.enrich(snapshot, {\n online: true,\n signal: options.signal,\n });\n throwIfAborted();\n\n // Phase 3: Research (LLM interpretation) \u2014 additive and optional.\n const researched = await this.research(enriched, {\n researcher: options.researcher,\n researchLimit: options.researchLimit,\n signal: options.signal,\n onProgress: (phase, completed, total) => {\n updateJob(phase, { phase, completed, total });\n },\n });\n throwIfAborted();\n\n // Persist enriched snapshot\n this.store.saveSnapshot(researched);\n\n updateJob('completed', { phase: 'completed', completed: 1, total: 1 });\n\n return {\n snapshot: researched,\n job: { ...job, status: 'completed', completedAt: new Date().toISOString() },\n };\n }\n\n // Offline mode \u2014 persist inventory-only snapshot\n this.store.saveSnapshot(snapshot);\n updateJob('completed', { phase: 'completed', completed: 1, total: 1 });\n\n return {\n snapshot,\n job: { ...job, status: 'completed', completedAt: new Date().toISOString() },\n };\n } catch (err) {\n if (options.signal?.aborted || (err instanceof DOMException && err.name === 'AbortError')) {\n updateJob('cancelled');\n } else {\n updateJob('failed');\n }\n throw err;\n }\n }\n // \u2500\u2500 Report generation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n /**\n * Generate a human-readable report from a snapshot.\n *\n * @param format 'md' for Markdown, 'json' for raw JSON.\n * @returns The report as a string.\n */\n generateReport(snapshot: Snapshot, format: 'md' | 'json' = 'md'): string {\n if (format === 'json') return JSON.stringify(snapshot, null, 2);\n\n const lines: string[] = [\n '# TechStack Report',\n '',\n `**Generated:** ${snapshot.createdAt}`,\n `**Target:** ${snapshot.targetRoot}`,\n `**Fingerprint:** ${snapshot.fingerprint}`,\n `**Workspaces:** ${snapshot.workspaces.length}`,\n `**Dependencies:** ${snapshot.dependencies.length}`,\n `**Findings:** ${snapshot.findings.length}`,\n `**Coverage:** ${snapshot.coverage}`,\n '',\n ];\n\n // Workspaces\n if (snapshot.workspaces.length > 0) {\n lines.push('## Workspaces', '');\n lines.push('| Workspace | Ecosystem | Coverage | Deps |');\n lines.push('|---|---|---|---|');\n for (const ws of snapshot.workspaces) {\n const depCount = snapshot.dependencies.filter((d) => d.workspaceId === ws.id).length;\n lines.push(`| ${ws.relativeRoot} | ${ws.ecosystem} | ${ws.coverage} | ${depCount} |`);\n }\n lines.push('');\n }\n\n // Findings by severity\n const findings = snapshot.findings as ReadonlyArray<{\n id: string;\n type: string;\n severity: string;\n action: string;\n rationale: string;\n dependencyId: string;\n }>;\n if (findings.length > 0) {\n lines.push('## Findings', '');\n const bySeverity = new Map<string, Array<(typeof findings)[number]>>();\n for (const f of findings) {\n const list = bySeverity.get(f.severity) ?? [];\n list.push(f);\n bySeverity.set(f.severity, list);\n }\n for (const sev of ['critical', 'high', 'medium', 'low', 'info']) {\n const items = bySeverity.get(sev);\n if (!items || items.length === 0) continue;\n lines.push(`### ${sev.charAt(0).toUpperCase() + sev.slice(1)} (${items.length})`, '');\n for (const f of items) {\n const dep = snapshot.dependencies.find((d) => d.id === f.dependencyId);\n lines.push(`- **${dep?.name ?? f.dependencyId}** \u2014 ${f.type} \u2014 ${f.rationale}`);\n }\n lines.push('');\n }\n }\n\n // Dependencies summary\n if (snapshot.dependencies.length > 0) {\n lines.push('## Dependencies', '');\n lines.push('| Name | Ecosystem | Status | Locked | Latest |');\n lines.push('|---|---|---|---|---|');\n for (const dep of snapshot.dependencies) {\n lines.push(\n `| ${dep.name} | ${dep.ecosystem} | ${dep.status} | ${dep.locked ?? '\u2014'} | ${dep.latestStable ?? '\u2014'} |`,\n );\n }\n }\n\n return lines.join('\\n');\n }\n}\n\n// \u2500\u2500 Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction computeFingerprint(dependencies: readonly DependencyObservation[]): string {\n const parts = dependencies\n .map((d) => `${d.name}@${d.locked ?? d.requested ?? 'unknown'}`)\n .sort()\n .join(',');\n let hash = 0;\n for (let i = 0; i < parts.length; i++) {\n const char = parts.charCodeAt(i);\n hash = ((hash << 5) - hash) + char;\n hash |= 0;\n }\n return `ts-${Math.abs(hash).toString(36)}`;\n}\n\nfunction createFindingForStatus(\n dependencyId: string,\n status: string,\n _license?: string,\n): Finding {\n switch (status) {\n case 'vulnerable':\n return {\n id: `finding-${dependencyId}-vuln`,\n dependencyId,\n type: 'vulnerability',\n severity: 'high',\n action: 'upgrade_patch',\n confidence: 1.0,\n rationale: 'Known security advisory found for this package',\n evidence: [],\n };\n case 'deprecated':\n return {\n id: `finding-${dependencyId}-dep`,\n dependencyId,\n type: 'deprecated',\n severity: 'medium',\n action: 'replace',\n confidence: 1.0,\n rationale: 'Package is deprecated in the registry',\n evidence: [],\n };\n case 'yanked':\n return {\n id: `finding-${dependencyId}-yank`,\n dependencyId,\n type: 'deprecated',\n severity: 'high',\n action: 'replace',\n confidence: 1.0,\n rationale: 'Package version has been yanked from the registry',\n evidence: [],\n };\n case 'update_available_safe':\n return {\n id: `finding-${dependencyId}-update`,\n dependencyId,\n type: 'upgrade',\n severity: 'info',\n action: 'upgrade_minor',\n confidence: 1.0,\n rationale: 'A newer compatible version is available',\n evidence: [],\n };\n case 'update_available_breaking':\n return {\n id: `finding-${dependencyId}-major`,\n dependencyId,\n type: 'upgrade',\n severity: 'low',\n action: 'upgrade_major',\n confidence: 1.0,\n rationale: 'A newer version is available that may require breaking changes',\n evidence: [],\n };\n default:\n return {\n id: `finding-${dependencyId}-investigate`,\n dependencyId,\n type: 'investigate',\n severity: 'info',\n action: 'investigate',\n confidence: 0.5,\n rationale: `Package status is \"${status}\" \u2014 may need investigation`,\n evidence: [],\n };\n }\n}\n", "/**\n * TechStack \u2014 Deterministic research triage.\n *\n * Decides *which* dependencies are worth an LLM call. No LLM, no network.\n *\n * This is the stage that makes research affordable: a monorepo resolves\n * thousands of dependencies, the overwhelming majority of which are `current`\n * and need no interpretation at all. Only statuses where the registry has\n * already told us something is wrong \u2014 but not what to do about it \u2014 earn a\n * research slot.\n *\n * @see docs/specs/techstack-sdd.md \u00A731, \u00A7557\n */\n\nimport type { DependencyObservation, DependencyStatus } from '../types.js';\nimport type { ResearchCluster, TriageCandidate } from './types.js';\n\n/** Default cap on researched packages. Keeps a full analyze bounded. */\nexport const DEFAULT_TRIAGE_LIMIT = 40;\n\n/**\n * Which cluster (if any) a status belongs to.\n *\n * Statuses deliberately absent: `current` (nothing to interpret),\n * `update_available_safe` (the registry already answered \u2014 a safe bump needs\n * no essay), `private_or_unresolved` / `unknown` (we have no facts to reason\n * from, so an LLM would only speculate), `local_path` / `git_dependency` /\n * `unsupported` (no registry identity to research).\n */\nconst CLUSTER_BY_STATUS: Partial<Record<DependencyStatus, ResearchCluster>> = {\n vulnerable: 'vulnerability',\n yanked: 'replacement',\n deprecated: 'replacement',\n unmaintained_suspected: 'replacement',\n update_available_breaking: 'breaking_change',\n};\n\n/**\n * Base priority per status \u2014 severity order. Tuned so that a transitive\n * vulnerability still outranks a direct major-version bump: being exploitable\n * matters more than being behind.\n */\nconst PRIORITY_BY_STATUS: Partial<Record<DependencyStatus, number>> = {\n vulnerable: 100,\n yanked: 80,\n deprecated: 60,\n update_available_breaking: 40,\n unmaintained_suspected: 30,\n};\n\nconst DIRECT_BONUS = 10;\nconst RUNTIME_BONUS = 5;\n\nfunction priorityFor(dep: DependencyObservation): number {\n const base = PRIORITY_BY_STATUS[dep.status] ?? 0;\n const direct = dep.direct ? DIRECT_BONUS : 0;\n const runtime = dep.scope === 'runtime' ? RUNTIME_BONUS : 0;\n return base + direct + runtime;\n}\n\n/**\n * Dedup key. A monorepo hoists the same package into many workspaces; they\n * share one registry identity and one answer, so researching `lodash@4.17.20`\n * twelve times would burn twelve LLM calls for one insight.\n *\n * Keyed on the resolved version too \u2014 `react@17` and `react@18` in the same\n * repo are genuinely different questions.\n */\nfunction dedupKey(dep: DependencyObservation): string {\n return `${dep.ecosystem}\u0000${dep.name}\u0000${dep.locked ?? dep.requested ?? ''}`;\n}\n\nexport interface TriageOptions {\n /** Max candidates returned. Defaults to {@link DEFAULT_TRIAGE_LIMIT}. */\n readonly limit?: number | undefined;\n}\n\n/**\n * Select and rank the dependencies worth researching.\n *\n * Ordering is fully deterministic \u2014 priority, then name, then version \u2014 so the\n * same snapshot always triages identically. Ties resolve by name rather than\n * by input order because adapter output order is not itself guaranteed stable\n * across platforms, and a wobbling triage would make the cap non-reproducible.\n */\nexport function triageCandidates(\n dependencies: readonly DependencyObservation[],\n options: TriageOptions = {},\n): readonly TriageCandidate[] {\n const limit = Math.max(0, options.limit ?? DEFAULT_TRIAGE_LIMIT);\n if (limit === 0) return [];\n\n const best = new Map<string, TriageCandidate>();\n\n for (const dependency of dependencies) {\n const cluster = CLUSTER_BY_STATUS[dependency.status];\n if (!cluster) continue;\n // Path/git deps have no registry identity to research even if some adapter\n // left a researchable status on them.\n if (dependency.sourceType === 'path' || dependency.sourceType === 'git') continue;\n\n const candidate: TriageCandidate = {\n dependency,\n cluster,\n priority: priorityFor(dependency),\n };\n\n const key = dedupKey(dependency);\n const existing = best.get(key);\n // Keep the highest-priority instance \u2014 the direct/runtime copy wins over\n // the transitive one, so the finding lands on the dep the user can act on.\n if (!existing || candidate.priority > existing.priority) {\n best.set(key, candidate);\n }\n }\n\n return [...best.values()]\n .sort(\n (a, b) =>\n b.priority - a.priority ||\n a.dependency.name.localeCompare(b.dependency.name) ||\n (a.dependency.locked ?? '').localeCompare(b.dependency.locked ?? ''),\n )\n .slice(0, limit);\n}\n\n/** Group triaged candidates by cluster, preserving triage order within each. */\nexport function clusterCandidates(\n candidates: readonly TriageCandidate[],\n): ReadonlyMap<ResearchCluster, readonly TriageCandidate[]> {\n const out = new Map<ResearchCluster, TriageCandidate[]>();\n for (const candidate of candidates) {\n const list = out.get(candidate.cluster);\n if (list) list.push(candidate);\n else out.set(candidate.cluster, [candidate]);\n }\n return out;\n}\n", "/**\n * TechStack \u2014 `Provider` \u2192 {@link ResearchLlm} adapter.\n *\n * Mirrors the capability-probing pattern established by the WebUI completion\n * handler (`packages/webui-server/src/server/completion-handlers.ts`,\n * `loadLlmSuggestions`): prefer strict structured output, fall back to JSON\n * mode, fall back again to prompt-only discipline. Providers differ, and a\n * research pass must not be exclusive to the ones with schema support.\n *\n * @see docs/specs/techstack-sdd.md \u00A74.2\n */\n\nimport type { Provider, Request } from '@wrongstack/core';\nimport type { ResearchLlm, ResearchLlmRequest } from './types.js';\n\n/** How the caller reaches a live provider. A getter, not a captured value \u2014\n * the user can switch model or rotate credentials mid-session, and a snapshot\n * of the provider would silently go stale. */\nexport type LlmAccessor = () => { provider: Provider; model: string } | undefined;\n\nconst DEFAULT_TIMEOUT_MS = 45_000;\n\n/**\n * Build a {@link ResearchLlm} from a provider accessor, or `undefined` when no\n * provider is currently wired \u2014 which is the signal `TechStackEngine` uses to\n * skip the research stage entirely and stay a deterministic tool.\n */\nexport function createProviderLlm(\n accessor: LlmAccessor,\n options: { readonly timeoutMs?: number | undefined } = {},\n): ResearchLlm | undefined {\n if (!accessor()) return undefined;\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n\n return async (req: ResearchLlmRequest): Promise<string> => {\n // Re-resolve per call so a mid-session model switch takes effect.\n const llm = accessor();\n if (!llm) throw new Error('TechStack research: no provider available');\n\n const request: Request = {\n model: llm.model,\n system: [{ type: 'text', text: req.system }],\n messages: [{ role: 'user', content: req.prompt }],\n maxTokens: req.maxTokens,\n };\n\n if (llm.provider.capabilities.structuredOutput) {\n request.responseFormat = {\n type: 'json_schema',\n jsonSchema: { name: req.schemaName, strict: false, schema: req.schema },\n };\n } else if (llm.provider.capabilities.jsonMode) {\n request.responseFormat = { type: 'json_object' };\n }\n\n const timer = new AbortController();\n const onAbort = () => {\n timer.abort(new Error('TechStack research: cancelled'));\n };\n req.signal?.addEventListener('abort', onAbort, { once: true });\n const to = setTimeout(() => {\n timer.abort(new Error('TechStack research: LLM timeout'));\n }, timeoutMs);\n to.unref?.();\n\n try {\n const res = await llm.provider.complete(request, { signal: timer.signal });\n return res.content\n .filter((block) => block.type === 'text')\n .map((block) => block.text)\n .join('\\n')\n .trim();\n } finally {\n req.signal?.removeEventListener('abort', onAbort);\n clearTimeout(to);\n timer.abort();\n }\n };\n}\n\n// \u2500\u2500 Response parsing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/** Strip one outer Markdown fence without touching inner fences. */\nfunction stripOuterFence(text: string): string {\n const trimmed = text.trim();\n const match = trimmed.match(/^```(?:[a-z0-9_-]+)?\\s*\\r?\\n([\\s\\S]*?)\\r?\\n```$/i);\n return (match?.[1] ?? trimmed).trim();\n}\n\n/**\n * Pull the outermost JSON object out of a response.\n *\n * Even with `json_object` set, models prepend prose often enough that a bare\n * `JSON.parse` is a coin flip. Same salvage the completion handler does\n * (`extractJson`).\n */\nfunction extractJsonObject(text: string): string {\n const trimmed = stripOuterFence(text);\n if (trimmed.startsWith('{')) return trimmed;\n const start = trimmed.indexOf('{');\n const end = trimmed.lastIndexOf('}');\n if (start !== -1 && end > start) return trimmed.slice(start, end + 1);\n return trimmed;\n}\n\n/**\n * Parse a research response into a plain object, or `null` when the model\n * returned something unusable.\n *\n * Never throws: an unparseable research response degrades that cluster to zero\n * findings, it does not fail the analyze job.\n */\nexport function parseResearchJson(text: string): Record<string, unknown> | null {\n if (!text.trim()) return null;\n try {\n const parsed: unknown = JSON.parse(extractJsonObject(text));\n return parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)\n ? (parsed as Record<string, unknown>)\n : null;\n } catch {\n return null;\n }\n}\n", "/**\n * TechStack \u2014 the research stage.\n *\n * Takes the deterministic snapshot's problem cases (see `triage.ts`), gathers\n * public context for them via web search, and asks the LLM the one question\n * the registry cannot answer: *what should I actually do about this?*\n *\n * Structure follows SDD \u00A7557 \u2014 one pass per finding-type cluster\n * (breaking-change / replacement / CVE-applicability), not one per package and\n * not one per ecosystem. That keeps the call count at \u22643 per analyze while\n * still letting each prompt be a focused specialist.\n *\n * @see docs/specs/techstack-sdd.md \u00A731, \u00A7472, \u00A7557\n */\n\nimport type { DependencyObservation, Evidence, Finding } from '../types.js';\nimport { parseResearchJson } from './llm.js';\nimport { clusterCandidates } from './triage.js';\nimport type {\n ResearchCluster,\n ResearchLlm,\n ResearchOptions,\n ResearchSearch,\n ResearchSearchResult,\n TechStackResearcher,\n TriageCandidate,\n} from './types.js';\n\n// \u2500\u2500 Tunables \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst SEARCH_CONCURRENCY = 4;\nconst MAX_SNIPPET_CHARS = 320;\nconst MAX_TOKENS_PER_CLUSTER = 2_000;\n\n/**\n * Confidence ceiling for anything the LLM produced. `1.0` is reserved for\n * deterministic findings (registry/OSV facts) \u2014 an interpretation must never\n * be able to present itself as a fact, however sure the model claims to be.\n */\nconst MAX_LLM_CONFIDENCE = 0.95;\nconst MIN_LLM_CONFIDENCE = 0.1;\nconst DEFAULT_LLM_CONFIDENCE = 0.5;\n\nconst FINDING_TYPE_BY_CLUSTER: Record<ResearchCluster, Finding['type']> = {\n breaking_change: 'upgrade',\n replacement: 'replacement',\n vulnerability: 'vulnerability',\n};\n\nconst VALID_SEVERITIES: ReadonlySet<string> = new Set([\n 'info',\n 'low',\n 'medium',\n 'high',\n 'critical',\n]);\n\nconst VALID_ACTIONS: ReadonlySet<string> = new Set([\n 'none',\n 'upgrade_patch',\n 'upgrade_minor',\n 'upgrade_major',\n 'replace',\n 'remove',\n 'investigate',\n]);\n\n// \u2500\u2500 Prompts \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst SHARED_RULES = [\n 'Return only JSON. No markdown, prose, or code fences.',\n 'Only reason about the packages listed in the input. Never introduce a package that is not listed.',\n 'Never state or guess version numbers other than the ones given to you; the version facts are already established.',\n 'If the provided sources do not support a conclusion, say so in the rationale and lower the confidence.',\n 'Be concrete and short. The reader is an engineer deciding what to do this afternoon.',\n].join('\\n');\n\nconst SYSTEM_BY_CLUSTER: Record<ResearchCluster, string> = {\n breaking_change: [\n 'You assess upgrade risk for software dependencies.',\n 'For each package, judge how disruptive moving to the latest version would be for a typical consumer,',\n 'and what the migration actually involves.',\n SHARED_RULES,\n ].join('\\n'),\n replacement: [\n 'You advise on deprecated, yanked, and unmaintained software dependencies.',\n 'For each package, say whether it should be replaced, what the community has moved to, and how urgent it is.',\n 'Prefer platform-native or well-maintained successors. If the package is fine to keep, say so plainly.',\n SHARED_RULES,\n ].join('\\n'),\n vulnerability: [\n 'You triage security advisories for software dependencies.',\n 'For each package, judge how exploitable the known advisory is in practice and what the fix is.',\n 'Distinguish advisories that require unusual usage from ones that affect every consumer.',\n SHARED_RULES,\n ].join('\\n'),\n};\n\nconst QUESTION_BY_CLUSTER: Record<ResearchCluster, string> = {\n breaking_change: 'How breaking is this upgrade, and what does the migration involve?',\n replacement: 'Should this be replaced, and with what?',\n vulnerability: 'Does this advisory realistically affect a consumer, and what is the fix?',\n};\n\nconst RESEARCH_JSON_SCHEMA: Record<string, unknown> = {\n type: 'object',\n additionalProperties: false,\n properties: {\n findings: {\n type: 'array',\n items: {\n type: 'object',\n additionalProperties: false,\n properties: {\n package: { type: 'string', description: 'Exact package name from the input list.' },\n severity: { type: 'string', enum: ['info', 'low', 'medium', 'high', 'critical'] },\n action: {\n type: 'string',\n enum: [\n 'none',\n 'upgrade_patch',\n 'upgrade_minor',\n 'upgrade_major',\n 'replace',\n 'remove',\n 'investigate',\n ],\n },\n confidence: { type: 'number', minimum: 0, maximum: 1 },\n rationale: { type: 'string' },\n breakingRisk: { type: 'string' },\n sources: { type: 'array', items: { type: 'string' } },\n },\n required: ['package', 'severity', 'action', 'confidence', 'rationale'],\n },\n },\n },\n required: ['findings'],\n};\n\n// \u2500\u2500 Search queries \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction searchQuery(candidate: TriageCandidate): string {\n const dep = candidate.dependency;\n const from = dep.locked ?? dep.requested ?? '';\n const to = dep.latestStable ?? '';\n switch (candidate.cluster) {\n case 'breaking_change':\n return `${dep.name} ${from} to ${to} migration guide breaking changes`;\n case 'replacement':\n return `${dep.name} ${dep.ecosystem} deprecated recommended alternative replacement`;\n case 'vulnerability':\n return `${dep.name} ${from} security advisory CVE affected versions`;\n }\n}\n\n/** Run `task` over `items` with bounded concurrency, preserving order. */\nasync function mapLimit<T, R>(\n items: readonly T[],\n limit: number,\n task: (item: T, index: number) => Promise<R>,\n): Promise<R[]> {\n const out = new Array<R>(items.length);\n let cursor = 0;\n const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {\n for (;;) {\n const index = cursor++;\n if (index >= items.length) return;\n out[index] = await task(items[index]!, index);\n }\n });\n await Promise.all(workers);\n return out;\n}\n\n// \u2500\u2500 Prompt assembly \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction describeDependency(dep: DependencyObservation): string {\n const bits = [\n `ecosystem: ${dep.ecosystem}`,\n `installed: ${dep.locked ?? dep.installed ?? dep.requested ?? 'unknown'}`,\n ];\n if (dep.latestStable) bits.push(`latest stable: ${dep.latestStable}`);\n if (dep.requested) bits.push(`constraint: ${dep.requested}`);\n bits.push(dep.direct ? 'direct dependency' : 'transitive dependency');\n bits.push(`scope: ${dep.scope}`);\n if (dep.license) bits.push(`license: ${dep.license}`);\n if (dep.deprecated) bits.push('registry flag: deprecated');\n if (dep.yanked) bits.push('registry flag: yanked');\n bits.push(`status: ${dep.status}`);\n return bits.join(', ');\n}\n\nfunction renderSources(results: readonly ResearchSearchResult[]): string {\n if (results.length === 0) return ' (no sources found \u2014 say so and lower confidence)';\n return results\n .map((r) => ` - ${r.title}\\n ${r.url}\\n ${truncate(r.snippet, MAX_SNIPPET_CHARS)}`)\n .join('\\n');\n}\n\nfunction truncate(value: string, max: number): string {\n const clean = value.replace(/\\s+/g, ' ').trim();\n return clean.length <= max ? clean : `${clean.slice(0, max)}\u2026`;\n}\n\nfunction buildPrompt(\n cluster: ResearchCluster,\n entries: readonly { candidate: TriageCandidate; sources: readonly ResearchSearchResult[] }[],\n): string {\n const blocks = entries.map(({ candidate, sources }, i) =>\n [\n `${i + 1}. ${candidate.dependency.name}`,\n ` ${describeDependency(candidate.dependency)}`,\n ' Web search results:',\n renderSources(sources),\n ].join('\\n'),\n );\n\n return [\n `Question for every package below: ${QUESTION_BY_CLUSTER[cluster]}`,\n '',\n 'Packages:',\n ...blocks,\n '',\n 'Return JSON shaped exactly as:',\n '{\"findings\":[{\"package\":\"exact-name-from-the-list\",\"severity\":\"medium\",\"action\":\"upgrade_major\",' +\n '\"confidence\":0.7,\"rationale\":\"one or two sentences\",\"breakingRisk\":\"optional short note\",' +\n '\"sources\":[\"https://\u2026\"]}]}',\n '',\n 'Emit one entry per package you can say something useful about. Omit packages you cannot.',\n ].join('\\n');\n}\n\n// \u2500\u2500 Response \u2192 Finding \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction optionalString(value: unknown): string | undefined {\n return typeof value === 'string' && value.trim() ? value.trim() : undefined;\n}\n\nfunction clampConfidence(value: unknown): number {\n if (typeof value !== 'number' || !Number.isFinite(value)) return DEFAULT_LLM_CONFIDENCE;\n return Math.min(MAX_LLM_CONFIDENCE, Math.max(MIN_LLM_CONFIDENCE, value));\n}\n\nfunction sourceEvidence(\n raw: unknown,\n sources: readonly ResearchSearchResult[],\n retrievedAt: string,\n): Evidence[] {\n const known = new Set(sources.map((s) => s.url));\n const cited = Array.isArray(raw)\n ? raw.filter((u): u is string => typeof u === 'string' && known.has(u))\n : [];\n // Fall back to everything we actually fetched when the model cited nothing \u2014\n // the user still deserves to see what the interpretation was based on.\n const urls = cited.length > 0 ? cited : sources.map((s) => s.url);\n return urls.map((url) => ({\n kind: 'agent' as const,\n source: url,\n retrievedAt,\n detail: sources.find((s) => s.url === url)?.title,\n }));\n}\n\nfunction toFinding(\n raw: Record<string, unknown>,\n cluster: ResearchCluster,\n byName: ReadonlyMap<string, { candidate: TriageCandidate; sources: readonly ResearchSearchResult[] }>,\n retrievedAt: string,\n): Finding | null {\n const name = optionalString(raw.package);\n if (!name) return null;\n\n // Anti-hallucination gate: a finding for a package we did not ask about is\n // discarded outright. The model does not get to expand the inventory.\n const entry = byName.get(name);\n if (!entry) return null;\n\n const rationale = optionalString(raw.rationale);\n if (!rationale) return null;\n\n const severity = VALID_SEVERITIES.has(raw.severity as string)\n ? (raw.severity as Finding['severity'])\n : 'info';\n const action = VALID_ACTIONS.has(raw.action as string)\n ? (raw.action as Finding['action'])\n : 'investigate';\n\n const breakingRisk = optionalString(raw.breakingRisk);\n\n return {\n id: `research-${entry.candidate.dependency.id}-${cluster}`,\n dependencyId: entry.candidate.dependency.id,\n type: FINDING_TYPE_BY_CLUSTER[cluster],\n severity,\n action,\n confidence: clampConfidence(raw.confidence),\n rationale,\n ...(breakingRisk ? { breakingRisk } : {}),\n evidence: sourceEvidence(raw.sources, entry.sources, retrievedAt),\n };\n}\n\nfunction parseFindings(\n parsed: Record<string, unknown> | null,\n cluster: ResearchCluster,\n byName: ReadonlyMap<string, { candidate: TriageCandidate; sources: readonly ResearchSearchResult[] }>,\n retrievedAt: string,\n): Finding[] {\n if (!parsed || !Array.isArray(parsed.findings)) return [];\n const seen = new Set<string>();\n const out: Finding[] = [];\n for (const raw of parsed.findings) {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) continue;\n const finding = toFinding(raw as Record<string, unknown>, cluster, byName, retrievedAt);\n if (!finding || seen.has(finding.id)) continue;\n seen.add(finding.id);\n out.push(finding);\n }\n return out;\n}\n\n// \u2500\u2500 Researcher \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface CreateResearcherOptions {\n readonly llm: ResearchLlm;\n readonly search: ResearchSearch;\n /** Injectable clock \u2014 keeps evidence timestamps deterministic in tests. */\n readonly now?: (() => Date) | undefined;\n}\n\n/**\n * Build a {@link TechStackResearcher} over the injected LLM and search ports.\n *\n * Failure policy throughout: a cluster that throws contributes zero findings\n * and does not disturb its siblings. Research is enrichment \u2014 a provider\n * outage or a dry search must degrade the report, never fail the analyze job\n * that already produced a complete deterministic inventory.\n */\nexport function createResearcher(options: CreateResearcherOptions): TechStackResearcher {\n const now = options.now ?? (() => new Date());\n\n return {\n async research(candidates, opts: ResearchOptions = {}): Promise<readonly Finding[]> {\n if (candidates.length === 0) return [];\n\n const clusters = [...clusterCandidates(candidates).entries()];\n const findings: Finding[] = [];\n let completed = 0;\n opts.onProgress?.(0, clusters.length);\n\n for (const [cluster, members] of clusters) {\n if (opts.signal?.aborted) break;\n\n try {\n const sources = await mapLimit(members, SEARCH_CONCURRENCY, (candidate) =>\n options.search(searchQuery(candidate), { signal: opts.signal }),\n );\n if (opts.signal?.aborted) break;\n\n const entries = members.map((candidate, i) => ({\n candidate,\n sources: sources[i] ?? [],\n }));\n const byName = new Map(entries.map((entry) => [entry.candidate.dependency.name, entry]));\n\n const text = await options.llm({\n system: SYSTEM_BY_CLUSTER[cluster],\n prompt: buildPrompt(cluster, entries),\n schema: RESEARCH_JSON_SCHEMA,\n schemaName: `techstack_${cluster}_findings`,\n maxTokens: MAX_TOKENS_PER_CLUSTER,\n signal: opts.signal,\n });\n\n findings.push(\n ...parseFindings(parseResearchJson(text), cluster, byName, now().toISOString()),\n );\n } catch {\n // This cluster contributed nothing. The others still run, and the\n // deterministic findings from `enrich()` are untouched.\n }\n\n completed++;\n opts.onProgress?.(completed, clusters.length);\n }\n\n return findings;\n },\n };\n}\n", "/**\n * TechStack \u2014 `searchTool` \u2192 {@link ResearchSearch} adapter.\n *\n * Wraps the built-in web search (`packages/tools/src/search.ts`) so the\n * researcher depends on a one-function port rather than the tool contract.\n *\n * Note on reliability: that tool scrapes DuckDuckGo/Google/Bing through\n * `guardedFetch` \u2014 there is no API key and no SLA. It can and does return\n * nothing. Every failure here resolves to `[]` so a dry search degrades a\n * finding to registry-only evidence instead of failing the analyze job.\n */\n\nimport type { Context } from '@wrongstack/core';\nimport { searchTool } from '@wrongstack/tools';\nimport type { ResearchSearch, ResearchSearchResult } from './types.js';\n\nconst DEFAULT_NUM_RESULTS = 5;\n\n/**\n * `searchTool` declares a `Context` parameter to satisfy the shared `Tool`\n * contract but never reads it \u2014 its implementation signature is\n * `executeStream(input, _ctx, opts)`. Research runs server-side with no agent\n * session to hand over, so we pass a placeholder rather than fabricate a\n * half-populated Context that would be more misleading than an obvious stub.\n */\nconst NO_CONTEXT = undefined as unknown as Context;\n\nexport interface SearchToolOptions {\n readonly numResults?: number | undefined;\n readonly source?: 'duckduckgo' | 'google' | 'bing' | undefined;\n}\n\n/** Build a {@link ResearchSearch} backed by the built-in `search` tool. */\nexport function createToolSearch(options: SearchToolOptions = {}): ResearchSearch {\n const numResults = options.numResults ?? DEFAULT_NUM_RESULTS;\n\n return async (query, opts): Promise<readonly ResearchSearchResult[]> => {\n if (opts.signal?.aborted) return [];\n try {\n const out = await searchTool.execute(\n {\n query,\n num_results: numResults,\n ...(options.source ? { source: options.source } : {}),\n },\n NO_CONTEXT,\n { signal: opts.signal ?? new AbortController().signal },\n );\n return out.results.map((result) => ({\n title: result.title,\n url: result.url,\n snippet: result.snippet,\n }));\n } catch {\n // Scraped search is best-effort. Interpretation without sources is still\n // better than no interpretation, and the finding's evidence list makes\n // the absence visible to the user.\n return [];\n }\n };\n}\n", "/**\n * TechStack \u2014 SQLite-backed store.\n *\n * Provides persistence for snapshots, jobs, and the delivery outbox\n * using Node 22.5+'s built-in `node:sqlite` module.\n *\n * Store path: ~/.wrongstack/projects/<slug>/techstack/techstack.db\n *\n * @see docs/specs/techstack-sdd.md \u00A73.2, \u00A74.1\n */\n\nimport { DatabaseSync } from 'node:sqlite';\nimport { mkdirSync, existsSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { homedir } from 'node:os';\nimport { applySchema } from './schema.js';\nimport type {\n DeliveryOutbox,\n DeliveryStatus,\n Snapshot,\n TechStackJob,\n TechStackJobStatus,\n TechStackJobProgress,\n} from '../types.js';\n\n// \u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface StoreOptions {\n /** Project slug used for the store directory path. */\n readonly projectSlug: string;\n /** Optional explicit dbPath override (for testing). */\n readonly dbPath?: string;\n}\n\n// \u2500\u2500 Store class \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport class TechStackStore {\n private db: DatabaseSync;\n private dbPath: string;\n\n constructor(options: StoreOptions) {\n this.dbPath =\n options.dbPath ??\n join(\n homedir(),\n '.wrongstack',\n 'projects',\n options.projectSlug,\n 'techstack',\n 'techstack.db',\n );\n\n // Ensure parent directory exists\n const dir = this.dbPath.slice(0, this.dbPath.lastIndexOf('\\\\'));\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n\n this.db = new DatabaseSync(this.dbPath);\n this.db.exec('PRAGMA journal_mode = WAL;');\n this.db.exec('PRAGMA foreign_keys = ON;');\n applySchema(this.db);\n }\n\n // \u2500\u2500 Lifecycle \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n /** Close the database connection. Idempotent. */\n close(): void {\n try {\n this.db.close();\n } catch {\n // Already closed \u2014 idempotent\n }\n }\n\n /** Get the database path (useful for tests). */\n get path(): string {\n return this.dbPath;\n }\n\n // \u2500\u2500 Snapshots \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n /** Persist a snapshot. */\n saveSnapshot(snapshot: Snapshot): void {\n const stmt = this.db.prepare(`\n INSERT OR REPLACE INTO snapshots (id, project_id, target_root, fingerprint, created_at, raw_json, adapter_version)\n VALUES (?, ?, ?, ?, ?, ?, ?)\n `);\n stmt.run(\n snapshot.id,\n snapshot.projectId,\n snapshot.targetRoot,\n snapshot.fingerprint,\n snapshot.createdAt,\n JSON.stringify(snapshot),\n snapshot.adapterVersion,\n );\n }\n\n /** Get a snapshot by project ID (latest). */\n getSnapshot(projectId: string): Snapshot | undefined {\n const stmt = this.db.prepare(`\n SELECT raw_json FROM snapshots\n WHERE project_id = ?\n ORDER BY created_at DESC\n LIMIT 1\n `);\n const row = stmt.get(projectId) as { raw_json: string } | undefined;\n if (!row) return undefined;\n try {\n return JSON.parse(row.raw_json) as Snapshot;\n } catch {\n return undefined;\n }\n }\n\n /** Get a snapshot by ID. */\n getSnapshotById(id: string): Snapshot | undefined {\n const stmt = this.db.prepare(`\n SELECT raw_json FROM snapshots WHERE id = ?\n `);\n const row = stmt.get(id) as { raw_json: string } | undefined;\n if (!row) return undefined;\n try {\n return JSON.parse(row.raw_json) as Snapshot;\n } catch {\n return undefined;\n }\n }\n\n /** List all snapshots for a project (newest first). */\n listSnapshots(projectId: string, limit = 20): Snapshot[] {\n const stmt = this.db.prepare(`\n SELECT raw_json FROM snapshots\n WHERE project_id = ?\n ORDER BY created_at DESC\n LIMIT ?\n `);\n const rows = stmt.all(projectId, limit) as Array<{ raw_json: string }>;\n return rows\n .map((r) => {\n try {\n return JSON.parse(r.raw_json) as Snapshot;\n } catch {\n return undefined;\n }\n })\n .filter((s): s is Snapshot => s !== undefined);\n }\n\n /** Delete snapshots older than a given timestamp. */\n deleteSnapshotsBefore(projectId: string, before: string): number {\n const stmt = this.db.prepare(`\n DELETE FROM snapshots WHERE project_id = ? AND created_at < ?\n `);\n const result = stmt.run(projectId, before);\n return Number(result.changes);\n }\n\n // \u2500\u2500 Jobs \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n /** Persist a job. */\n saveJob(job: TechStackJob): void {\n const stmt = this.db.prepare(`\n INSERT OR REPLACE INTO jobs\n (id, project_id, target_root, kind, status, fingerprint, requested_by, session_id, created_at, completed_at, error, progress_json)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n `);\n stmt.run(\n job.id,\n job.projectId,\n job.targetRoot,\n job.kind,\n job.status,\n job.fingerprint,\n job.requestedBy,\n job.sessionId ?? null,\n job.createdAt,\n job.completedAt ?? null,\n job.error ?? null,\n job.progress ? JSON.stringify(job.progress) : null,\n );\n }\n\n /** Get a job by ID. */\n getJob(id: string): TechStackJob | undefined {\n const stmt = this.db.prepare(`\n SELECT * FROM jobs WHERE id = ?\n `);\n const row = stmt.get(id) as Record<string, unknown> | undefined;\n if (!row) return undefined;\n return this.rowToJob(row);\n }\n\n /** Update job status and optional progress. */\n updateJobStatus(id: string, status: TechStackJobStatus, progress?: TechStackJobProgress): void {\n const progressJson = progress ? JSON.stringify(progress) : null;\n const completedAt = status === 'completed' || status === 'failed' || status === 'cancelled'\n ? new Date().toISOString()\n : null;\n\n const stmt = this.db.prepare(`\n UPDATE jobs\n SET status = ?, progress_json = ?, completed_at = COALESCE(?, completed_at)\n WHERE id = ?\n `);\n stmt.run(status, progressJson, completedAt, id);\n }\n\n /** List jobs for a project (newest first). */\n listJobs(projectId: string, limit = 50): TechStackJob[] {\n const stmt = this.db.prepare(`\n SELECT * FROM jobs WHERE project_id = ? ORDER BY created_at DESC LIMIT ?\n `);\n const rows = stmt.all(projectId, limit) as Array<Record<string, unknown>>;\n return rows.map((r) => this.rowToJob(r));\n }\n\n // \u2500\u2500 Outbox \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n /** Create an outbox entry. */\n createOutbox(deliveryId: string, reportId: string, sessionId: string): void {\n const stmt = this.db.prepare(`\n INSERT OR IGNORE INTO outbox (delivery_id, report_id, session_id, status, attempts)\n VALUES (?, ?, ?, 'pending', 0)\n `);\n stmt.run(deliveryId, reportId, sessionId);\n }\n\n /** Claim an outbox entry (atomic CAS). */\n claimOutbox(deliveryId: string, sessionId: string): boolean {\n const stmt = this.db.prepare(`\n UPDATE outbox\n SET status = 'claimed', claimed_at = datetime('now'), attempts = attempts + 1\n WHERE delivery_id = ? AND session_id = ? AND status = 'pending'\n `);\n const result = stmt.run(deliveryId, sessionId);\n return result.changes > 0;\n }\n\n /** Mark an outbox entry as delivered. */\n deliverOutbox(deliveryId: string): void {\n const stmt = this.db.prepare(`\n UPDATE outbox SET status = 'delivered', delivered_at = datetime('now')\n WHERE delivery_id = ?\n `);\n stmt.run(deliveryId);\n }\n\n /** Mark an outbox entry as failed. */\n failOutbox(deliveryId: string): void {\n const stmt = this.db.prepare(`\n UPDATE outbox SET status = 'failed' WHERE delivery_id = ?\n `);\n stmt.run(deliveryId);\n }\n\n /** List outbox entries by status. */\n listOutboxByStatus(status: DeliveryStatus): DeliveryOutbox[] {\n const stmt = this.db.prepare(`\n SELECT * FROM outbox WHERE status = ?\n `);\n const rows = stmt.all(status) as Array<Record<string, unknown>>;\n return rows.map((r) => this.rowToOutbox(r));\n }\n\n // \u2500\u2500 Row mapping helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n private rowToJob(row: Record<string, unknown>): TechStackJob {\n let progress: TechStackJobProgress | undefined;\n if (row.progress_json && typeof row.progress_json === 'string') {\n try {\n progress = JSON.parse(row.progress_json) as TechStackJobProgress;\n } catch {\n // ignore\n }\n }\n\n return {\n id: String(row.id),\n projectId: String(row.project_id),\n targetRoot: String(row.target_root),\n kind: row.kind as TechStackJob['kind'],\n status: row.status as TechStackJobStatus,\n fingerprint: String(row.fingerprint ?? ''),\n requestedBy: String(row.requested_by ?? ''),\n sessionId: row.session_id ? String(row.session_id) : undefined,\n createdAt: String(row.created_at),\n completedAt: row.completed_at ? String(row.completed_at) : undefined,\n error: row.error ? String(row.error) : undefined,\n ...(progress ? { progress } : {}),\n };\n }\n\n private rowToOutbox(row: Record<string, unknown>): DeliveryOutbox {\n return {\n deliveryId: String(row.delivery_id),\n reportId: String(row.report_id),\n sessionId: String(row.session_id),\n status: row.status as DeliveryStatus,\n attempts: Number(row.attempts),\n claimedAt: row.claimed_at ? String(row.claimed_at) : undefined,\n deliveredAt: row.delivered_at ? String(row.delivered_at) : undefined,\n };\n }\n}\n", "/**\n * TechStack \u2014 SQLite schema (DDL) and migration helpers.\n *\n * Defines the tables for snapshots, jobs, and the delivery outbox.\n * Uses `node:sqlite` (built-in since Node 22.5+).\n *\n * Tables:\n * - snapshots: persisted inventory snapshots\n * - jobs: async inventory/analyze job state\n * - outbox: idle-delivery tracking\n *\n * @see docs/specs/techstack-sdd.md \u00A73.2, \u00A74.1\n */\n\nexport const SCHEMA_VERSION = 1;\n\nexport const DDL = `\nCREATE TABLE IF NOT EXISTS techstack_schema_version (\n version INTEGER NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS snapshots (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL,\n target_root TEXT NOT NULL,\n fingerprint TEXT NOT NULL,\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n raw_json TEXT NOT NULL,\n adapter_version TEXT NOT NULL DEFAULT ''\n);\n\nCREATE INDEX IF NOT EXISTS idx_snapshots_project_id ON snapshots(project_id);\nCREATE INDEX IF NOT EXISTS idx_snapshots_created_at ON snapshots(created_at DESC);\n\nCREATE TABLE IF NOT EXISTS jobs (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL,\n target_root TEXT NOT NULL,\n kind TEXT NOT NULL CHECK(kind IN ('inventory', 'analyze')),\n status TEXT NOT NULL DEFAULT 'queued'\n CHECK(status IN ('queued','discovering','inventorying','enriching','researching','synthesizing','completed','failed','cancelled')),\n fingerprint TEXT NOT NULL DEFAULT '',\n requested_by TEXT NOT NULL DEFAULT '',\n session_id TEXT,\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n completed_at TEXT,\n error TEXT,\n progress_json TEXT\n);\n\nCREATE INDEX IF NOT EXISTS idx_jobs_project_id ON jobs(project_id);\nCREATE INDEX IF NOT EXISTS idx_jobs_status ON jobs(status);\n\nCREATE TABLE IF NOT EXISTS outbox (\n delivery_id TEXT PRIMARY KEY,\n report_id TEXT NOT NULL,\n session_id TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT 'pending'\n CHECK(status IN ('pending', 'claimed', 'delivered', 'failed')),\n attempts INTEGER NOT NULL DEFAULT 0,\n claimed_at TEXT,\n delivered_at TEXT\n);\n\nCREATE INDEX IF NOT EXISTS idx_outbox_status ON outbox(status);\n`;\n\n/**\n * Run the schema DDL and check/migrate version.\n */\nexport function applySchema(db: import('node:sqlite').DatabaseSync): void {\n // Execute DDL (IF NOT EXISTS makes it idempotent)\n for (const statement of DDL.split(';')) {\n const trimmed = statement.trim();\n if (trimmed) {\n db.exec(trimmed);\n }\n }\n\n // Check version\n const row = db.prepare('SELECT version FROM techstack_schema_version').get() as\n | { version: number }\n | undefined;\n\n if (!row) {\n db.prepare('INSERT INTO techstack_schema_version (version) VALUES (?)').run(SCHEMA_VERSION);\n } else if (row.version < SCHEMA_VERSION) {\n // Future: run migrations here\n db.prepare('UPDATE techstack_schema_version SET version = ?').run(SCHEMA_VERSION);\n }\n}\n", "/**\n * TechStack \u2014 DeliveryCoordinator.\n *\n * Per-session idle-delivery coordinator for TechStack reports.\n *\n * Guarantees (per SDD \u00A75):\n * 1. Idle-only: report is never injected while a run is in progress.\n * 2. Exactly-once visible delivery: duplicate outbox entries \u2192 single delivery.\n * 3. Durable: survives store close/reopen; pending entries retry on recovery.\n * 4. Bounded: at most one delivery per report per session.\n *\n * @see docs/specs/techstack-sdd.md \u00A75\n */\n\nimport type { TechStackStore } from '../store/sqlite.js';\nimport type { Snapshot } from '../types.js';\n\nexport interface DeliveryCoordinatorOptions {\n readonly store: TechStackStore;\n /** Check whether an agent run is currently in progress. */\n readonly isRunInProgress: () => boolean;\n /** Deliver the report summary to the chat session (append to journal). */\n readonly deliverToSession: (sessionId: string, reportId: string, summary: string) => Promise<boolean>;\n /** Called when a delivery completes (for WS event broadcasting). */\n readonly onDelivered?: ((deliveryId: string, sessionId: string) => void) | undefined;\n /** Poll interval in ms. Default: 2000. */\n readonly pollIntervalMs?: number | undefined;\n}\n\nexport interface DeliveryResult {\n deliveryId: string;\n sessionId: string;\n delivered: boolean;\n}\n\n/**\n * Attempt to deliver a single pending outbox entry.\n *\n * Returns `{ delivered: false }` if the run is still in progress or the\n * entry has already been delivered. Returns `{ delivered: true }` on success.\n *\n * This function is idempotent: calling it twice for the same deliveryId\n * will deliver exactly once (the CAS in claimOutbox prevents double-claim).\n */\nexport async function attemptDelivery(\n deliveryId: string,\n opts: DeliveryCoordinatorOptions,\n): Promise<DeliveryResult> {\n const { store, isRunInProgress, deliverToSession, onDelivered } = opts;\n\n // Idle-only gate\n if (isRunInProgress()) {\n return { deliveryId, sessionId: '', delivered: false };\n }\n\n // Find the outbox entry\n const pending = store.listOutboxByStatus('pending');\n const entry = pending.find((e: { deliveryId: string }) => e.deliveryId === deliveryId);\n if (!entry) {\n return { deliveryId, sessionId: '', delivered: false };\n }\n\n // Atomic CAS claim \u2014 prevents double-delivery\n const claimed = store.claimOutbox(deliveryId, entry.sessionId);\n if (!claimed) {\n return { deliveryId, sessionId: entry.sessionId, delivered: false };\n }\n\n // Generate a summary from the snapshot\n const snapshot = store.getSnapshotById(entry.reportId);\n const summary = snapshot ? buildSummary(snapshot) : `TechStack report ${entry.reportId} is ready.`;\n\n // Deliver to session\n const success = await deliverToSession(entry.sessionId, entry.reportId, summary);\n\n if (success) {\n store.deliverOutbox(deliveryId);\n onDelivered?.(deliveryId, entry.sessionId);\n return { deliveryId, sessionId: entry.sessionId, delivered: true };\n }\n\n store.failOutbox(deliveryId);\n return { deliveryId, sessionId: entry.sessionId, delivered: false };\n}\n\n/**\n * Process all pending outbox entries for a given session.\n * Called by the coordinator loop when the session becomes idle.\n */\nexport async function drainPendingDeliveries(\n sessionId: string,\n opts: DeliveryCoordinatorOptions,\n): Promise<number> {\n const { store, isRunInProgress } = opts;\n if (isRunInProgress()) return 0;\n\n const pending = store.listOutboxByStatus('pending');\n let delivered = 0;\n for (const entry of pending) {\n if (entry.sessionId !== sessionId) continue;\n const result = await attemptDelivery(entry.deliveryId, opts);\n if (result.delivered) delivered++;\n }\n return delivered;\n}\n\n/**\n * Build a concise chat-friendly summary from a snapshot.\n *\n * Format: counts + top findings + report link.\n */\nfunction buildSummary(snapshot: Snapshot): string {\n const lines: string[] = [\n `\uD83D\uDCCA **TechStack Report Ready**`,\n '',\n `**${snapshot.workspaces.length}** workspaces \u00B7 **${snapshot.dependencies.length}** dependencies \u00B7 **${snapshot.findings.length}** findings`,\n ];\n\n const findings = snapshot.findings as ReadonlyArray<{\n severity: string;\n type: string;\n rationale: string;\n dependencyId: string;\n }>;\n const critical = findings.filter((f) => f.severity === 'critical' || f.severity === 'high');\n if (critical.length > 0) {\n lines.push('', `**Top ${Math.min(5, critical.length)} urgent findings:**`);\n for (const f of critical.slice(0, 5)) {\n const dep = snapshot.dependencies.find((d: { id: string }) => d.id === f.dependencyId);\n lines.push(` \u2022 **${dep?.name ?? f.dependencyId}** \u2014 ${f.type}: ${f.rationale}`);\n }\n }\n\n lines.push('', `_Open the TechStack view for the full report._`);\n return lines.join('\\n');\n}\n"],
|
|
4
|
+
"sourcesContent": ["/**\n * TechStack \u2014 Package URL (PURL) construction and parsing.\n *\n * PURL spec: https://github.com/package-url/purl-spec\n *\n * Every dependency that comes from a registry gets a PURL identifier\n * so it can be matched across ecosystems, deduplicated in aggregate views,\n * and queried against OSV's /v1/querybatch endpoint.\n *\n * @see docs/specs/techstack-sdd.md \u00A74.1, \u00A75\n */\n\nimport type { EcosystemId } from '../types.js';\n\n/**\n * Parsed PURL components.\n */\nexport interface PurlParts {\n readonly type: string;\n readonly namespace?: string | undefined;\n readonly name: string;\n readonly version?: string | undefined;\n readonly qualifiers?: ReadonlyMap<string, string> | undefined;\n readonly subpath?: string | undefined;\n}\n\n/**\n * Map ecosystem ids to PURL type strings.\n * Some ecosystems have different PURL types than their internal id.\n */\nconst ECOSYSTEM_TO_PURL_TYPE: Readonly<Record<EcosystemId, string>> = {\n npm: 'npm',\n python: 'pypi',\n rust: 'cargo',\n go: 'golang',\n dotnet: 'nuget',\n php: 'composer',\n dart: 'pub',\n maven: 'maven',\n gradle: 'maven',\n ruby: 'gem',\n swift: 'swift',\n elixir: 'hex',\n cpp: 'conan',\n};\n\n/**\n * Reverse map for parsing PURL type back to ecosystem id.\n */\nconst PURL_TYPE_TO_ECOSYSTEM: Readonly<Record<string, EcosystemId>> = {\n npm: 'npm',\n pypi: 'python',\n cargo: 'rust',\n golang: 'go',\n nuget: 'dotnet',\n composer: 'php',\n pub: 'dart',\n maven: 'maven',\n gem: 'ruby',\n swift: 'swift',\n hex: 'elixir',\n conan: 'cpp',\n};\n\n/**\n * Build a PURL string from components.\n *\n * @example\n * buildPurl({ type: 'npm', name: 'react', version: '19.1.0' })\n * // \u2192 'pkg:npm/react@19.1.0'\n *\n * buildPurl({ type: 'npm', namespace: '@types', name: 'node', version: '22.0.0' })\n * // \u2192 'pkg:npm/%40types/node@22.0.0'\n *\n * buildPurl({ type: 'maven', namespace: 'org.springframework', name: 'spring-core', version: '6.2.7' })\n * // \u2192 'pkg:maven/org.springframework/spring-core@6.2.7'\n *\n * buildPurl({ type: 'pypi', name: 'django', version: '5.2' })\n * // \u2192 'pkg:pypi/django@5.2'\n */\nexport function buildPurl(parts: PurlParts): string {\n const segments: string[] = ['pkg:', parts.type, '/'];\n\n if (parts.namespace) {\n segments.push(encodePurlSegment(parts.namespace), '/');\n }\n\n segments.push(encodePurlSegment(parts.name));\n\n if (parts.version) {\n segments.push('@', encodePurlSegment(parts.version));\n }\n\n if (parts.qualifiers && parts.qualifiers.size > 0) {\n const qs = [...parts.qualifiers.entries()]\n .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)\n .join('&');\n segments.push('?', qs);\n }\n\n if (parts.subpath) {\n segments.push('#', encodePurlSegment(parts.subpath));\n }\n\n return segments.join('');\n}\n\n/**\n * Parse a PURL string into its components.\n * Returns undefined for malformed PURLs.\n *\n * @example\n * parsePurl('pkg:npm/react@19.1.0')\n * // \u2192 { type: 'npm', name: 'react', version: '19.1.0' }\n *\n * parsePurl('pkg:maven/org.springframework/spring-core@6.2.7')\n * // \u2192 { type: 'maven', namespace: 'org.springframework', name: 'spring-core', version: '6.2.7' }\n */\nexport function parsePurl(purl: string): PurlParts | undefined {\n if (!purl.startsWith('pkg:')) return undefined;\n\n const withoutPrefix = purl.slice(4);\n\n // Split subpath\n let main = withoutPrefix;\n let subpath: string | undefined;\n const hashIdx = main.indexOf('#');\n if (hashIdx >= 0) {\n subpath = decodePurlSegment(main.slice(hashIdx + 1));\n main = main.slice(0, hashIdx);\n }\n\n // Split qualifiers\n let qualifiers: Map<string, string> | undefined;\n const qIdx = main.indexOf('?');\n if (qIdx >= 0) {\n const qs = main.slice(qIdx + 1);\n main = main.slice(0, qIdx);\n qualifiers = new Map<string, string>();\n for (const pair of qs.split('&')) {\n const eqIdx = pair.indexOf('=');\n if (eqIdx > 0) {\n qualifiers.set(\n decodeURIComponent(pair.slice(0, eqIdx)),\n decodeURIComponent(pair.slice(eqIdx + 1)),\n );\n }\n }\n }\n\n // Split version\n let version: string | undefined;\n const atIdx = main.lastIndexOf('@');\n if (atIdx > 0) {\n // Must be after the type prefix (i.e., not @scope)\n version = decodePurlSegment(main.slice(atIdx + 1));\n main = main.slice(0, atIdx);\n }\n\n // Split type\n const slashIdx = main.indexOf('/');\n if (slashIdx < 0) return undefined;\n const type = main.slice(0, slashIdx);\n let remainder = main.slice(slashIdx + 1);\n\n // Check for namespace (second /)\n let namespace: string | undefined;\n let name: string;\n const nsSlashIdx = remainder.indexOf('/');\n if (nsSlashIdx >= 0 && type !== 'npm') {\n // Most ecosystems use namespace/name; npm uses @scope/name encoded as %40scope/name\n namespace = decodePurlSegment(remainder.slice(0, nsSlashIdx));\n name = decodePurlSegment(remainder.slice(nsSlashIdx + 1));\n } else if (nsSlashIdx >= 0 && type === 'npm' && remainder.startsWith('%40')) {\n // npm scoped: %40scope/name\n namespace = decodePurlSegment(remainder.slice(0, nsSlashIdx));\n name = decodePurlSegment(remainder.slice(nsSlashIdx + 1));\n } else {\n name = decodePurlSegment(remainder);\n }\n\n if (!type || !name) return undefined;\n\n return {\n type,\n ...(namespace ? { namespace } : {}),\n name,\n ...(version ? { version } : {}),\n ...(qualifiers && qualifiers.size > 0 ? { qualifiers } : {}),\n ...(subpath ? { subpath } : {}),\n };\n}\n\n/**\n * Get the PURL type string for an ecosystem.\n */\nexport function purlTypeForEcosystem(ecosystem: EcosystemId): string {\n return ECOSYSTEM_TO_PURL_TYPE[ecosystem];\n}\n\n/**\n * Get the ecosystem id for a PURL type string.\n * Returns undefined for unknown types.\n */\nexport function ecosystemForPurlType(type: string): EcosystemId | undefined {\n return PURL_TYPE_TO_ECOSYSTEM[type];\n}\n\n/**\n * Construct a PURL string for a dependency in a given ecosystem.\n *\n * This is the spec-required convenience constructor. It composes the richer\n * `buildPurl` primitive with per-ecosystem namespace splitting so callers can\n * pass `name` in the ecosystem-native form (e.g. `\"@types/node\"` for npm\n * scoped packages, or `\"org.springframework/spring-core\"` for Maven groupId).\n *\n * Special-case: Go module paths (`github.com/gorilla/mux`) are written as a\n * single un-encoded name \u2014 the PURL spec treats `/` as the namespace\n * separator for most ecosystems but Go module paths use literal `/`s.\n *\n * @example\n * constructPurl('npm', 'react', '19.1.0') // \u2192 'pkg:npm/react@19.1.0'\n * constructPurl('npm', '@types/node', '22.0.0') // \u2192 'pkg:npm/%40types/node@22.0.0'\n * constructPurl('pypi', 'django', '5.2') // \u2192 'pkg:pypi/django@5.2'\n * constructPurl('maven', 'org.springframework/spring-core', '6.2.7')\n * // \u2192 'pkg:maven/org.springframework/spring-core@6.2.7'\n * constructPurl('go', 'github.com/gorilla/mux', '1.8.1')\n * // \u2192 'pkg:golang/github.com/gorilla/mux@1.8.1'\n */\nexport function constructPurl(\n ecosystem: EcosystemId,\n name: string,\n version?: string,\n): string {\n const type = purlTypeForEcosystem(ecosystem);\n // Go module paths contain literal slashes that are part of the name, not\n // a namespace separator. `buildPurl` encodes `/` in segments per the PURL\n // spec, but the Go ecosystem is the documented exception: module paths\n // like `github.com/gorilla/mux` stay literal. Build the Go PURL string\n // directly so no `/` encoding is applied.\n if (ecosystem === 'go') {\n const versionSuffix = version !== undefined ? `@${encodePurlSegment(version)}` : '';\n return `pkg:${type}/${name}${versionSuffix}`;\n }\n const slashIdx = name.indexOf('/');\n // npm scoped packages: '@scope/name' \u2192 namespace='@scope', name='name'.\n // Maven coordinates: 'groupId/artifactId' \u2192 namespace=groupId, name=artifactId.\n if (slashIdx > 0) {\n const namespace = name.slice(0, slashIdx);\n const pkgName = name.slice(slashIdx + 1);\n if (pkgName.length > 0) {\n return buildPurl({\n type,\n namespace,\n name: pkgName,\n ...(version !== undefined ? { version } : {}),\n });\n }\n }\n return buildPurl({\n type,\n name,\n ...(version !== undefined ? { version } : {}),\n });\n}\n\n/**\n * Parse a PURL string back into the ecosystem-shaped parts used by the\n * TechStack dependency model: ecosystem id, package name, and optional version.\n *\n * The npm scope and Maven groupId are folded back into `name` so the returned\n * shape matches `constructPurl`'s inputs (round-trippable).\n *\n * Returns `undefined` for malformed PURLs or PURL types that don't map to a\n * TechStack ecosystem.\n *\n * @example\n * parsePurl('pkg:npm/%40types/node@22.0.0') // \u2192 { ecosystem:'npm', name:'@types/node', version:'22.0.0' }\n * parsePurl('pkg:pypi/django@5.2') // \u2192 { ecosystem:'python', name:'django', version:'5.2' }\n * parsePurl('pkg:npm/react') // \u2192 { ecosystem:'npm', name:'react' }\n * parsePurl('not-a-purl') // \u2192 undefined\n */\nexport interface ParsedEcosystemPurl {\n readonly ecosystem: EcosystemId;\n readonly name: string;\n readonly version?: string | undefined;\n}\n\nexport function parsePurlEcosystem(purl: string): ParsedEcosystemPurl | undefined {\n const parts = parsePurl(purl);\n if (!parts) return undefined;\n const ecosystem = ecosystemForPurlType(parts.type);\n if (!ecosystem) return undefined;\n const namespace = parts.namespace;\n const name = namespace ? `${namespace}/${parts.name}` : parts.name;\n return {\n ecosystem,\n name,\n ...(parts.version !== undefined ? { version: parts.version } : {}),\n };\n}\n\n/**\n * Encode a PURL path segment per the spec.\n * Percent-encode characters that are not allowed unencoded.\n */\nfunction encodePurlSegment(segment: string): string {\n // The PURL spec says the value must be percent-encoded as per RFC 3986.\n // In practice, we need to encode: @ / % and other reserved chars.\n return segment\n .replace(/%/g, '%25')\n .replace(/@/g, '%40')\n .replace(/\\//g, '%2F');\n}\n\n/**\n * Decode a PURL path segment.\n */\nfunction decodePurlSegment(segment: string): string {\n try {\n return decodeURIComponent(segment);\n } catch {\n return segment;\n }\n}\n", "/**\n * TechStack \u2014 Workspace discovery wrapper.\n *\n * Wraps `detectLanguageWorkspaces()` from `@wrongstack/tools/languages` and\n * maps each `DetectedWorkspace` into a TechStack `Workspace` with:\n * - `EcosystemId` (TechStack's package-manager classification) instead of\n * the tools-package `LanguageProfileId` (which is language-shaped, not\n * package-manager-shaped \u2014 e.g. both `typescript` and `javascript` map to\n * `npm`).\n * - `relativeRoot` (project-relative, portable across machines).\n * - `lockfiles` extracted from the detected evidence (manifest/config/lockfile\n * kinds), independent of which evidence manifests were used.\n * - `coverage` classified against the SDD \u00A76 ecosystem support matrix\n * (Tier A \u2192 `full`, Tier B \u2192 `partial`, Tier C/unsupported \u2192 `unsupported`).\n *\n * Languages with no `EcosystemId` mapping (`deno`, `shell`) are dropped from\n * the result \u2014 they cannot be inventoried by the TechStack pipeline.\n *\n * @see docs/specs/techstack-sdd.md \u00A73.2, \u00A76\n */\n\nimport { detectLanguageWorkspaces } from '@wrongstack/tools/languages';\nimport type {\n DetectLanguageOptions,\n DetectedWorkspace,\n LanguageEvidence,\n LanguageProfileId,\n} from '@wrongstack/tools/languages';\nimport type { Coverage, EcosystemId, Workspace } from '../types.js';\n\n/**\n * Map a language profile id (from `@wrongstack/tools/languages`) to a TechStack\n * ecosystem id. Returns `undefined` for languages that don't correspond to a\n * package-manager ecosystem (e.g. `deno`, `shell`).\n *\n * `java` is disambiguated by gradle-vs-maven evidence: if any lockfile\n * detector matches `gradle.lockfile` we return `gradle`, otherwise `maven`.\n */\nconst STATIC_LANGUAGE_TO_ECOSYSTEM: Readonly<Record<LanguageProfileId, EcosystemId | undefined>> = {\n typescript: 'npm',\n javascript: 'npm',\n deno: undefined,\n python: 'python',\n go: 'go',\n rust: 'rust',\n csharp: 'dotnet',\n php: 'php',\n ruby: 'ruby',\n swift: 'swift',\n dart: 'dart',\n elixir: 'elixir',\n c: 'cpp',\n cpp: 'cpp',\n java: 'maven', // overridden by `resolveJavaEcosystem` when gradle evidence is present\n shell: undefined,\n};\n\n/**\n * Tier classification per SDD \u00A76.\n *\n * Tier A (full deterministic support: inventory + registry + advisory):\n * npm, python, rust, go, dotnet, php, dart\n * Tier B (partial: best-effort inventory + OSV, may not have rich registry):\n * maven, gradle, ruby, swift, elixir\n * Tier C (best-effort only \u2014 listed here for completeness; same as\n * `unsupported` in the Coverage union because the SDD defines coverage as\n * \"full / partial / unsupported\"):\n * cpp\n */\nconst ECOSYSTEM_TIER: Readonly<Record<EcosystemId, Coverage>> = {\n npm: 'full',\n python: 'full',\n rust: 'full',\n go: 'full',\n dotnet: 'full',\n php: 'full',\n dart: 'full',\n maven: 'partial',\n gradle: 'partial',\n ruby: 'partial',\n swift: 'partial',\n elixir: 'partial',\n cpp: 'unsupported',\n};\n\nfunction resolveJavaEcosystem(evidence: readonly LanguageEvidence[]): 'maven' | 'gradle' {\n for (const item of evidence) {\n if (item.kind === 'lockfile' && item.value === 'gradle.lockfile') return 'gradle';\n if (item.kind === 'manifest' && (item.value === 'build.gradle' || item.value === 'build.gradle.kts' || item.value === 'settings.gradle')) {\n return 'gradle';\n }\n }\n return 'maven';\n}\n\nfunction ecosystemForWorkspace(detected: DetectedWorkspace): EcosystemId | undefined {\n if (detected.language === 'java') {\n return resolveJavaEcosystem(detected.evidence);\n }\n return STATIC_LANGUAGE_TO_ECOSYSTEM[detected.language];\n}\n\nfunction extractLockfiles(evidence: readonly LanguageEvidence[]): string[] {\n const seen = new Set<string>();\n const out: string[] = [];\n for (const item of evidence) {\n if (item.kind !== 'lockfile') continue;\n if (seen.has(item.path)) continue;\n seen.add(item.path);\n out.push(item.path);\n }\n return out.sort();\n}\n\n/**\n * Map a single `DetectedWorkspace` to a TechStack `Workspace`.\n * Returns `undefined` when the language has no TechStack ecosystem mapping.\n */\nexport function mapDetectedWorkspace(\n detected: DetectedWorkspace,\n projectRoot: string,\n): Workspace | undefined {\n const ecosystem = ecosystemForWorkspace(detected);\n if (!ecosystem) return undefined;\n const relativeRoot =\n detected.root === projectRoot\n ? '.'\n : detected.root.startsWith(`${projectRoot}/`) || detected.root.startsWith(`${projectRoot}\\\\`)\n ? detected.root.slice(projectRoot.length + 1)\n : detected.root;\n const lockfiles = extractLockfiles(detected.evidence);\n return {\n id: detected.id,\n relativeRoot,\n ecosystem,\n ...(detected.packageManager ? { packageManager: detected.packageManager } : {}),\n manifests: [...detected.manifests].sort(),\n lockfiles,\n confidence: Math.max(0, Math.min(1, detected.confidence)),\n coverage: ECOSYSTEM_TIER[ecosystem],\n };\n}\n\n/**\n * Directory names whose manifests are test scaffolding, not real project\n * dependencies. A `package.json` under `tests/fixtures/` describes a fake\n * project used to exercise a scanner \u2014 inventorying it would surface\n * deliberately-outdated pins (e.g. a fixture pinning `zod@^3`) as findings\n * against the real project. Matched by basename anywhere in the tree;\n * merged with any caller-provided `ignoredDirectories`.\n */\nconst TEST_FIXTURE_DIRECTORIES: readonly string[] = [\n 'fixtures',\n '__fixtures__',\n 'test-fixtures',\n 'testdata',\n '__mocks__',\n];\n\n/**\n * Discover all TechStack workspaces under `projectRoot` by delegating to\n * `detectLanguageWorkspaces` and mapping each detected workspace.\n *\n * Workspaces whose language does not map to a TechStack ecosystem are\n * silently dropped (e.g. `deno`, `shell`) \u2014 they cannot be inventoried and\n * would only inflate coverage counts with `unsupported` entries that add no\n * value at the inventory-engine boundary.\n *\n * Test-fixture directories (`fixtures`, `testdata`, \u2026) are skipped so fake\n * fixture manifests never enter the inventory; the project root itself is\n * never skipped, so scanning a fixture directly (as tests do) still works.\n *\n * Results are sorted by `(ecosystem, relativeRoot, id)` for stable display.\n */\nexport async function discoverWorkspaces(\n projectRoot: string,\n options?: Omit<DetectLanguageOptions, 'projectRoot'>,\n): Promise<Workspace[]> {\n const result = await detectLanguageWorkspaces({\n ...(options ?? {}),\n projectRoot,\n ignoredDirectories: [...TEST_FIXTURE_DIRECTORIES, ...(options?.ignoredDirectories ?? [])],\n });\n const mapped: Workspace[] = [];\n for (const detected of result.workspaces) {\n const workspace = mapDetectedWorkspace(detected, result.projectRoot);\n if (workspace) mapped.push(workspace);\n }\n mapped.sort((a, b) => {\n return (\n a.ecosystem.localeCompare(b.ecosystem) ||\n a.relativeRoot.localeCompare(b.relativeRoot) ||\n a.id.localeCompare(b.id)\n );\n });\n return mapped;\n}\n\n/**\n * Coverage for a workspace's ecosystem \u2014 the per-workspace view of the\n * SDD \u00A76 tier matrix.\n */\nexport function coverageForEcosystem(ecosystem: EcosystemId): Coverage {\n return ECOSYSTEM_TIER[ecosystem];\n}", "/**\n * TechStack \u2014 npm ecosystem adapter.\n *\n * Parses package.json manifests and pnpm-lock.yaml (or package-lock.json /\n * yarn.lock) to produce DependencyObservation[] for Node.js workspaces.\n *\n * Supports: pnpm, npm, yarn, bun \u2014 determined by lockfile presence.\n *\n * @see docs/specs/techstack-sdd.md \u00A76 Tier A\n */\n\nimport { existsSync, readFileSync } from 'node:fs';\nimport { dirname, join, relative, resolve } from 'node:path';\nimport type {\n DependencyObservation,\n DependencyScope,\n DependencyStatus,\n Evidence,\n EcosystemId,\n Workspace,\n} from '../types.js';\nimport type {\n EcosystemAdapter,\n InventoryOptions,\n} from './interface.js';\nimport { resolveIn, workspaceRoot } from './paths.js';\nimport { buildPurl } from '../registry/purl.js';\n\n// \u2500\u2500 Lockfile types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ntype LockfileKind = 'pnpm' | 'npm' | 'yarn' | 'bun' | 'none';\n\ninterface LockfileInfo {\n readonly kind: LockfileKind;\n readonly path: string;\n}\n\n// \u2500\u2500 package.json shape \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ninterface PackageJsonDeps {\n readonly [packageName: string]: string;\n}\n\ninterface PackageJson {\n readonly name?: string | undefined;\n readonly dependencies?: PackageJsonDeps | undefined;\n readonly devDependencies?: PackageJsonDeps | undefined;\n readonly peerDependencies?: PackageJsonDeps | undefined;\n readonly optionalDependencies?: PackageJsonDeps | undefined;\n}\n\n// \u2500\u2500 Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Locate the lockfile that governs a workspace.\n *\n * Walks up from the workspace to `stopAt` (the project root). In a pnpm or npm\n * workspace only the repo root holds a lockfile \u2014 `packages/cli` has none \u2014 so\n * looking only in the workspace directory finds nothing for every package but\n * the root, and every dependency ends up with no resolved version.\n */\nfunction detectLockfile(workspaceDir: string, stopAt?: string): LockfileInfo {\n const candidates: Array<{ file: string; kind: LockfileKind }> = [\n { file: 'pnpm-lock.yaml', kind: 'pnpm' },\n { file: 'package-lock.json', kind: 'npm' },\n { file: 'yarn.lock', kind: 'yarn' },\n { file: 'bun.lockb', kind: 'bun' },\n ];\n\n const ceiling = stopAt ? resolve(stopAt) : undefined;\n let dir = resolve(workspaceDir);\n\n for (;;) {\n for (const c of candidates) {\n const candidate = join(dir, c.file);\n if (existsSync(candidate)) return { kind: c.kind, path: candidate };\n }\n if (ceiling && dir === ceiling) break;\n const parent = dirname(dir);\n if (parent === dir) break; // filesystem root\n // Without a ceiling, don't wander above the workspace at all.\n if (!ceiling) break;\n dir = parent;\n }\n return { kind: 'none', path: '' };\n}\n\n/** Strip pnpm's peer-dependency suffix: `19.1.0(react@19.1.0)` \u2192 `19.1.0`. */\nfunction stripPeerSuffix(version: string): string {\n const paren = version.indexOf('(');\n return (paren === -1 ? version : version.slice(0, paren)).trim();\n}\n\n/**\n * Extract the resolved versions pnpm recorded for one importer (workspace).\n *\n * pnpm's `importers:` section maps each workspace to the exact version it\n * resolved for every declared dependency \u2014 which is precisely the per-workspace\n * question this adapter asks, and it stays correct when two workspaces pin\n * different versions of the same package.\n *\n * Line-based on purpose: the lockfile is machine-generated with a stable\n * 2-space indent, and pulling in a YAML parser for four fields isn't worth the\n * dependency.\n *\n * ```yaml\n * importers:\n * packages/cli: # 2 spaces \u2014 importer\n * dependencies: # 4 spaces \u2014 section\n * react: # 6 spaces \u2014 package\n * specifier: ^19.0.0 # 8 spaces \u2014 fields\n * version: 19.1.0\n * ```\n */\nfunction parsePnpmImporterVersions(lockContent: string, importerPath: string): Map<string, string> {\n const versions = new Map<string, string>();\n const lines = lockContent.split(/\\r?\\n/);\n\n let inImporters = false;\n let inTargetImporter = false;\n let currentPackage: string | undefined;\n\n for (const raw of lines) {\n if (raw.trim() === '' || raw.trimStart().startsWith('#')) continue;\n\n // Top-level key ends the importers block.\n if (!/^\\s/.test(raw)) {\n if (inImporters) break;\n inImporters = raw.startsWith('importers:');\n continue;\n }\n if (!inImporters) continue;\n\n const indent = raw.length - raw.trimStart().length;\n const line = raw.trim();\n\n if (indent === 2) {\n // New importer \u2014 `packages/cli:` or `.:`\n const key = line.endsWith(':') ? unquote(line.slice(0, -1)) : undefined;\n inTargetImporter = key === importerPath;\n currentPackage = undefined;\n continue;\n }\n if (!inTargetImporter) continue;\n\n if (indent === 4) {\n currentPackage = undefined; // dependencies: / devDependencies: / \u2026\n continue;\n }\n if (indent === 6 && line.endsWith(':')) {\n currentPackage = unquote(line.slice(0, -1));\n continue;\n }\n if (indent >= 8 && currentPackage && line.startsWith('version:')) {\n const version = stripPeerSuffix(unquote(line.slice('version:'.length).trim()));\n // `link:../core` is a workspace link, not a released version \u2014 recording\n // it as `locked` would make a local package look like a registry one.\n if (version && !version.startsWith('link:') && !version.startsWith('file:')) {\n versions.set(currentPackage, version);\n }\n currentPackage = undefined;\n }\n }\n\n return versions;\n}\n\nfunction unquote(value: string): string {\n const trimmed = value.trim();\n if (\n (trimmed.startsWith(\"'\") && trimmed.endsWith(\"'\")) ||\n (trimmed.startsWith('\"') && trimmed.endsWith('\"'))\n ) {\n return trimmed.slice(1, -1);\n }\n return trimmed;\n}\n\n/**\n * Parse package-lock.json (npm) to extract resolved versions.\n */\nfunction parseNpmLockVersions(lockContent: string): Map<string, string> {\n const versions = new Map<string, string>();\n try {\n const lock = JSON.parse(lockContent);\n // npm v3: lock.dependencies\n const deps = lock.dependencies ?? {};\n for (const [name, info] of Object.entries(deps)) {\n const depInfo = info as { version?: string };\n if (depInfo.version) {\n // Strip version prefixes like ^, ~, >=\n const cleanVersion = depInfo.version.replace(/^[^0-9]+/, '');\n versions.set(name, cleanVersion);\n }\n }\n // npm v2 (lockfileVersion 2+): lock.packages\n const packages = lock.packages ?? {};\n for (const key of Object.keys(packages)) {\n const pkgInfo = packages[key] as { version?: string };\n if (pkgInfo.version) {\n // Key format: \"node_modules/package-name\" or \"node_modules/@scope/package-name\"\n const name = key.replace(/^node_modules\\//, '');\n if (!versions.has(name)) {\n versions.set(name, pkgInfo.version);\n }\n }\n }\n } catch {\n // Malformed lockfile \u2014 return empty map\n }\n return versions;\n}\n\n/**\n * Create a manifest evidence entry.\n */\nfunction manifestEvidence(path: string): Evidence {\n return {\n kind: 'manifest',\n source: path,\n retrievedAt: new Date().toISOString(),\n };\n}\n\n/**\n * Create a lockfile evidence entry.\n */\nfunction lockfileEvidence(path: string): Evidence {\n return {\n kind: 'lockfile',\n source: path,\n retrievedAt: new Date().toISOString(),\n };\n}\n\n/**\n * Determine the dependency scope from the manifest section it appears in.\n */\nfunction scopeForSection(section: string): DependencyScope {\n switch (section) {\n case 'dependencies':\n return 'runtime';\n case 'devDependencies':\n return 'development';\n case 'peerDependencies':\n return 'peer';\n case 'optionalDependencies':\n return 'optional';\n default:\n return 'runtime';\n }\n}\n\n/**\n * Determine status: local_path for file: / link: / workspace:,\n * git_dependency for git+ / github: / git:, registry otherwise.\n */\nfunction statusForSpec(spec: string): DependencyStatus {\n if (spec.startsWith('file:') || spec.startsWith('link:') || spec.startsWith('workspace:')) {\n return 'local_path';\n }\n if (spec.startsWith('git+') || spec.startsWith('github:') || spec.startsWith('git:')) {\n return 'git_dependency';\n }\n return 'current';\n}\n\n/**\n * Check if a spec is a local/git reference (not resolvable to a registry version).\n */\nfunction isRegistrySpec(spec: string): boolean {\n return (\n !spec.startsWith('file:') &&\n !spec.startsWith('link:') &&\n !spec.startsWith('workspace:') &&\n !spec.startsWith('git+') &&\n !spec.startsWith('github:') &&\n !spec.startsWith('git:')\n );\n}\n\n// \u2500\u2500 Adapter \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport class NpmAdapter implements EcosystemAdapter {\n readonly ecosystem: EcosystemId = 'npm';\n\n async inventory(\n workspace: Workspace,\n options: InventoryOptions,\n ): Promise<readonly DependencyObservation[]> {\n const observations: DependencyObservation[] = [];\n const root = workspaceRoot(workspace, options);\n // Pick the actual package.json rather than `manifests[0]` \u2014 discovery also\n // reports tsconfig.json as a manifest, and sort order is not a contract.\n const manifestPath = resolveIn(\n root,\n workspace.manifests.find((m) => m.endsWith('package.json')) ?? 'package.json',\n );\n\n // Read package.json\n let pkg: PackageJson;\n let manifestContent: string;\n try {\n manifestContent = readFileSync(manifestPath, 'utf-8');\n pkg = JSON.parse(manifestContent) as PackageJson;\n } catch {\n return []; // Can't read manifest \u2014 no dependencies\n }\n\n const manifestEv = manifestEvidence(manifestPath);\n\n // Read lockfile for resolved versions\n const lockInfo = detectLockfile(root, options.projectRoot);\n const resolvedVersions = new Map<string, string>();\n let lockEv: Evidence | undefined;\n if (lockInfo.kind === 'pnpm') {\n try {\n const lockContent = readFileSync(lockInfo.path, 'utf-8');\n // The importer key is this workspace's path relative to the lockfile,\n // POSIX-style; the root workspace is `.`.\n const importerPath =\n relative(dirname(lockInfo.path), root).split(/[/\\\\]/).filter(Boolean).join('/') || '.';\n const parsed = parsePnpmImporterVersions(lockContent, importerPath);\n for (const [k, v] of parsed) resolvedVersions.set(k, v);\n if (parsed.size > 0) lockEv = lockfileEvidence(lockInfo.path);\n } catch {\n // ignore\n }\n } else if (lockInfo.kind === 'npm') {\n try {\n const lockContent = readFileSync(lockInfo.path, 'utf-8');\n const parsed = parseNpmLockVersions(lockContent);\n for (const [k, v] of parsed) resolvedVersions.set(k, v);\n lockEv = lockfileEvidence(lockInfo.path);\n } catch {\n // ignore\n }\n }\n\n // Process each dependency section\n const sections: Array<{ name: string; deps: PackageJsonDeps | undefined }> = [\n { name: 'dependencies', deps: pkg.dependencies },\n { name: 'devDependencies', deps: pkg.devDependencies },\n { name: 'peerDependencies', deps: pkg.peerDependencies },\n { name: 'optionalDependencies', deps: pkg.optionalDependencies },\n ];\n\n const seen = new Set<string>(); // dedup within workspace\n\n for (const section of sections) {\n if (!section.deps) continue;\n const scope = scopeForSection(section.name);\n\n for (const [name, requested] of Object.entries(section.deps)) {\n const dedupKey = `${name}`;\n if (seen.has(dedupKey)) continue;\n seen.add(dedupKey);\n\n const isRegistry = isRegistrySpec(requested);\n const status = statusForSpec(requested);\n\n // Resolve locked version from lockfile\n const locked = resolvedVersions.get(name);\n\n // Build PURL for registry deps\n const purl = isRegistry && locked\n ? buildPurl({ type: 'npm', name, version: locked })\n : isRegistry\n ? buildPurl({ type: 'npm', name })\n : undefined;\n\n const evidence: Evidence[] = [manifestEv];\n if (lockEv && locked) evidence.push(lockEv);\n\n observations.push({\n id: `dep-${workspace.id}-${name}`,\n workspaceId: workspace.id,\n ...(purl ? { purl } : {}),\n ecosystem: 'npm' as const,\n name,\n sourceType: isRegistry ? 'registry' : status === 'local_path' ? 'path' : 'git',\n direct: true,\n scope,\n requested,\n ...(locked ? { locked } : {}),\n status,\n evidence,\n });\n }\n }\n\n // Parse transitive dependencies from lockfile if requested\n // (Phase 1 enhancement: includeTransitive option)\n\n return observations;\n }\n}\n\n/**\n * Default singleton instance.\n */\nexport const npmAdapter = new NpmAdapter();\n", "/**\n * TechStack \u2014 adapter path resolution.\n *\n * `Workspace.relativeRoot` is deliberately project-relative: snapshots are\n * persisted and shipped to the browser, so they must stay portable across\n * machines. Adapters, however, have to actually open files, and a relative\n * root resolves against `process.cwd()` \u2014 which is only the project root by\n * coincidence. The server can be started from anywhere, and switching projects\n * mid-session doesn't move `cwd` at all.\n *\n * So the absolute base travels alongside the workspace, via\n * `InventoryOptions.projectRoot`, instead of being baked into the persisted\n * type.\n *\n * @see docs/specs/techstack-sdd.md \u00A73.2\n */\n\nimport { resolve } from 'node:path';\nimport type { Workspace } from '../types.js';\nimport type { InventoryOptions } from './interface.js';\n\n/**\n * Absolute filesystem root of a workspace.\n *\n * Falls back to resolving against `cwd` when no `projectRoot` is supplied, to\n * keep older direct callers working.\n */\nexport function workspaceRoot(workspace: Workspace, options: InventoryOptions): string {\n const relative = workspace.relativeRoot || '.';\n return options.projectRoot ? resolve(options.projectRoot, relative) : resolve(relative);\n}\n\n/**\n * Resolve a manifest/lockfile path that may arrive either absolute (as\n * `Workspace.manifests` entries do, straight from discovery) or as a bare\n * filename (as adapter fallbacks use).\n *\n * Use this instead of `join`: `join('/a/b', '/a/b/package.json')` yields\n * `/a/b/a/b/package.json`, which is exactly the bug that silently emptied the\n * npm, Go, and Rust inventories \u2014 every workspace threw on read and the\n * engine's `catch { deps = [] }` swallowed it.\n */\nexport function resolveIn(root: string, candidate: string): string {\n return resolve(root, candidate);\n}\n", "/**\n * TechStack \u2014 Python ecosystem adapter.\n *\n * Parses pyproject.toml (PEP 621), requirements.txt, and Pipfile to produce\n * DependencyObservation[] for Python workspaces.\n *\n * Supports: pip, pipenv, poetry, uv \u2014 determined by manifest/lockfile presence.\n */\n\nimport { readFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type {\n DependencyObservation,\n DependencyScope,\n Evidence,\n EcosystemId,\n Workspace,\n} from '../types.js';\nimport type {\n EcosystemAdapter,\n InventoryOptions,\n} from './interface.js';\nimport { workspaceRoot } from './paths.js';\nimport { buildPurl } from '../registry/purl.js';\n\n// \u2500\u2500 Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction manifestEvidence(path: string): Evidence {\n return { kind: 'manifest', source: path, retrievedAt: new Date().toISOString() };\n}\n\nfunction lockfileEvidence(path: string): Evidence {\n return { kind: 'lockfile', source: path, retrievedAt: new Date().toISOString() };\n}\n\n// \u2500\u2500 Minimal TOML parser (line-based, sufficient for pyproject.toml) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ninterface TomlSection {\n readonly name: string;\n readonly lines: string[];\n}\n\nfunction parseTomlSections(content: string): TomlSection[] {\n const sections: TomlSection[] = [];\n let currentSection = '__header__';\n let currentLines: string[] = [];\n for (const raw of content.split('\\n')) {\n const line = raw.trim();\n if (line.startsWith('#') || line === '') continue;\n const sectionMatch = line.match(/^\\[([^\\]]+)\\]$/);\n if (sectionMatch) {\n if (currentLines.length > 0) sections.push({ name: currentSection, lines: currentLines });\n currentSection = sectionMatch[1]!;\n currentLines = [];\n } else {\n currentLines.push(raw);\n }\n }\n if (currentLines.length > 0) sections.push({ name: currentSection, lines: currentLines });\n return sections;\n}\n\nfunction extractTomlArray(sectionLines: string[], key: string): string[] {\n const result: string[] = [];\n let inArray = false;\n for (const line of sectionLines) {\n const trimmed = line.trim();\n if (!inArray) {\n const match = trimmed.match(new RegExp(`^${key}\\\\s*=\\\\s*\\\\[`));\n if (match) {\n inArray = true;\n const rest = trimmed.slice(match[0].length);\n if (rest.includes(']')) {\n const items = rest.replace(/\\]\\s*,?\\s*$/, '').trim();\n for (const item of items.split(',')) {\n const cleaned = item.trim().replace(/^\"|\"$/g, '').trim();\n if (cleaned) result.push(cleaned);\n }\n inArray = false;\n }\n }\n } else {\n const closeIdx = trimmed.indexOf(']');\n if (closeIdx >= 0) {\n const items = trimmed.slice(0, closeIdx).trim();\n for (const item of items.split(',')) {\n const cleaned = item.trim().replace(/^\"|\"$/g, '').trim();\n if (cleaned) result.push(cleaned);\n }\n inArray = false;\n } else {\n const cleaned = trimmed.replace(/,$/, '').trim().replace(/^\"|\"$/g, '').trim();\n if (cleaned) result.push(cleaned);\n }\n }\n }\n return result;\n}\n\nfunction parsePep508(spec: string): { name: string; constraint: string | undefined } {\n let s = spec.trim();\n s = s.replace(/\\[.*?\\]/g, '');\n const match = s.match(/^([a-zA-Z0-9][a-zA-Z0-9._-]*)\\s*(.*)$/);\n if (!match) return { name: s, constraint: undefined };\n return { name: match[1]!, constraint: match[2]?.trim() || undefined };\n}\n\n// \u2500\u2500 pyproject.toml parser (PEP 621) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction parsePyprojectDeps(content: string): Array<{ name: string; constraint: string | undefined; scope: DependencyScope }> {\n const deps: Array<{ name: string; constraint: string | undefined; scope: DependencyScope }> = [];\n const sections = parseTomlSections(content);\n\n const projectSection = sections.find((s) => s.name === 'project');\n if (projectSection) {\n const depSpecs = extractTomlArray(projectSection.lines, 'dependencies');\n for (const spec of depSpecs) {\n const { name, constraint } = parsePep508(spec);\n if (name) deps.push({ name, constraint, scope: 'runtime' });\n }\n }\n\n for (const section of sections) {\n if (section.name === 'project.optional-dependencies') {\n // Each line is: group_name = [\"dep1\", \"dep2\", ...]\n for (const line of section.lines) {\n const trimmed = line.trim();\n const groupMatch = trimmed.match(/^([a-zA-Z0-9_-]+)\\s*=\\s*\\[/);\n if (groupMatch) {\n const depSpecs = extractTomlArray(section.lines, groupMatch[1]!);\n for (const spec of depSpecs) {\n const { name, constraint } = parsePep508(spec);\n if (name) deps.push({ name, constraint, scope: 'optional' });\n }\n }\n }\n }\n }\n\n return deps;\n}\n\n// \u2500\u2500 requirements.txt parser \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction parseRequirementsTxt(content: string): Array<{ name: string; constraint: string | undefined }> {\n const deps: Array<{ name: string; constraint: string | undefined }> = [];\n for (const raw of content.split('\\n')) {\n const line = raw.trim();\n if (!line || line.startsWith('#') || line.startsWith('-')) continue;\n const { name, constraint } = parsePep508(line);\n if (name) deps.push({ name, constraint });\n }\n return deps;\n}\n\n// \u2500\u2500 Pipfile parser \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction parsePipfileDeps(content: string): Array<{ name: string; constraint: string | undefined; scope: DependencyScope }> {\n const deps: Array<{ name: string; constraint: string | undefined; scope: DependencyScope }> = [];\n const sections = parseTomlSections(content);\n for (const section of sections) {\n const scope: DependencyScope = section.name === 'dev-packages' ? 'development' : 'runtime';\n for (const line of section.lines) {\n const trimmed = line.trim();\n if (trimmed.startsWith('#')) continue;\n const match = trimmed.match(/^([a-zA-Z0-9][a-zA-Z0-9._-]*)\\s*=\\s*\"([^\"]*)\"$/);\n if (match) {\n const constraint = match[2]! === '*' ? undefined : match[2]!;\n deps.push({ name: match[1]!, constraint, scope });\n }\n }\n }\n return deps;\n}\n\nfunction parseRequirementsLockVersions(content: string): Map<string, string> {\n const versions = new Map<string, string>();\n for (const raw of content.split('\\n')) {\n const line = raw.trim();\n if (!line || line.startsWith('#') || line.startsWith('-')) continue;\n const match = line.match(/^([a-zA-Z0-9][a-zA-Z0-9._-]*)\\s*==\\s*([^\\s;]+)/);\n if (match) versions.set(match[1]!, match[2]!);\n }\n return versions;\n}\n\n/**\n * Parse poetry.lock to extract resolved versions.\n * Format:\n * [[package]]\n * name = \"flask\"\n * version = \"3.0.3\"\n */\nexport function parsePoetryLock(content: string): Map<string, string> {\n const versions = new Map<string, string>();\n let currentName: string | undefined;\n for (const raw of content.split('\\n')) {\n const line = raw.trim();\n const nameMatch = line.match(/^name\\s*=\\s*\"([^\"]+)\"/);\n if (nameMatch) {\n currentName = nameMatch[1]!;\n continue;\n }\n const versionMatch = line.match(/^version\\s*=\\s*\"([^\"]+)\"/);\n if (versionMatch && currentName) {\n versions.set(normalizePkgName(currentName), versionMatch[1]!);\n currentName = undefined;\n }\n }\n return versions;\n}\n\n/**\n * Normalize Python package names per PEP 503:\n * underscores and hyphens are equivalent, all lowercase.\n */\nfunction normalizePkgName(name: string): string {\n return name.toLowerCase().replace(/_/g, '-');\n}\n\n// \u2500\u2500 Adapter \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport class PythonAdapter implements EcosystemAdapter {\n readonly ecosystem: EcosystemId = 'python';\n\n async inventory(workspace: Workspace, options: InventoryOptions): Promise<readonly DependencyObservation[]> {\n const observations: DependencyObservation[] = [];\n const root = workspaceRoot(workspace, options);\n const seen = new Set<string>();\n\n const hasPyproject = workspace.manifests.some((m) => m.includes('pyproject.toml')) || this.fileExists(join(root, 'pyproject.toml'));\n const hasRequirements = workspace.manifests.some((m) => m.includes('requirements.txt')) || this.fileExists(join(root, 'requirements.txt'));\n const hasPipfile = workspace.manifests.some((m) => m.includes('Pipfile')) || this.fileExists(join(root, 'Pipfile'));\n\n const lockfilePath = this.detectLockfile(root);\n\n let allDeps: Array<{ name: string; constraint: string | undefined; scope: DependencyScope; source: string }> = [];\n let pyprojectEv: Evidence | undefined;\n\n if (hasPyproject) {\n try {\n const content = readFileSync(join(root, 'pyproject.toml'), 'utf-8');\n pyprojectEv = manifestEvidence(join(root, 'pyproject.toml'));\n const parsed = parsePyprojectDeps(content);\n for (const d of parsed) allDeps.push({ ...d, source: 'pyproject.toml' });\n } catch { /* ignore */ }\n }\n\n let reqLockVersions = new Map<string, string>();\n let requirementsEv: Evidence | undefined;\n\n if (hasRequirements) {\n try {\n const content = readFileSync(join(root, 'requirements.txt'), 'utf-8');\n requirementsEv = manifestEvidence(join(root, 'requirements.txt'));\n const parsed = parseRequirementsTxt(content);\n for (const d of parsed) {\n if (!allDeps.some((existing) => existing.name === d.name)) {\n allDeps.push({ ...d, scope: 'runtime', source: 'requirements.txt' });\n }\n }\n reqLockVersions = parseRequirementsLockVersions(content);\n } catch { /* ignore */ }\n }\n\n if (hasPipfile) {\n try {\n const content = readFileSync(join(root, 'Pipfile'), 'utf-8');\n if (!pyprojectEv) pyprojectEv = manifestEvidence(join(root, 'Pipfile'));\n const parsed = parsePipfileDeps(content);\n for (const d of parsed) {\n if (!allDeps.some((existing) => existing.name === d.name)) {\n allDeps.push({ ...d, source: 'Pipfile' });\n }\n }\n } catch { /* ignore */ }\n }\n\n let lockEv: Evidence | undefined;\n if (lockfilePath) {\n try {\n readFileSync(lockfilePath, 'utf-8');\n lockEv = lockfileEvidence(lockfilePath);\n } catch { /* ignore */ }\n }\n\n const manifestEv = pyprojectEv || requirementsEv;\n\n for (const dep of allDeps) {\n if (seen.has(dep.name)) continue;\n seen.add(dep.name);\n\n const locked = reqLockVersions.get(dep.name) || undefined;\n const isRegistry = !dep.constraint || (!dep.constraint.startsWith('file:') && !dep.constraint.startsWith('git+') && !dep.constraint.startsWith('-e'));\n\n const purl = isRegistry && locked ? buildPurl({ type: 'python', name: dep.name, version: locked })\n : isRegistry ? buildPurl({ type: 'python', name: dep.name }) : undefined;\n\n const evidence: Evidence[] = [];\n if (manifestEv) evidence.push(manifestEv);\n if (lockEv && locked) evidence.push(lockEv);\n if (evidence.length === 0) {\n evidence.push({ kind: 'manifest', source: dep.source, retrievedAt: new Date().toISOString() });\n }\n\n const status: DependencyObservation['status'] =\n dep.constraint && (dep.constraint.startsWith('file:') || dep.constraint.startsWith('-e')) ? 'local_path'\n : dep.constraint?.startsWith('git+') ? 'git_dependency' : 'current';\n\n observations.push({\n id: `dep-${workspace.id}-${dep.name}`,\n workspaceId: workspace.id,\n ...(purl ? { purl } : {}),\n ecosystem: 'python',\n name: dep.name,\n sourceType: isRegistry ? 'registry' : status === 'local_path' ? 'path' : 'git',\n direct: true,\n scope: dep.scope,\n ...(dep.constraint ? { requested: dep.constraint } : {}),\n ...(locked ? { locked } : {}),\n status,\n evidence,\n });\n }\n\n return observations;\n }\n\n private fileExists(filePath: string): boolean {\n try { readFileSync(filePath, 'utf-8'); return true; } catch { return false; }\n }\n\n private detectLockfile(workspaceRoot: string): string | undefined {\n for (const file of ['Pipfile.lock', 'poetry.lock', 'uv.lock']) {\n try { readFileSync(join(workspaceRoot, file), 'utf-8'); return join(workspaceRoot, file); } catch { /* not found */ }\n }\n return undefined;\n }\n}\n\nexport const pythonAdapter = new PythonAdapter();\n", "/**\n * TechStack \u2014 Rust ecosystem adapter.\n *\n * Parses Cargo.toml manifests and Cargo.lock lockfiles to produce\n * DependencyObservation[] for Rust workspaces.\n *\n * @see docs/specs/techstack-sdd.md \u00A76 Tier A\n */\n\nimport { readFileSync } from 'node:fs';\nimport type {\n DependencyObservation,\n DependencyScope,\n Evidence,\n EcosystemId,\n Workspace,\n} from '../types.js';\nimport type {\n EcosystemAdapter,\n InventoryOptions,\n} from './interface.js';\nimport { resolveIn, workspaceRoot } from './paths.js';\nimport { buildPurl } from '../registry/purl.js';\n\n// \u2500\u2500 Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction manifestEvidence(path: string): Evidence {\n return { kind: 'manifest', source: path, retrievedAt: new Date().toISOString() };\n}\n\nfunction lockfileEvidence(path: string): Evidence {\n return { kind: 'lockfile', source: path, retrievedAt: new Date().toISOString() };\n}\n\n// \u2500\u2500 Minimal TOML parser (line-based, sufficient for Cargo.toml + Cargo.lock) \u2500\u2500\n\ninterface TomlSection {\n readonly name: string;\n readonly lines: string[];\n}\n\nfunction parseTomlSections(content: string): TomlSection[] {\n const sections: TomlSection[] = [];\n let currentSection = '__header__';\n let currentLines: string[] = [];\n for (const raw of content.split('\\n')) {\n const line = raw.trim();\n if (line.startsWith('#') || line === '') continue;\n const sectionMatch = line.match(/^\\[([^\\]]+)\\]$/);\n if (sectionMatch) {\n if (currentLines.length > 0) sections.push({ name: currentSection, lines: currentLines });\n currentSection = sectionMatch[1]!;\n currentLines = [];\n } else {\n currentLines.push(raw);\n }\n }\n if (currentLines.length > 0) sections.push({ name: currentSection, lines: currentLines });\n return sections;\n}\n\n/**\n * Parse a key = value pair from a TOML line.\n * Handles: key = \"string\", key = { inline = \"table\" }\n * Returns undefined for lines that are continuations of inline tables.\n */\nfunction parseTomlKeyValue(line: string): { key: string; value: string } | undefined {\n const trimmed = line.trim();\n // Skip inline table continuation lines that start with a key\n if (trimmed.startsWith('#')) return undefined;\n const match = trimmed.match(/^([a-zA-Z0-9_-]+)\\s*=\\s*(.+)$/);\n if (!match) return undefined;\n return { key: match[1]!, value: match[2]!.trim() };\n}\n\n/**\n * Extract simple string key-value pairs from a TOML section.\n * Returns { name, version } for entries like `serde = \"1.0\"` or `serde = { version = \"1.0\", ... }`.\n * Handles both simple and inline-table formats.\n */\nfunction extractTomlDeps(sectionLines: string[]): Array<{ name: string; version: string | undefined }> {\n const deps: Array<{ name: string; version: string | undefined }> = [];\n for (const raw of sectionLines) {\n const line = raw.trim();\n if (line.startsWith('#') || line === '') continue;\n\n // Check if it's an inline table: serde = { version = \"1.0\", features = [...] }\n const tableMatch = line.match(/^([a-zA-Z0-9_-]+)\\s*=\\s*\\{\\s*(.*?)\\s*\\}$/);\n if (tableMatch) {\n const name = tableMatch[1]!;\n const inner = tableMatch[2]!;\n const versionMatch = inner.match(/version\\s*=\\s*\"([^\"]+)\"/);\n deps.push({ name, version: versionMatch ? versionMatch[1]! : undefined });\n continue;\n }\n\n // Simple key = \"value\"\n const simpleMatch = line.match(/^([a-zA-Z0-9_-]+)\\s*=\\s*\"([^\"]*)\"$/);\n if (simpleMatch) {\n deps.push({ name: simpleMatch[1]!, version: simpleMatch[2]! || undefined });\n continue;\n }\n\n // Try partial inline table (may span lines)\n const partialMatch = parseTomlKeyValue(line);\n if (partialMatch && !partialMatch.value.startsWith('{') && !partialMatch.value.startsWith('\"')) {\n // Might be a path or git dep: serde = { path = \"../foo\" }\n // Ignore these for now\n }\n }\n return deps;\n}\n\n/**\n * Parse Cargo.lock format for package entries.\n * Cargo.lock uses TOML format with [[package]] array entries.\n */\nfunction parseCargoLock(content: string): Map<string, string> {\n const versions = new Map<string, string>();\n const lines = content.split('\\n');\n let currentName: string | undefined;\n let currentVersion: string | undefined;\n let inPackage = false;\n\n for (const raw of lines) {\n const line = raw.trim();\n if (line.startsWith('#') || line === '') continue;\n\n if (line.startsWith('[[') && line.includes('package')) {\n // Save previous\n if (inPackage && currentName && currentVersion) {\n versions.set(currentName, currentVersion);\n }\n currentName = undefined;\n currentVersion = undefined;\n inPackage = true;\n continue;\n }\n\n if (inPackage) {\n if (line.startsWith('name')) {\n const m = line.match(/^name\\s*=\\s*\"([^\"]+)\"/);\n if (m) currentName = m[1]!;\n } else if (line.startsWith('version')) {\n const m = line.match(/^version\\s*=\\s*\"([^\"]+)\"/);\n if (m) currentVersion = m[1]!;\n }\n }\n }\n\n // Save last\n if (inPackage && currentName && currentVersion) {\n versions.set(currentName, currentVersion);\n }\n\n return versions;\n}\n\n// \u2500\u2500 Scope mapping \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction scopeForCargoSection(section: string): DependencyScope {\n switch (section) {\n case 'dependencies':\n return 'runtime';\n case 'dev-dependencies':\n return 'development';\n case 'build-dependencies':\n return 'build';\n default:\n return 'runtime';\n }\n}\n\n// \u2500\u2500 Adapter \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport class RustAdapter implements EcosystemAdapter {\n readonly ecosystem: EcosystemId = 'rust';\n\n async inventory(\n workspace: Workspace,\n options: InventoryOptions,\n ): Promise<readonly DependencyObservation[]> {\n const observations: DependencyObservation[] = [];\n const root = workspaceRoot(workspace, options);\n const seen = new Set<string>();\n\n // Find manifests\n const cargoTomlPath = workspace.manifests.find((m) => m.includes('Cargo.toml'))\n || (this.fileExists(resolveIn(root, 'Cargo.toml')) ? 'Cargo.toml' : undefined);\n\n if (!cargoTomlPath) return [];\n\n const fullManifestPath = resolveIn(root, cargoTomlPath);\n let cargoContent: string;\n try {\n cargoContent = readFileSync(fullManifestPath, 'utf-8');\n } catch {\n return [];\n }\n\n const manifestEv = manifestEvidence(fullManifestPath);\n\n // Find lockfile\n const cargoLockPath = resolveIn(root, 'Cargo.lock');\n let lockVersions = new Map<string, string>();\n let lockEv: Evidence | undefined;\n try {\n const lockContent = readFileSync(cargoLockPath, 'utf-8');\n lockVersions = parseCargoLock(lockContent);\n lockEv = lockfileEvidence(cargoLockPath);\n } catch {\n // No lockfile \u2014 that's OK\n }\n\n // Parse dependency sections from Cargo.toml\n const sections = parseTomlSections(cargoContent);\n const depSections = ['dependencies', 'dev-dependencies', 'build-dependencies'];\n\n for (const section of sections) {\n // Cargo.toml has [dependencies], [dev-dependencies], [build-dependencies]\n // Also [target.'cfg(...)'.dependencies] patterns\n const sectionName = section.name;\n let matchedScope: string | undefined;\n\n for (const depSec of depSections) {\n if (sectionName === depSec || sectionName.endsWith(`.${depSec}`)) {\n matchedScope = depSec;\n break;\n }\n }\n\n if (!matchedScope) continue;\n\n const scope = scopeForCargoSection(matchedScope);\n const deps = extractTomlDeps(section.lines);\n\n for (const dep of deps) {\n if (seen.has(dep.name)) continue;\n seen.add(dep.name);\n\n const locked = lockVersions.get(dep.name) || dep.version;\n const isRegistry = !dep.version || (\n !dep.version.startsWith('path=') &&\n !dep.version.startsWith('git=') &&\n !dep.version.startsWith('../')\n );\n\n const purl = isRegistry && locked\n ? buildPurl({ type: 'rust', name: dep.name, version: locked })\n : isRegistry\n ? buildPurl({ type: 'rust', name: dep.name })\n : undefined;\n\n const evidence: Evidence[] = [manifestEv];\n if (lockEv && locked && lockVersions.has(dep.name)) evidence.push(lockEv);\n\n const status: DependencyObservation['status'] =\n dep.version && (dep.version.startsWith('path=') || dep.version.startsWith('git='))\n ? dep.version.startsWith('git=')\n ? 'git_dependency'\n : 'local_path'\n : 'current';\n\n observations.push({\n id: `dep-${workspace.id}-${dep.name}`,\n workspaceId: workspace.id,\n ...(purl ? { purl } : {}),\n ecosystem: 'rust',\n name: dep.name,\n sourceType: isRegistry ? 'registry' : status === 'local_path' ? 'path' : 'git',\n direct: true,\n scope,\n ...(dep.version ? { requested: dep.version } : {}),\n ...(locked ? { locked } : {}),\n status,\n evidence,\n });\n }\n }\n\n return observations;\n }\n\n private fileExists(filePath: string): boolean {\n try {\n readFileSync(filePath, 'utf-8');\n return true;\n } catch {\n return false;\n }\n }\n}\n\n/**\n * Default singleton instance.\n */\nexport const rustAdapter = new RustAdapter();\n", "/**\n * TechStack \u2014 Go ecosystem adapter.\n *\n * Parses go.mod manifests and go.sum to produce DependencyObservation[]\n * for Go workspaces.\n *\n * @see docs/specs/techstack-sdd.md \u00A76 Tier A\n */\n\nimport { readFileSync } from 'node:fs';\nimport type {\n DependencyObservation,\n DependencyScope,\n Evidence,\n EcosystemId,\n Workspace,\n} from '../types.js';\nimport type {\n EcosystemAdapter,\n InventoryOptions,\n} from './interface.js';\nimport { resolveIn, workspaceRoot } from './paths.js';\nimport { buildPurl } from '../registry/purl.js';\n\n// \u2500\u2500 Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction manifestEvidence(path: string): Evidence {\n return { kind: 'manifest', source: path, retrievedAt: new Date().toISOString() };\n}\n\nfunction lockfileEvidence(path: string): Evidence {\n return { kind: 'lockfile', source: path, retrievedAt: new Date().toISOString() };\n}\n\nfunction cleanGoVersion(v: string): string {\n return v.replace(/^v/i, '');\n}\n\n// \u2500\u2500 go.mod parser\n\ninterface GoRequireStmt {\n readonly modulePath: string;\n readonly version: string;\n /** Indirect dependencies (// indirect comment) */\n readonly indirect?: boolean;\n}\n\n/**\n * Parse a go.mod file to extract require statements.\n * Handles:\n * require module/path v1.2.3\n * require (\n * module/path v1.2.3\n * module/other v0.5.0 // indirect\n * )\n * exclude, replace, retract are ignored.\n */\nfunction parseGoMod(content: string): GoRequireStmt[] {\n const deps: GoRequireStmt[] = [];\n const lines = content.split('\\n');\n let inRequireBlock = false;\n\n for (const raw of lines) {\n const line = raw.trim();\n\n // Skip comments and empty lines\n if (line === '' || line.startsWith('//')) continue;\n\n // Track require blocks\n if (line.startsWith('require (') && line.endsWith('(')) {\n inRequireBlock = true;\n continue;\n }\n if (line.startsWith('require ') && !line.includes('(')) {\n // Single-line require\n const m = line.match(/^require\\s+(\\S+)\\s+(\\S+)/);\n if (m) {\n const indirect = raw.includes('// indirect');\n deps.push({ modulePath: m[1]!, version: cleanGoVersion(m[2]!), indirect });\n }\n continue;\n }\n\n if (inRequireBlock) {\n if (line === ')') {\n inRequireBlock = false;\n continue;\n }\n // Module path v1.2.3 // indirect\n const m = line.match(/^(\\S+)\\s+(\\S+)/);\n if (m) {\n const indirect = raw.includes('// indirect');\n deps.push({ modulePath: m[1]!, version: cleanGoVersion(m[2]!), indirect });\n }\n continue;\n }\n\n // Skip exclude/replace/retract blocks\n if (line.startsWith('exclude') || line.startsWith('replace') || line.startsWith('retract')) {\n continue;\n }\n }\n\n return deps;\n}\n\n/**\n * Parse go.sum to extract resolved versions.\n * Format: module_path version h1:hash\n * module_path version/go.mod h1:hash\n */\nfunction parseGoSum(content: string): Map<string, string> {\n const versions = new Map<string, string>();\n for (const raw of content.split('\\n')) {\n const line = raw.trim();\n if (!line) continue;\n // module_path version hash\n const m = line.match(/^(\\S+)\\s+(\\S+)\\s+\\S+/);\n if (m) {\n const modulePath = m[1]!;\n const version = cleanGoVersion(m[2]!);\n // Only set if not already set (first occurrence wins)\n if (!versions.has(modulePath)) {\n // Skip pseudo-versions like v0.0.0-20240701012345-abcdef\n versions.set(modulePath, version);\n }\n }\n }\n return versions;\n}\n\n/**\n * Extract the module name from go.mod (go module statement).\n */\nfunction parseGoModuleName(content: string): string | undefined {\n for (const raw of content.split('\\n')) {\n const line = raw.trim();\n const m = line.match(/^module\\s+(\\S+)/);\n if (m) return m[1]!;\n }\n return undefined;\n}\n\n// \u2500\u2500 Adapter \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport class GoAdapter implements EcosystemAdapter {\n readonly ecosystem: EcosystemId = 'go';\n\n async inventory(\n workspace: Workspace,\n options: InventoryOptions,\n ): Promise<readonly DependencyObservation[]> {\n const observations: DependencyObservation[] = [];\n const root = workspaceRoot(workspace, options);\n const seen = new Set<string>();\n\n // Find go.mod\n const goModPath = workspace.manifests.find((m) => m.includes('go.mod'))\n || (this.fileExists(resolveIn(root, 'go.mod')) ? 'go.mod' : undefined);\n if (!goModPath) return [];\n\n const fullManifestPath = resolveIn(root, goModPath);\n let goModContent: string;\n try {\n goModContent = readFileSync(fullManifestPath, 'utf-8');\n } catch {\n return [];\n }\n\n const manifestEv = manifestEvidence(fullManifestPath);\n\n // Parse go.mod\n const requires = parseGoMod(goModContent);\n const modName = parseGoModuleName(goModContent);\n\n // Parse go.sum for locked versions\n const goSumPath = resolveIn(root, 'go.sum');\n let lockVersions = new Map<string, string>();\n let lockEv: Evidence | undefined;\n try {\n const sumContent = readFileSync(goSumPath, 'utf-8');\n lockVersions = parseGoSum(sumContent);\n lockEv = lockfileEvidence(goSumPath);\n } catch {\n // No go.sum\n }\n\n for (const req of requires) {\n if (seen.has(req.modulePath)) continue;\n seen.add(req.modulePath);\n\n // Skip the module itself if it appears (rare but possible)\n if (req.modulePath === modName) continue;\n\n // Determine scope\n // Go has no dev/prod distinction in go.mod \u2014 everything is runtime\n // unless marked indirect (which Go treats as transitive)\n const scope: DependencyScope = req.indirect ? 'transitive' : 'runtime';\n const direct = !req.indirect;\n\n // Resolve locked version\n const locked = lockVersions.get(req.modulePath) || req.version;\n\n // Go module paths work like: github.com/gorilla/mux\n // Build PURL with full module path as name\n const purl = buildPurl({ type: 'go', name: req.modulePath, version: locked });\n\n const evidence: Evidence[] = [manifestEv];\n if (lockEv && lockVersions.has(req.modulePath)) evidence.push(lockEv);\n\n observations.push({\n id: `dep-${workspace.id}-${req.modulePath}`,\n workspaceId: workspace.id,\n purl,\n ecosystem: 'go',\n name: req.modulePath,\n sourceType: 'registry',\n direct,\n scope,\n requested: req.version,\n ...(locked ? { locked } : {}),\n status: 'current',\n evidence,\n });\n }\n\n return observations;\n }\n\n private fileExists(filePath: string): boolean {\n try {\n readFileSync(filePath, 'utf-8');\n return true;\n } catch {\n return false;\n }\n }\n}\n\n/**\n * Default singleton instance.\n */\nexport const goAdapter = new GoAdapter();\n", "/**\n * TechStack \u2014 .NET ecosystem adapter.\n *\n * Parses .csproj files and project.assets.json to produce\n * DependencyObservation[] for .NET workspaces.\n *\n * @see docs/specs/techstack-sdd.md \u00A76 Tier A\n */\n\nimport { readFileSync, readdirSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type {\n DependencyObservation,\n Evidence,\n EcosystemId,\n Workspace,\n} from '../types.js';\nimport type {\n EcosystemAdapter,\n InventoryOptions,\n} from './interface.js';\nimport { workspaceRoot } from './paths.js';\nimport { buildPurl } from '../registry/purl.js';\n\n// \u2500\u2500 Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction manifestEvidence(path: string): Evidence {\n return { kind: 'manifest', source: path, retrievedAt: new Date().toISOString() };\n}\n\nfunction lockfileEvidence(path: string): Evidence {\n return { kind: 'lockfile', source: path, retrievedAt: new Date().toISOString() };\n}\n\n// \u2500\u2500 Minimal XML parser for .csproj \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ninterface CsprojPackageRef {\n readonly name: string;\n readonly version: string | undefined;\n}\n\n/**\n * Parse a .csproj file to extract PackageReference items.\n * Handles:\n * <PackageReference Include=\"Newtonsoft.Json\" Version=\"13.0.3\" />\n * <PackageReference Include=\"Serilog\" Version=\"4.2.0\">\n * <PrivateAssets>all</PrivateAssets>\n * </PackageReference>\n * <PackageReference Include=\"Microsoft.AspNetCore.App\" />\n * Condition attributes are ignored.\n */\nfunction parseCsproj(content: string): CsprojPackageRef[] {\n const refs: CsprojPackageRef[] = [];\n // Match: <PackageReference Include=\"Name\" Version=\"ver\" ... />\n const regex = /<PackageReference\\s+Include\\s*=\\s*\"([^\"]+)\"\\s*(?:Version\\s*=\\s*\"([^\"]*)\")?\\s*\\/?\\s*>/g;\n let match: RegExpExecArray | null;\n while ((match = regex.exec(content)) !== null) {\n const name = match[1]!;\n const version = match[2] || undefined;\n refs.push({ name, version });\n }\n return refs;\n}\n\n/**\n * Parse project.assets.json for resolved dependency versions.\n * Format: {\n * \"libraries\": {\n * \"Newtonsoft.Json/13.0.3\": { ... },\n * \"Serilog/4.2.0\": { ... }\n * }\n * }\n */\nfunction parseProjectAssetsJson(content: string): Map<string, string> {\n const versions = new Map<string, string>();\n try {\n const json = JSON.parse(content) as {\n libraries?: Record<string, { type?: string }>;\n };\n if (json.libraries) {\n for (const key of Object.keys(json.libraries)) {\n // Format: \"PackageName/Version\"\n const sepIndex = key.lastIndexOf('/');\n if (sepIndex >= 0) {\n const name = key.slice(0, sepIndex);\n const version = key.slice(sepIndex + 1);\n if (name && version) {\n versions.set(name, version);\n }\n }\n }\n }\n } catch {\n // Malformed JSON\n }\n return versions;\n}\n\n// \u2500\u2500 Adapter \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport class DotNetAdapter implements EcosystemAdapter {\n readonly ecosystem: EcosystemId = 'dotnet';\n\n async inventory(\n workspace: Workspace,\n options: InventoryOptions,\n ): Promise<readonly DependencyObservation[]> {\n const observations: DependencyObservation[] = [];\n const root = workspaceRoot(workspace, options);\n const seen = new Set<string>();\n\n // Find .csproj file via readdirSync\n let csprojPath: string | undefined;\n try {\n const files = readdirSync(root);\n const csproj = files.find((f: string) => f.endsWith('.csproj'));\n if (csproj) csprojPath = join(root, csproj);\n } catch {\n // Can't read directory\n }\n\n if (!csprojPath) return [];\n\n let csprojContent: string;\n try {\n csprojContent = readFileSync(csprojPath, 'utf-8');\n } catch {\n return [];\n }\n\n const manifestEv = manifestEvidence(csprojPath);\n\n // Parse PackageReferences\n const refs = parseCsproj(csprojContent);\n\n // Read project.assets.json for locked versions\n const assetsPath = join(root, 'project.assets.json');\n let lockVersions = new Map<string, string>();\n let lockEv: Evidence | undefined;\n try {\n const assetsContent = readFileSync(assetsPath, 'utf-8');\n lockVersions = parseProjectAssetsJson(assetsContent);\n lockEv = lockfileEvidence(assetsPath);\n } catch {\n // No assets file\n }\n\n for (const ref of refs) {\n if (seen.has(ref.name)) continue;\n seen.add(ref.name);\n\n const locked = lockVersions.get(ref.name) || ref.version;\n\n // .NET PackageReferences are always registry (NuGet)\n const purl = locked\n ? buildPurl({ type: 'dotnet', name: ref.name, version: locked })\n : buildPurl({ type: 'dotnet', name: ref.name });\n\n const evidence: Evidence[] = [manifestEv];\n if (lockEv && lockVersions.has(ref.name)) evidence.push(lockEv);\n\n observations.push({\n id: `dep-${workspace.id}-${ref.name}`,\n workspaceId: workspace.id,\n purl,\n ecosystem: 'dotnet',\n name: ref.name,\n sourceType: 'registry',\n direct: true,\n scope: 'runtime',\n ...(ref.version ? { requested: ref.version } : {}),\n ...(locked ? { locked } : {}),\n status: 'current',\n evidence,\n });\n }\n\n return observations;\n }\n}\n\n/**\n * Default singleton instance.\n */\nexport const dotNetAdapter = new DotNetAdapter();\n", "/**\n * TechStack \u2014 PHP ecosystem adapter.\n *\n * Parses composer.json and composer.lock to produce\n * DependencyObservation[] for PHP workspaces.\n *\n * @see docs/specs/techstack-sdd.md \u00A76 Tier A\n */\n\nimport { readFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type {\n DependencyObservation,\n DependencyScope,\n Evidence,\n EcosystemId,\n Workspace,\n} from '../types.js';\nimport type {\n EcosystemAdapter,\n InventoryOptions,\n} from './interface.js';\nimport { workspaceRoot } from './paths.js';\nimport { buildPurl } from '../registry/purl.js';\n\n// \u2500\u2500 Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction manifestEvidence(path: string): Evidence {\n return { kind: 'manifest', source: path, retrievedAt: new Date().toISOString() };\n}\n\nfunction lockfileEvidence(path: string): Evidence {\n return { kind: 'lockfile', source: path, retrievedAt: new Date().toISOString() };\n}\n\n// \u2500\u2500 composer.json types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ninterface ComposerJson {\n readonly require?: Record<string, string>;\n readonly 'require-dev'?: Record<string, string>;\n}\n\n// \u2500\u2500 composer.lock types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ninterface ComposerLockPackage {\n readonly name: string;\n readonly version: string;\n readonly type?: string;\n readonly 'require'?: Record<string, string>;\n readonly 'require-dev'?: Record<string, string>;\n}\n\ninterface ComposerLock {\n readonly packages?: ComposerLockPackage[];\n readonly 'packages-dev'?: ComposerLockPackage[];\n}\n\n/**\n * Parse a composer.lock file to extract resolved versions.\n */\nfunction parseComposerLock(content: string): Map<string, string> {\n const versions = new Map<string, string>();\n try {\n const lock = JSON.parse(content) as ComposerLock;\n for (const pkg of [...(lock.packages ?? []), ...(lock['packages-dev'] ?? [])]) {\n versions.set(pkg.name, pkg.version);\n }\n } catch {\n // Malformed lockfile\n }\n return versions;\n}\n\n/**\n * Determine status from version constraint.\n */\nfunction statusForComposerSpec(spec: string): DependencyObservation['status'] {\n if (spec.startsWith('file:') || spec.startsWith('path:')) return 'local_path';\n if (spec.startsWith('git@') || spec.startsWith('git:') || spec.startsWith('http')) return 'git_dependency';\n return 'current';\n}\n\n/**\n * Determine sourceType from version constraint.\n */\nfunction sourceTypeForComposerSpec(spec: string): Exclude<DependencyObservation['sourceType'], undefined> {\n if (spec.startsWith('file:') || spec.startsWith('path:')) return 'path';\n if (spec.startsWith('git@') || spec.startsWith('git:') || spec.startsWith('http')) return 'git';\n return 'registry';\n}\n\n// \u2500\u2500 Adapter \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport class PhpAdapter implements EcosystemAdapter {\n readonly ecosystem: EcosystemId = 'php';\n\n async inventory(\n workspace: Workspace,\n options: InventoryOptions,\n ): Promise<readonly DependencyObservation[]> {\n const observations: DependencyObservation[] = [];\n const root = workspaceRoot(workspace, options);\n const seen = new Set<string>();\n\n // Find composer.json\n const composerJsonPath = workspace.manifests.find((m) => m.includes('composer.json'))\n || (this.fileExists(join(root, 'composer.json')) ? join(root, 'composer.json') : undefined);\n if (!composerJsonPath) return [];\n\n let content: string;\n try {\n content = readFileSync(composerJsonPath, 'utf-8');\n } catch {\n return [];\n }\n\n const manifestEv = manifestEvidence(composerJsonPath);\n\n // Parse composer.json\n let composerJson: ComposerJson;\n try {\n composerJson = JSON.parse(content) as ComposerJson;\n } catch {\n return [];\n }\n\n // Parse composer.lock for resolved versions\n const lockPath = join(root, 'composer.lock');\n let lockVersions = new Map<string, string>();\n let lockEv: Evidence | undefined;\n try {\n const lockContent = readFileSync(lockPath, 'utf-8');\n lockVersions = parseComposerLock(lockContent);\n lockEv = lockfileEvidence(lockPath);\n } catch {\n // No lockfile\n }\n\n // Process require (runtime deps) and require-dev (dev deps)\n const sections: Array<{ deps: Record<string, string> | undefined; scope: DependencyScope }> = [\n { deps: composerJson.require, scope: 'runtime' },\n { deps: composerJson['require-dev'], scope: 'development' },\n ];\n\n for (const { deps, scope } of sections) {\n if (!deps) continue;\n for (const [name, constraint] of Object.entries(deps)) {\n if (seen.has(name)) continue;\n seen.add(name);\n\n const locked = lockVersions.get(name);\n const status = statusForComposerSpec(constraint);\n const sourceType = sourceTypeForComposerSpec(constraint);\n const isRegistry = sourceType === 'registry';\n\n const purl = isRegistry && locked\n ? buildPurl({ type: 'php', name, version: locked })\n : isRegistry\n ? buildPurl({ type: 'php', name })\n : undefined;\n\n const evidence: Evidence[] = [manifestEv];\n if (lockEv && locked) evidence.push(lockEv);\n\n observations.push({\n id: `dep-${workspace.id}-${name}`,\n workspaceId: workspace.id,\n ...(purl ? { purl } : {}),\n ecosystem: 'php',\n name,\n sourceType,\n direct: true,\n scope,\n requested: constraint,\n ...(locked ? { locked } : {}),\n status,\n evidence,\n });\n }\n }\n\n return observations;\n }\n\n private fileExists(filePath: string): boolean {\n try {\n readFileSync(filePath, 'utf-8');\n return true;\n } catch {\n return false;\n }\n }\n}\n\n/**\n * Default singleton instance.\n */\nexport const phpAdapter = new PhpAdapter();\n", "/**\n * TechStack \u2014 Dart ecosystem adapter.\n *\n * Parses pubspec.yaml and pubspec.lock to produce\n * DependencyObservation[] for Dart/Flutter workspaces.\n *\n * @see docs/specs/techstack-sdd.md \u00A76 Tier A\n */\n\nimport { readFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type {\n DependencyObservation,\n DependencyScope,\n Evidence,\n EcosystemId,\n Workspace,\n} from '../types.js';\nimport type {\n EcosystemAdapter,\n InventoryOptions,\n} from './interface.js';\nimport { workspaceRoot } from './paths.js';\nimport { buildPurl } from '../registry/purl.js';\n\n// \u2500\u2500 Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction manifestEvidence(path: string): Evidence {\n return { kind: 'manifest', source: path, retrievedAt: new Date().toISOString() };\n}\n\nfunction lockfileEvidence(path: string): Evidence {\n return { kind: 'lockfile', source: path, retrievedAt: new Date().toISOString() };\n}\n\n// \u2500\u2500 Minimal YAML parser (line-based, sufficient for pubspec.yaml) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Parse a pubspec.yaml to extract dependencies sections.\n * Returns a map of section name \u2192 Map of dependency name \u2192 constraint.\n *\n * Handles:\n * dependencies:\n * flutter:\n * sdk: flutter\n * http: ^1.2.0\n * dev_dependencies:\n * test: ^1.24.0\n */\nfunction parsePubspecYaml(content: string): Map<string, Map<string, string>> {\n const sections = new Map<string, Map<string, string>>();\n let currentSection: string | undefined;\n let currentName: string | undefined;\n\n for (const raw of content.split('\\n')) {\n const line = raw.trimEnd();\n const trimmed = line.trim();\n if (trimmed === '' || trimmed.startsWith('#')) continue;\n\n // Section header (no indent): `dependencies:`\n const sectionMatch = trimmed.match(/^(\\w[\\w-]*):\\s*$/);\n if (sectionMatch && line.startsWith(sectionMatch[1]!)) {\n currentSection = sectionMatch[1]!;\n currentName = undefined;\n if (!sections.has(currentSection)) {\n sections.set(currentSection, new Map());\n }\n continue;\n }\n\n if (!currentSection) continue;\n\n // Dependency definition: ` package_name: ^1.0.0`\n // Or sub-properties: ` sdk: flutter` \u2014 skip these\n const depMatch = trimmed.match(/^(\\S[^:]*?):\\s*(.*)$/);\n if (depMatch && line.startsWith(' ') && !line.startsWith(' ')) {\n currentName = depMatch[1]!.trim();\n let constraint = depMatch[2]!.trim();\n // Handle empty constraints (sdk: flutter has constraint as sub-props)\n if (!constraint || constraint.startsWith('{')) {\n constraint = '*';\n }\n const sec = sections.get(currentSection)!;\n sec.set(currentName, constraint);\n }\n }\n\n return sections;\n}\n\n/**\n * Parse pubspec.lock to extract resolved versions.\n * pubspec.lock uses YAML format with packages as a map.\n *\n * packages:\n * http:\n * version: \"1.2.0\"\n * path:\n * version: \"2.0.0\"\n */\nfunction parsePubspecLock(content: string): Map<string, string> {\n const versions = new Map<string, string>();\n const lines = content.split('\\n');\n let currentPackage: string | undefined;\n let inPackages = false;\n\n for (const raw of lines) {\n const trimmed = raw.trim();\n if (trimmed === '') continue;\n\n if (trimmed === 'packages:') {\n inPackages = true;\n continue;\n }\n\n if (!inPackages) continue;\n\n // Package name: ` package_name:`\n const pkgMatch = trimmed.match(/^(\\S[^:]*):\\s*$/);\n if (pkgMatch && raw.startsWith(' ') && !raw.startsWith(' ')) {\n currentPackage = pkgMatch[1]!.trim();\n continue;\n }\n\n // Version: ` version: \"1.2.0\"`\n if (currentPackage) {\n const verMatch = trimmed.match(/^version:\\s*\"?([^\"\\s]+)\"?\\s*$/);\n if (verMatch && raw.startsWith(' ')) {\n versions.set(currentPackage, verMatch[1]!);\n currentPackage = undefined;\n }\n }\n }\n\n return versions;\n}\n\n// \u2500\u2500 Adapter \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport class DartAdapter implements EcosystemAdapter {\n readonly ecosystem: EcosystemId = 'dart';\n\n async inventory(\n workspace: Workspace,\n options: InventoryOptions,\n ): Promise<readonly DependencyObservation[]> {\n const observations: DependencyObservation[] = [];\n const root = workspaceRoot(workspace, options);\n const seen = new Set<string>();\n\n // Find pubspec.yaml\n const pubspecPath = workspace.manifests.find((m) => m.includes('pubspec.yaml'))\n || (this.fileExists(join(root, 'pubspec.yaml')) ? join(root, 'pubspec.yaml') : undefined);\n if (!pubspecPath) return [];\n\n let content: string;\n try {\n content = readFileSync(pubspecPath, 'utf-8');\n } catch {\n return [];\n }\n\n const manifestEv = manifestEvidence(pubspecPath);\n\n // Parse pubspec.yaml\n const sections = parsePubspecYaml(content);\n\n // Parse pubspec.lock\n const lockPath = join(root, 'pubspec.lock');\n let lockVersions = new Map<string, string>();\n let lockEv: Evidence | undefined;\n try {\n const lockContent = readFileSync(lockPath, 'utf-8');\n lockVersions = parsePubspecLock(lockContent);\n lockEv = lockfileEvidence(lockPath);\n } catch {\n // No lockfile\n }\n\n // Process sections\n const sectionMapping: Array<{ yamlSection: string; scope: DependencyScope }> = [\n { yamlSection: 'dependencies', scope: 'runtime' },\n { yamlSection: 'dev_dependencies', scope: 'development' },\n { yamlSection: 'dependency_overrides', scope: 'runtime' },\n ];\n\n for (const { yamlSection, scope } of sectionMapping) {\n const deps = sections.get(yamlSection);\n if (!deps) continue;\n\n for (const [name, constraint] of deps) {\n if (seen.has(name)) continue;\n seen.add(name);\n\n // Skip sdk dependencies (they are the Dart SDK itself)\n if (constraint === '*' || constraint.startsWith('{')) continue;\n\n const locked = lockVersions.get(name);\n\n // Determine status\n let status: DependencyObservation['status'] = 'current';\n let sourceType: Exclude<DependencyObservation['sourceType'], undefined> = 'registry';\n\n if (constraint.startsWith('path:')) {\n status = 'local_path';\n sourceType = 'path';\n } else if (constraint.startsWith('git:')) {\n status = 'git_dependency';\n sourceType = 'git';\n } else if (constraint.startsWith('{')) {\n // Inline map: e.g. {sdk: flutter}\n status = 'local_path';\n sourceType = 'path';\n }\n\n const isRegistry = sourceType === 'registry';\n // Strip caret/tilde/>= for PURL \u2014 use locked if available\n const purl = isRegistry && (locked || constraint)\n ? buildPurl({ type: 'dart', name, version: locked || constraint.replace(/^[\\^~>=<\\s]+/, '') })\n : isRegistry\n ? buildPurl({ type: 'dart', name })\n : undefined;\n\n const evidence: Evidence[] = [manifestEv];\n if (lockEv && locked) evidence.push(lockEv);\n\n observations.push({\n id: `dep-${workspace.id}-${name}`,\n workspaceId: workspace.id,\n ...(purl ? { purl } : {}),\n ecosystem: 'dart',\n name,\n sourceType,\n direct: true,\n scope,\n ...(constraint && constraint !== '*' ? { requested: constraint } : {}),\n ...(locked ? { locked } : {}),\n status,\n evidence,\n });\n }\n }\n\n return observations;\n }\n\n private fileExists(filePath: string): boolean {\n try {\n readFileSync(filePath, 'utf-8');\n return true;\n } catch {\n return false;\n }\n }\n}\n\n/**\n * Default singleton instance.\n */\nexport const dartAdapter = new DartAdapter();\n", "/**\n * TechStack \u2014 Maven ecosystem adapter (Tier B).\n *\n * Parses pom.xml for direct dependencies. Partial support \u2014 no lockfile\n * parsing (Maven has no standardized lockfile); version resolution\n * requires `mvn dependency:tree` which is not invoked here.\n *\n * @see docs/specs/techstack-sdd.md \u00A76 Tier B\n */\n\nimport { readFileSync } from 'node:fs';\nimport type {\n DependencyObservation,\n DependencyScope,\n Evidence,\n EcosystemId,\n Workspace,\n} from '../types.js';\nimport type { EcosystemAdapter, InventoryOptions } from './interface.js';\nimport { buildPurl } from '../registry/purl.js';\n\ninterface MavenDependency {\n readonly groupId: string;\n readonly artifactId: string;\n readonly version?: string | undefined;\n readonly scope?: string | undefined;\n}\n\nfunction manifestEvidence(path: string): Evidence {\n return { kind: 'manifest', source: path, retrievedAt: new Date().toISOString() };\n}\n\n/**\n * Minimal XML parser for `<dependency>` blocks inside pom.xml.\n * Does not handle inheritance/dependencyManagement \u2014 this is Tier B partial.\n */\nfunction parsePomDependencies(xml: string): MavenDependency[] {\n const deps: MavenDependency[] = [];\n const depRegex = /<dependency>\\s*([\\s\\S]*?)<\\/dependency>/g;\n let match: RegExpExecArray | null;\n while ((match = depRegex.exec(xml)) !== null) {\n const block = match[1]!;\n const groupId = block.match(/<groupId>([^<]+)<\\/groupId>/)?.[1]?.trim();\n const artifactId = block.match(/<artifactId>([^<]+)<\\/artifactId>/)?.[1]?.trim();\n const version = block.match(/<version>([^<]+)<\\/version>/)?.[1]?.trim();\n const scope = block.match(/<scope>([^<]+)<\\/scope>/)?.[1]?.trim();\n if (groupId && artifactId) {\n deps.push({ groupId, artifactId, version, scope });\n }\n }\n return deps;\n}\n\nfunction mavenScopeToScope(scope: string | undefined): DependencyScope {\n switch (scope) {\n case 'test': return 'development';\n case 'provided': return 'optional';\n case 'runtime': return 'runtime';\n case 'compile': return 'runtime';\n default: return 'runtime';\n }\n}\n\nexport class MavenAdapter implements EcosystemAdapter {\n readonly ecosystem: EcosystemId = 'maven';\n\n async inventory(\n workspace: Workspace,\n _options: InventoryOptions,\n ): Promise<readonly DependencyObservation[]> {\n const observations: DependencyObservation[] = [];\n const pomPath = workspace.manifests.find((m) => m.includes('pom.xml'));\n if (!pomPath) return [];\n\n let content: string;\n try {\n content = readFileSync(pomPath, 'utf-8');\n } catch {\n return [];\n }\n\n const manifestEv = manifestEvidence(pomPath);\n const deps = parsePomDependencies(content);\n const seen = new Set<string>();\n\n for (const dep of deps) {\n const name = `${dep.groupId}:${dep.artifactId}`;\n if (seen.has(name)) continue;\n seen.add(name);\n\n const purl = dep.version\n ? buildPurl({ type: 'maven', name, version: dep.version })\n : buildPurl({ type: 'maven', name });\n\n observations.push({\n id: `dep-${workspace.id}-${name}`,\n workspaceId: workspace.id,\n purl,\n ecosystem: 'maven',\n name,\n sourceType: 'registry',\n direct: true,\n scope: mavenScopeToScope(dep.scope),\n ...(dep.version ? { requested: dep.version } : {}),\n status: 'current',\n evidence: [manifestEv],\n });\n }\n\n return observations;\n }\n}\n\nexport const mavenAdapter = new MavenAdapter();\n", "/**\n * TechStack \u2014 Ruby/Bundler ecosystem adapter (Tier B).\n *\n * Parses Gemfile and Gemfile.lock for direct and transitive dependencies.\n * Partial support \u2014 no registry API; OSV-only advisory enrichment.\n *\n * @see docs/specs/techstack-sdd.md \u00A76 Tier B\n */\n\nimport { readFileSync } from 'node:fs';\nimport type {\n DependencyObservation,\n DependencyScope,\n Evidence,\n EcosystemId,\n Workspace,\n} from '../types.js';\nimport type { EcosystemAdapter, InventoryOptions } from './interface.js';\nimport { buildPurl } from '../registry/purl.js';\n\nfunction manifestEvidence(path: string): Evidence {\n return { kind: 'manifest', source: path, retrievedAt: new Date().toISOString() };\n}\n\nfunction lockfileEvidence(path: string): Evidence {\n return { kind: 'lockfile', source: path, retrievedAt: new Date().toISOString() };\n}\n\n/**\n * Parse Gemfile for direct `gem 'name'` and `gem 'name', 'version'` calls.\n */\nfunction parseGemfile(content: string): Array<{ name: string; version?: string | undefined }> {\n const gems: Array<{ name: string; version?: string | undefined }> = [];\n const gemRegex = /gem\\s+['\"]([^'\"]+)['\"](?:\\s*,\\s*['\"]([^'\"]+)['\"])?/g;\n let match: RegExpExecArray | null;\n while ((match = gemRegex.exec(content)) !== null) {\n const name = match[1]!;\n // Skip gems that are clearly comments or block-evaluated\n if (name === 'rails' || name === 'ruby') continue;\n gems.push({ name, version: match[2] });\n }\n return gems;\n}\n\n/**\n * Parse Gemfile.lock `GEM` section for resolved versions.\n * Format: ` name (version)`.\n */\nfunction parseGemfileLock(content: string): Map<string, string> {\n const versions = new Map<string, string>();\n const lines = content.split('\\n');\n let inSpecs = false;\n for (const line of lines) {\n if (line.startsWith('GEM')) { inSpecs = true; continue; }\n if (inSpecs && /^[A-Z]/.test(line) && !line.startsWith(' ')) { inSpecs = false; continue; }\n if (!inSpecs) continue;\n const match = /^\\s{4,}([\\w-]+)\\s+\\(([^)]+)\\)/.exec(line);\n if (match) {\n const version = match[2]!.split(' ')[0] ?? match[2]!;\n versions.set(match[1]!, version);\n }\n }\n return versions;\n}\n\nexport class RubyAdapter implements EcosystemAdapter {\n readonly ecosystem: EcosystemId = 'ruby';\n\n async inventory(\n workspace: Workspace,\n _options: InventoryOptions,\n ): Promise<readonly DependencyObservation[]> {\n const observations: DependencyObservation[] = [];\n const gemfilePath = workspace.manifests.find((m) => m.includes('Gemfile'));\n if (!gemfilePath) return [];\n\n let content: string;\n try {\n content = readFileSync(gemfilePath, 'utf-8');\n } catch {\n return [];\n }\n\n const manifestEv = manifestEvidence(gemfilePath);\n const gems = parseGemfile(content);\n const seen = new Set<string>();\n\n // Parse lockfile\n const lockfilePath = workspace.lockfiles.find((l) => l.includes('Gemfile.lock'));\n let lockVersions = new Map<string, string>();\n let lockEv: Evidence | undefined;\n if (lockfilePath) {\n try {\n const lockContent = readFileSync(lockfilePath, 'utf-8');\n lockVersions = parseGemfileLock(lockContent);\n lockEv = lockfileEvidence(lockfilePath);\n } catch {\n // No lockfile\n }\n }\n\n for (const gem of gems) {\n if (seen.has(gem.name)) continue;\n seen.add(gem.name);\n\n const locked = lockVersions.get(gem.name);\n const version = locked ?? gem.version;\n const purl = version\n ? buildPurl({ type: 'gem', name: gem.name, version })\n : buildPurl({ type: 'gem', name: gem.name });\n\n const evidence: Evidence[] = [manifestEv];\n if (lockEv && locked) evidence.push(lockEv);\n\n observations.push({\n id: `dep-${workspace.id}-${gem.name}`,\n workspaceId: workspace.id,\n purl,\n ecosystem: 'ruby',\n name: gem.name,\n sourceType: 'registry',\n direct: true,\n scope: 'runtime' as DependencyScope,\n ...(gem.version ? { requested: gem.version } : {}),\n ...(locked ? { locked } : {}),\n status: 'current',\n evidence,\n });\n }\n\n return observations;\n }\n}\n\nexport const rubyAdapter = new RubyAdapter();\n", "/**\n * TechStack \u2014 Elixir/Hex ecosystem adapter (Tier B).\n *\n * Parses mix.exs for direct dependencies and mix.lock for resolved versions.\n * Partial support \u2014 no registry API; OSV-only advisory enrichment.\n *\n * @see docs/specs/techstack-sdd.md \u00A76 Tier B\n */\n\nimport { readFileSync } from 'node:fs';\nimport type {\n DependencyObservation,\n DependencyScope,\n Evidence,\n EcosystemId,\n Workspace,\n} from '../types.js';\nimport type { EcosystemAdapter, InventoryOptions } from './interface.js';\nimport { buildPurl } from '../registry/purl.js';\n\nfunction manifestEvidence(path: string): Evidence {\n return { kind: 'manifest', source: path, retrievedAt: new Date().toISOString() };\n}\n\nfunction lockfileEvidence(path: string): Evidence {\n return { kind: 'lockfile', source: path, retrievedAt: new Date().toISOString() };\n}\n\n/**\n * Parse mix.exs `defp deps do` block for `{:name, \"version\"}` tuples.\n */\nfunction parseMixExsDeps(content: string): Array<{ name: string; version?: string | undefined }> {\n const deps: Array<{ name: string; version?: string | undefined }> = [];\n // Match: {:name, \"version\"} or {:name, \"~> x.y\"} or {:name, github: \"...\"} or {:name, path: \"...\"}\n const depRegex = /\\{:(\\w+),\\s*[\"']([^\"']+)[\"']\\}/g;\n let match: RegExpExecArray | null;\n while ((match = depRegex.exec(content)) !== null) {\n deps.push({ name: match[1]!, version: match[2] });\n }\n return deps;\n}\n\n/**\n * Parse mix.lock for resolved hex versions.\n * Format: `{\"name\", hex: \":uuid\", \"1.2.3\"}`\n */\nfunction parseMixLock(content: string): Map<string, string> {\n const versions = new Map<string, string>();\n const lockRegex = /\\{:\"(\\w+)\",\\s*hex: \"[^\"]*\",\\s*\"([^\"]+)\"/g;\n let match: RegExpExecArray | null;\n while ((match = lockRegex.exec(content)) !== null) {\n versions.set(match[1]!, match[2]!);\n }\n return versions;\n}\n\nexport class ElixirAdapter implements EcosystemAdapter {\n readonly ecosystem: EcosystemId = 'elixir';\n\n async inventory(\n workspace: Workspace,\n _options: InventoryOptions,\n ): Promise<readonly DependencyObservation[]> {\n const observations: DependencyObservation[] = [];\n const mixExsPath = workspace.manifests.find((m) => m.includes('mix.exs'));\n if (!mixExsPath) return [];\n\n let content: string;\n try {\n content = readFileSync(mixExsPath, 'utf-8');\n } catch {\n return [];\n }\n\n const manifestEv = manifestEvidence(mixExsPath);\n const deps = parseMixExsDeps(content);\n const seen = new Set<string>();\n\n // Parse lockfile\n const lockfilePath = workspace.lockfiles.find((l) => l.includes('mix.lock'));\n let lockVersions = new Map<string, string>();\n let lockEv: Evidence | undefined;\n if (lockfilePath) {\n try {\n const lockContent = readFileSync(lockfilePath, 'utf-8');\n lockVersions = parseMixLock(lockContent);\n lockEv = lockfileEvidence(lockfilePath);\n } catch {\n // No lockfile\n }\n }\n\n for (const dep of deps) {\n if (seen.has(dep.name)) continue;\n seen.add(dep.name);\n\n const locked = lockVersions.get(dep.name);\n const version = locked ?? dep.version;\n const purl = version\n ? buildPurl({ type: 'hex', name: dep.name, version })\n : buildPurl({ type: 'hex', name: dep.name });\n\n const evidence: Evidence[] = [manifestEv];\n if (lockEv && locked) evidence.push(lockEv);\n\n observations.push({\n id: `dep-${workspace.id}-${dep.name}`,\n workspaceId: workspace.id,\n purl,\n ecosystem: 'elixir',\n name: dep.name,\n sourceType: 'registry',\n direct: true,\n scope: 'runtime' as DependencyScope,\n ...(dep.version ? { requested: dep.version } : {}),\n ...(locked ? { locked } : {}),\n status: 'current',\n evidence,\n });\n }\n\n return observations;\n }\n}\n\nexport const elixirAdapter = new ElixirAdapter();\n", "/**\n * TechStack \u2014 C/C++ ecosystem adapter (Tier C).\n *\n * Best-effort: parses conanfile.txt / conanfile.py for `[requires]` and\n * vcpkg.json for dependencies. No lockfile resolution; coverage='unsupported'.\n *\n * @see docs/specs/techstack-sdd.md \u00A76 Tier C\n */\n\nimport { readFileSync } from 'node:fs';\nimport type {\n DependencyObservation,\n Evidence,\n EcosystemId,\n Workspace,\n} from '../types.js';\nimport type { EcosystemAdapter, InventoryOptions } from './interface.js';\nimport { buildPurl } from '../registry/purl.js';\n\nfunction manifestEvidence(path: string): Evidence {\n return { kind: 'manifest', source: path, retrievedAt: new Date().toISOString() };\n}\n\n/**\n * Parse conanfile.txt `[requires]` section.\n */\nfunction parseConanTxt(content: string): Array<{ name: string; version?: string | undefined }> {\n const deps: Array<{ name: string; version?: string | undefined }> = [];\n const requiresMatch = /\\[requires\\]\\s*\\n([\\s\\S]*?)(?:\\[|$)/;\n const block = requiresMatch.exec(content)?.[1];\n if (!block) return deps;\n\n for (const line of block.split('\\n')) {\n const trimmed = line.trim();\n if (!trimmed || trimmed.startsWith('#')) continue;\n const parts = trimmed.split('/');\n if (parts.length >= 2) {\n deps.push({ name: parts[0]!, version: parts[1] });\n } else {\n deps.push({ name: trimmed });\n }\n }\n return deps;\n}\n\n/**\n * Parse vcpkg.json `dependencies` array.\n */\nfunction parseVcpkgJson(content: string): Array<{ name: string; version?: string | undefined }> {\n const deps: Array<{ name: string; version?: string | undefined }> = [];\n try {\n const json = JSON.parse(content) as { dependencies?: Array<string | { name: string; version?: string }> };\n for (const dep of json.dependencies ?? []) {\n if (typeof dep === 'string') {\n deps.push({ name: dep });\n } else {\n deps.push({ name: dep.name, version: dep.version });\n }\n }\n } catch {\n // Malformed\n }\n return deps;\n}\n\nexport class CppAdapter implements EcosystemAdapter {\n readonly ecosystem: EcosystemId = 'cpp';\n\n async inventory(\n workspace: Workspace,\n _options: InventoryOptions,\n ): Promise<readonly DependencyObservation[]> {\n const observations: DependencyObservation[] = [];\n const seen = new Set<string>();\n\n for (const manifestPath of workspace.manifests) {\n let content: string;\n try {\n content = readFileSync(manifestPath, 'utf-8');\n } catch {\n continue;\n }\n\n const manifestEv = manifestEvidence(manifestPath);\n let deps: Array<{ name: string; version?: string | undefined }> = [];\n\n if (manifestPath.includes('conanfile')) {\n deps = parseConanTxt(content);\n } else if (manifestPath.includes('vcpkg.json')) {\n deps = parseVcpkgJson(content);\n } else {\n continue;\n }\n\n for (const dep of deps) {\n if (seen.has(dep.name)) continue;\n seen.add(dep.name);\n\n const purl = dep.version\n ? buildPurl({ type: 'conan', name: dep.name, version: dep.version })\n : buildPurl({ type: 'conan', name: dep.name });\n\n observations.push({\n id: `dep-${workspace.id}-${dep.name}`,\n workspaceId: workspace.id,\n purl,\n ecosystem: 'cpp',\n name: dep.name,\n sourceType: 'registry',\n direct: true,\n scope: 'runtime',\n ...(dep.version ? { requested: dep.version } : {}),\n // Tier C \u2014 we cannot verify current/version status\n status: 'unknown',\n evidence: [manifestEv],\n });\n }\n }\n\n return observations;\n }\n}\n\nexport const cppAdapter = new CppAdapter();\n", "/**\n * TechStack \u2014 Snapshot diff utility.\n *\n * Compares two snapshots to identify added, removed, and changed dependencies.\n *\n * @see docs/specs/techstack-sdd.md \u00A79\n */\n\nimport type { DependencyObservation, Snapshot } from './types.js';\n\nexport interface SnapshotDiff {\n added: DependencyObservation[];\n removed: DependencyObservation[];\n changed: Array<{\n name: string;\n ecosystem: string;\n field: string;\n from: string;\n to: string;\n }>;\n}\n\n/**\n * Compare two snapshots by dependency name + ecosystem.\n *\n * Returns added (in new but not old), removed (in old but not new), and\n * changed (version/status differences for matching dependencies).\n */\nexport function diffSnapshots(oldSnapshot: Snapshot, newSnapshot: Snapshot): SnapshotDiff {\n const oldByKey = new Map<string, DependencyObservation>();\n for (const dep of oldSnapshot.dependencies) {\n oldByKey.set(`${dep.ecosystem}:${dep.name}`, dep);\n }\n\n const newByKey = new Map<string, DependencyObservation>();\n for (const dep of newSnapshot.dependencies) {\n newByKey.set(`${dep.ecosystem}:${dep.name}`, dep);\n }\n\n const added: DependencyObservation[] = [];\n const removed: DependencyObservation[] = [];\n const changed: SnapshotDiff['changed'] = [];\n\n // Find added + changed\n for (const [key, newDep] of newByKey) {\n const oldDep = oldByKey.get(key);\n if (!oldDep) {\n added.push(newDep);\n continue;\n }\n\n // Check version changes\n const fields: Array<keyof DependencyObservation> = ['locked', 'requested', 'status', 'latestStable'];\n for (const field of fields) {\n const oldVal = String(oldDep[field] ?? '');\n const newVal = String(newDep[field] ?? '');\n if (oldVal !== newVal) {\n changed.push({\n name: newDep.name,\n ecosystem: newDep.ecosystem,\n field: String(field),\n from: oldVal,\n to: newVal,\n });\n }\n }\n }\n\n // Find removed\n for (const [key, oldDep] of oldByKey) {\n if (!newByKey.has(key)) {\n removed.push(oldDep);\n }\n }\n\n return { added, removed, changed };\n}\n", "/**\n * TechStack \u2014 SBOM (Software Bill of Materials) export.\n *\n * Converts a Snapshot into SPDX or CycloneDX JSON format.\n *\n * @see docs/specs/techstack-sdd.md \u00A79\n */\n\nimport type { Snapshot } from './types.js';\n\n// \u2500\u2500 SPDX \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface SpdxDocument {\n spdxVersion: string;\n dataLicense: string;\n SPDXID: string;\n name: string;\n documentNamespace: string;\n creationInfo: {\n created: string;\n creators: string[];\n };\n packages: Array<{\n name: string;\n SPDXID: string;\n versionInfo?: string | undefined;\n downloadLocation?: string | undefined;\n licenseConcluded?: string | undefined;\n }>;\n}\n\nexport function toSpdx(snapshot: Snapshot): SpdxDocument {\n const created = snapshot.createdAt;\n return {\n spdxVersion: 'SPDX-2.3',\n dataLicense: 'CC0-1.0',\n SPDXID: 'SPDXRef-DOCUMENT',\n name: `TechStack-SBOM-${snapshot.projectId}`,\n documentNamespace: `https://wrongstack.dev/spdx/${snapshot.id}`,\n creationInfo: {\n created,\n creators: ['Tool: WrongStack TechStack Engine'],\n },\n packages: snapshot.dependencies.map((dep, index) => ({\n name: dep.name,\n SPDXID: `SPDXRef-Package-${index}`,\n versionInfo: dep.locked ?? dep.requested,\n downloadLocation: dep.purl ? `https://purl.io/${dep.purl}` : 'NOASSERTION',\n licenseConcluded: dep.license ?? 'NOASSERTION',\n })),\n };\n}\n\n// \u2500\u2500 CycloneDX \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface CycloneDXBom {\n bomFormat: string;\n specVersion: string;\n version: number;\n metadata: {\n timestamp: string;\n tools: Array<{ name: string; version: string }>;\n };\n components: Array<{\n type: string;\n name: string;\n version?: string | undefined;\n purl?: string | undefined;\n licenses?: Array<{ license: { id: string } }> | undefined;\n }>;\n}\n\nexport function toCycloneDX(snapshot: Snapshot): CycloneDXBom {\n return {\n bomFormat: 'CycloneDX',\n specVersion: '1.5',\n version: 1,\n metadata: {\n timestamp: snapshot.createdAt,\n tools: [{ name: 'WrongStack TechStack Engine', version: snapshot.adapterVersion }],\n },\n components: snapshot.dependencies.map((dep) => ({\n type: 'library',\n name: dep.name,\n version: dep.locked ?? dep.requested,\n ...(dep.purl ? { purl: dep.purl } : {}),\n ...(dep.license\n ? { licenses: [{ license: { id: dep.license } }] }\n : {}),\n })),\n };\n}\n", "/**\n * TechStack \u2014 Remediation planning.\n *\n * Generates dry-run upgrade plans from a snapshot's findings. NEVER mutates\n * dependency files \u2014 the plan is read-only output that the user must\n * explicitly approve before any `language_package` or `install` tool runs.\n *\n * @see docs/specs/techstack-sdd.md \u00A72 (R25), \u00A79\n */\n\nimport type { DependencyObservation, Finding, Snapshot } from './types.js';\n\nexport interface UpgradePlanItem {\n readonly dependencyName: string;\n readonly ecosystem: string;\n readonly workspaceId: string;\n readonly currentVersion: string | undefined;\n readonly targetVersion: string | undefined;\n readonly action: Finding['action'];\n readonly severity: Finding['severity'];\n readonly rationale: string;\n readonly breakingRisk: string | undefined;\n /** Command suggestion (informational only \u2014 never auto-executed). */\n readonly suggestedCommand: string | undefined;\n}\n\nexport interface UpgradePlan {\n readonly snapshotId: string;\n readonly generatedAt: string;\n readonly items: readonly UpgradePlanItem[];\n readonly summary: {\n readonly total: number;\n readonly patch: number;\n readonly minor: number;\n readonly major: number;\n readonly replace: number;\n readonly remove: number;\n readonly investigate: number;\n };\n readonly warning: string;\n}\n\n/**\n * Suggest a package-manager command for a given ecosystem + action.\n * This is purely informational \u2014 the actual execution goes through the\n * permission-gated `language_package` tool.\n */\nfunction suggestCommand(\n ecosystem: string,\n name: string,\n action: Finding['action'],\n targetVersion?: string,\n): string | undefined {\n const ver = targetVersion ? `@${targetVersion}` : '@latest';\n switch (ecosystem) {\n case 'npm':\n if (action === 'remove') return `npm uninstall ${name}`;\n return `npm install ${name}${ver}`;\n case 'python':\n if (action === 'remove') return `pip uninstall ${name}`;\n return `pip install ${name}${ver}`;\n case 'rust':\n if (action === 'remove') return `cargo remove ${name}`;\n return `cargo add ${name}@${targetVersion ?? 'latest'}`;\n case 'go':\n if (action === 'remove') return `go get ${name}@none`;\n return `go get ${name}@${targetVersion ?? 'latest'}`;\n case 'php':\n if (action === 'remove') return `composer remove ${name}`;\n return `composer require ${name}:${targetVersion ?? 'latest'}`;\n case 'dotnet':\n if (action === 'remove') return `dotnet remove package ${name}`;\n return `dotnet add package ${name}`;\n default:\n return undefined;\n }\n}\n\n/**\n * Generate a dry-run upgrade plan from a snapshot.\n *\n * This function is **read-only** \u2014 it never touches manifests, lockfiles,\n * or the filesystem. It only reads the snapshot's findings and produces\n * a structured plan the user can review.\n *\n * @param snapshot The enriched snapshot to plan from.\n * @returns A structured upgrade plan.\n */\nexport function generateUpgradePlan(snapshot: Snapshot): UpgradePlan {\n const findings = snapshot.findings as readonly Finding[];\n const items: UpgradePlanItem[] = [];\n\n for (const finding of findings) {\n if (finding.action === 'none') continue;\n\n const dep = snapshot.dependencies.find(\n (d: DependencyObservation) => d.id === finding.dependencyId,\n );\n if (!dep) continue;\n\n const targetVersion = dep.latestStable ?? dep.resolvable ?? dep.wanted;\n\n items.push({\n dependencyName: dep.name,\n ecosystem: dep.ecosystem,\n workspaceId: dep.workspaceId,\n currentVersion: dep.locked ?? dep.installed ?? dep.requested,\n targetVersion,\n action: finding.action,\n severity: finding.severity,\n rationale: finding.rationale,\n breakingRisk: finding.breakingRisk,\n suggestedCommand: suggestCommand(dep.ecosystem, dep.name, finding.action, targetVersion),\n });\n }\n\n // Sort by severity (critical first, then high, medium, low, info)\n const severityOrder = new Map([\n ['critical', 0],\n ['high', 1],\n ['medium', 2],\n ['low', 3],\n ['info', 4],\n ]);\n items.sort((a, b) => {\n const sa = severityOrder.get(a.severity) ?? 5;\n const sb = severityOrder.get(b.severity) ?? 5;\n return sa - sb;\n });\n\n const summary = {\n total: items.length,\n patch: items.filter((i) => i.action === 'upgrade_patch').length,\n minor: items.filter((i) => i.action === 'upgrade_minor').length,\n major: items.filter((i) => i.action === 'upgrade_major').length,\n replace: items.filter((i) => i.action === 'replace').length,\n remove: items.filter((i) => i.action === 'remove').length,\n investigate: items.filter((i) => i.action === 'investigate').length,\n };\n\n return {\n snapshotId: snapshot.id,\n generatedAt: new Date().toISOString(),\n items,\n summary,\n warning:\n 'This plan is read-only. No dependency files will be modified unless you explicitly approve and execute each item.',\n };\n}\n\n/**\n * Render an upgrade plan as Markdown for the report endpoint or CLI display.\n */\nexport function renderPlanMarkdown(plan: UpgradePlan): string {\n const lines: string[] = [\n '# TechStack Remediation Plan',\n '',\n `**Generated:** ${plan.generatedAt}`,\n `**Snapshot:** ${plan.snapshotId}`,\n `**Total items:** ${plan.summary.total}`,\n '',\n `> \u26A0\uFE0F ${plan.warning}`,\n '',\n ];\n\n if (plan.items.length === 0) {\n lines.push('_No remediation actions needed \u2014 all dependencies are current._');\n return lines.join('\\n');\n }\n\n // Summary table\n lines.push('## Summary', '');\n lines.push('| Action | Count |');\n lines.push('|---|---|');\n lines.push(`| Patch upgrade | ${plan.summary.patch} |`);\n lines.push(`| Minor upgrade | ${plan.summary.minor} |`);\n lines.push(`| Major upgrade | ${plan.summary.major} |`);\n lines.push(`| Replace | ${plan.summary.replace} |`);\n lines.push(`| Remove | ${plan.summary.remove} |`);\n lines.push(`| Investigate | ${plan.summary.investigate} |`);\n lines.push('');\n\n // Detail items\n lines.push('## Items', '');\n for (const item of plan.items) {\n const icon =\n item.severity === 'critical' ? '\uD83D\uDD34' :\n item.severity === 'high' ? '\uD83D\uDFE0' :\n item.severity === 'medium' ? '\uD83D\uDFE1' :\n item.severity === 'low' ? '\uD83D\uDD35' : '\u2139\uFE0F';\n\n lines.push(`### ${icon} ${item.dependencyName} (${item.ecosystem})`, '');\n lines.push(`- **Action:** ${item.action}`);\n lines.push(`- **Current:** ${item.currentVersion ?? 'unknown'}`);\n lines.push(`- **Target:** ${item.targetVersion ?? 'latest'}`);\n lines.push(`- **Severity:** ${item.severity}`);\n lines.push(`- **Rationale:** ${item.rationale}`);\n if (item.breakingRisk) lines.push(`- **Breaking risk:** ${item.breakingRisk}`);\n if (item.suggestedCommand) {\n lines.push(`- **Suggested command:** \\`${item.suggestedCommand}\\``);\n }\n lines.push('');\n }\n\n return lines.join('\\n');\n}\n", "/**\n * TechStack \u2014 Registry metadata HTTP client.\n *\n * Per-ecosystem registry API clients built on Node's built-in https module.\n * Features: in-memory cache with ETag/TTL, per-host concurrency limit (max 3),\n * exponential backoff on 429/5xx responses.\n *\n * @see docs/specs/techstack-sdd.md \u00A75, \u00A76\n */\n\nimport { get as httpsGet, type RequestOptions } from 'node:https';\nimport type { IncomingMessage } from 'node:http';\nimport { get as httpGet } from 'node:http';\n\n// \u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface RegistryEntry {\n readonly latestStable?: string | undefined;\n readonly license?: string | undefined;\n readonly deprecated?: boolean | undefined;\n readonly yanked?: boolean | undefined;\n readonly retrievedAt: string;\n readonly source: string;\n}\n\nexport interface CacheEntry {\n readonly data: RegistryEntry;\n readonly etag?: string | undefined;\n readonly expiresAt: number;\n}\n\nexport interface HostConcurrency {\n active: number;\n readonly queue: Array<() => void>;\n}\n\n// \u2500\u2500 Constants \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst DEFAULT_TTL_MS = 10 * 60 * 1000; // 10 minutes\nconst MAX_CONCURRENCY_PER_HOST = 3;\nconst MAX_RETRIES = 3;\nconst BASE_BACKOFF_MS = 1000;\n\n// \u2500\u2500 In-memory cache \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst registryCache = new Map<string, CacheEntry>();\nconst hostConcurrency = new Map<string, HostConcurrency>();\n\nfunction getCacheKey(host: string, path: string): string {\n return `${host}${path}`;\n}\n\nfunction getCached(key: string): RegistryEntry | undefined {\n const entry = registryCache.get(key);\n if (!entry) return undefined;\n if (Date.now() > entry.expiresAt) {\n registryCache.delete(key);\n return undefined;\n }\n return entry.data;\n}\n\nfunction setCache(\n key: string,\n data: RegistryEntry,\n etag?: string,\n ttlMs = DEFAULT_TTL_MS,\n): void {\n registryCache.set(key, {\n data,\n etag,\n expiresAt: Date.now() + ttlMs,\n });\n}\n\n// \u2500\u2500 Concurrency limiter \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction acquireHostSlot(host: string): Promise<void> {\n let concurrency = hostConcurrency.get(host);\n if (!concurrency) {\n concurrency = { active: 0, queue: [] };\n hostConcurrency.set(host, concurrency);\n }\n\n if (concurrency.active < MAX_CONCURRENCY_PER_HOST) {\n concurrency.active++;\n return Promise.resolve();\n }\n\n return new Promise<void>((resolve) => {\n concurrency!.queue.push(resolve);\n });\n}\n\nfunction releaseHostSlot(host: string): void {\n const concurrency = hostConcurrency.get(host);\n if (!concurrency) return;\n\n concurrency.active--;\n\n if (concurrency.queue.length > 0) {\n const next = concurrency.queue.shift();\n if (next) {\n concurrency.active++;\n next();\n }\n }\n}\n\n// \u2500\u2500 Backoff helper \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction computeBackoff(attempt: number, statusCode: number): number {\n const base = statusCode === 429 ? BASE_BACKOFF_MS * 2 : BASE_BACKOFF_MS;\n return base * Math.pow(2, attempt) + Math.random() * 500;\n}\n\n// \u2500\u2500 HTTPS/HTTP fetch wrapper \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ninterface FetchResponse {\n readonly statusCode: number;\n readonly headers: Record<string, string | string[] | undefined>;\n readonly body: string;\n readonly isFromCache: boolean;\n}\n\nfunction httpsFetch(\n hostname: string,\n path: string,\n etag?: string,\n signal?: AbortSignal,\n): Promise<FetchResponse> {\n return new Promise((resolve, reject) => {\n const options: RequestOptions = {\n hostname,\n path,\n method: 'GET',\n headers: {\n Accept: 'application/json',\n 'User-Agent': 'WrongStack-TechStack/1.0',\n ...(etag ? { 'If-None-Match': etag } : {}),\n },\n signal,\n timeout: 15000,\n };\n\n const mod = hostname === 'localhost' || hostname === '127.0.0.1' ? httpGet : httpsGet;\n\n const req = mod(options, (res: IncomingMessage) => {\n const statusCode = res.statusCode ?? 0;\n const responseHeaders = res.headers as Record<string, string | string[] | undefined>;\n\n let body = '';\n res.on('data', (chunk: string) => {\n body += chunk;\n });\n res.on('end', () => {\n resolve({\n statusCode,\n headers: responseHeaders,\n body,\n isFromCache: false,\n });\n });\n });\n\n req.on('error', (err: Error) => {\n reject(err);\n });\n\n req.on('timeout', () => {\n req.destroy();\n reject(new Error(`Request timeout for ${hostname}${path}`));\n });\n\n req.end();\n });\n}\n\n// \u2500\u2500 Registry metadata response parser \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ninterface EcosystemFetcher {\n readonly host: string;\n readonly path: (name: string) => string;\n readonly parser: (json: Record<string, unknown>, name: string, ecosystem: string) => RegistryEntry | undefined;\n}\n\n// \u2500\u2500 Per-ecosystem parsers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Parse an npm packument into a {@link RegistryEntry}.\n *\n * Exported so the deprecation rule is unit-testable without a network round\n * trip \u2014 it was untested, and drifted into flagging most of the ecosystem dead.\n */\nexport function parseNpmPackument(json: Record<string, unknown>, name: string): RegistryEntry {\n const latestVersion = (json['dist-tags'] as Record<string, string> | undefined)?.['latest'];\n\n // A package counts as deprecated only when its *latest* version is marked so\n // \u2014 that's what `npm deprecate` leaves behind for a dead package, and it's\n // the signal other tooling reads.\n //\n // Deliberately NOT \"any version in history is deprecated\": every long-lived\n // package eventually deprecates an old beta or a bad patch, so that rule\n // flags essentially the entire mature ecosystem. It marked vitest, biome and\n // cross-env dead in this very repo.\n let deprecated: boolean | undefined;\n if (latestVersion && json.versions && typeof json.versions === 'object') {\n const versions = json.versions as Record<string, Record<string, unknown>>;\n deprecated = versions[latestVersion]?.deprecated ? true : undefined;\n }\n\n // npm has no standard \"yanked\" field in registry metadata.\n return {\n latestStable: latestVersion,\n license: (json.license as string) ?? undefined,\n deprecated: deprecated ?? undefined,\n yanked: undefined,\n retrievedAt: new Date().toISOString(),\n source: `https://registry.npmjs.org/${name}`,\n };\n}\n\n// \u2500\u2500 Per-ecosystem fetcher definitions \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst ECOSYSTEM_FETCHERS: Readonly<Record<string, EcosystemFetcher>> = {\n npm: {\n host: 'registry.npmjs.org',\n path: (name: string) => {\n // Scoped packages: /@scope%2Fname\n const encoded = name.startsWith('@') ? name.replace('/', '%2F') : name;\n return `/${encoded}`;\n },\n parser: parseNpmPackument,\n },\n\n python: {\n host: 'pypi.org',\n path: (name: string) => `/pypi/${name}/json`,\n parser: (json: Record<string, unknown>): RegistryEntry => {\n const info = json.info as Record<string, unknown> | undefined;\n return {\n latestStable: (info?.version as string) ?? undefined,\n license: (info?.license as string) ?? undefined,\n deprecated: (info?.deprecated as boolean) ?? undefined,\n yanked: undefined,\n retrievedAt: new Date().toISOString(),\n source: `https://pypi.org/pypi/${info?.name ?? ''}/json`,\n };\n },\n },\n\n cargo: {\n host: 'crates.io',\n path: (name: string) => `/api/v1/crates/${name}`,\n parser: (json: Record<string, unknown>): RegistryEntry => {\n const crate = json.crate as Record<string, unknown> | undefined;\n return {\n latestStable: (crate?.max_stable_version as string) ?? (crate?.max_version as string) ?? undefined,\n license: (crate?.license as string) ?? undefined,\n deprecated: undefined, // crates.io doesn't have deprecation\n yanked: undefined,\n retrievedAt: new Date().toISOString(),\n source: `https://crates.io/api/v1/crates/${crate?.name ?? ''}`,\n };\n },\n },\n\n golang: {\n host: 'proxy.golang.org',\n path: (module: string) => `/${module}/@latest`,\n parser: (json: Record<string, unknown>, module: string): RegistryEntry => {\n return {\n latestStable: (json.Version as string) ?? undefined,\n license: undefined, // Go proxy doesn't provide license\n deprecated: undefined,\n yanked: undefined,\n retrievedAt: new Date().toISOString(),\n source: `https://proxy.golang.org/${module}/@latest`,\n };\n },\n },\n\n nuget: {\n host: 'api.nuget.org',\n path: (name: string) => {\n const lower = name.toLowerCase();\n return `/v3/registration5-semver1/${lower}/index.json`;\n },\n parser: (json: Record<string, unknown>, name: string): RegistryEntry => {\n // NuGet V3 registration index has items with catalog entries\n const items = json.items as Array<Record<string, unknown>> | undefined;\n let latestStable: string | undefined;\n\n if (items && items.length > 0) {\n // Items are ordered; look through all items for the latest stable version\n for (const item of items) {\n const itemItems = item.items as Array<Record<string, unknown>> | undefined;\n if (itemItems && Array.isArray(itemItems)) {\n for (const entry of itemItems) {\n const catalogEntry = entry.catalogEntry as Record<string, unknown> | undefined;\n if (catalogEntry?.version) {\n const ver = catalogEntry.version as string;\n // Prefer non-prerelease\n if (!latestStable || (!ver.includes('-') && latestStable.includes('-'))) {\n latestStable = ver;\n } else if (!ver.includes('-') && !latestStable.includes('-')) {\n // Both stable \u2014 take greater\n if (ver > latestStable) latestStable = ver;\n }\n }\n }\n }\n }\n }\n\n return {\n latestStable,\n license: undefined, // License requires per-version catalog entry\n deprecated: undefined,\n yanked: undefined,\n retrievedAt: new Date().toISOString(),\n source: `https://api.nuget.org/v3/registration5-semver1/${name.toLowerCase()}/index.json`,\n };\n },\n },\n\n composer: {\n host: 'repo.packagist.org',\n path: (name: string) => `/p2/${name}.json`,\n parser: (json: Record<string, unknown>, name: string): RegistryEntry => {\n const packages = json.packages as Record<string, Array<Record<string, unknown>>> | undefined;\n const versions = packages?.[name];\n if (!versions || versions.length === 0) {\n return { retrievedAt: new Date().toISOString(), source: 'packagist' };\n }\n\n // Find the latest stable version\n let latestStable: string | undefined;\n for (const ver of versions) {\n const version = ver.version as string;\n if (version && !version.includes('dev') && !version.includes('alpha') && !version.includes('beta') && !version.includes('RC') && !version.includes('rc')) {\n if (!latestStable || version > latestStable) {\n latestStable = version;\n }\n }\n }\n\n // Use the latest version entry for license info\n const latest = versions[0]!;\n\n return {\n latestStable,\n license: latest.license as string ?? undefined,\n deprecated: (latest.deprecated as boolean) ?? undefined,\n yanked: (latest.abandoned as boolean) ?? undefined,\n retrievedAt: new Date().toISOString(),\n source: `https://repo.packagist.org/p2/${name}.json`,\n };\n },\n },\n\n pub: {\n host: 'pub.dev',\n path: (name: string) => `/api/packages/${name}`,\n parser: (json: Record<string, unknown>): RegistryEntry => {\n const latest = json.latest as Record<string, unknown> | undefined;\n return {\n latestStable: (json.latestVersion as string) ?? (latest?.version as string) ?? (json.version as string) ?? undefined,\n license: (latest?.license as string) ?? undefined,\n deprecated: (json.isDiscontinued as boolean) ?? undefined,\n yanked: (json.isRetracted as boolean) ?? undefined,\n retrievedAt: new Date().toISOString(),\n source: `https://pub.dev/api/packages/${(json.name as string) ?? ''}`,\n };\n },\n },\n};\n\n// \u2500\u2500 Public API \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface RegistryLookupOptions {\n /** Abort signal for cancellation. */\n readonly signal?: AbortSignal | undefined;\n /** Bypass cache and force a fresh lookup. */\n readonly force?: boolean | undefined;\n}\n\n/**\n * Look up registry metadata for a package in a given ecosystem.\n *\n * Returns `undefined` on 404/401 (private/unresolved package).\n * Throws on network errors (timeout, DNS failure).\n */\nexport async function lookupRegistry(\n ecosystem: string,\n name: string,\n options: RegistryLookupOptions = {},\n): Promise<RegistryEntry | undefined> {\n const fetcher = ECOSYSTEM_FETCHERS[ecosystem];\n if (!fetcher) {\n throw new Error(`Unsupported ecosystem for registry lookup: ${ecosystem}`);\n }\n\n const path = fetcher.path(name);\n const cacheKey = getCacheKey(fetcher.host, path);\n\n // Check cache (unless force refresh)\n if (!options.force) {\n const cached = getCached(cacheKey);\n if (cached) return cached;\n }\n\n // Acquire concurrency slot\n await acquireHostSlot(fetcher.host);\n try {\n // Get cached ETag\n const existingEntry = registryCache.get(cacheKey);\n const etag = existingEntry?.etag;\n\n let lastError: Error | undefined;\n for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {\n try {\n const response = await httpsFetch(fetcher.host, path, etag, options.signal);\n\n // 304 Not Modified \u2014 use cached data and extend TTL\n if (response.statusCode === 304 && existingEntry) {\n setCache(cacheKey, existingEntry.data, existingEntry.etag, DEFAULT_TTL_MS);\n return existingEntry.data;\n }\n\n // 401/403/404 \u2014 private or unresolved package\n if (response.statusCode === 401 || response.statusCode === 403 || response.statusCode === 404) {\n return undefined;\n }\n\n // 429/5xx \u2014 retry with backoff\n if (response.statusCode === 429 || response.statusCode >= 500) {\n if (attempt < MAX_RETRIES - 1) {\n const backoff = computeBackoff(attempt, response.statusCode);\n await sleep(backoff);\n continue;\n }\n throw new Error(`Registry ${fetcher.host} returned ${response.statusCode} after ${MAX_RETRIES} attempts`);\n }\n\n // Success (2xx)\n let json: Record<string, unknown>;\n try {\n json = JSON.parse(response.body) as Record<string, unknown>;\n } catch {\n throw new Error(`Invalid JSON response from ${fetcher.host}${path}`);\n }\n\n const parsed = fetcher.parser(json, name, ecosystem);\n if (!parsed) {\n // Parser returned nothing \u2014 package exists but no metadata\n return undefined;\n }\n\n // Cache with ETag\n const responseEtag = response.headers['etag'] as string | undefined;\n setCache(cacheKey, parsed, responseEtag, DEFAULT_TTL_MS);\n\n return parsed;\n } catch (err) {\n lastError = err instanceof Error ? err : new Error(String(err));\n if (attempt < MAX_RETRIES - 1) {\n const isRateLimit = err instanceof Error && err.message.includes('429');\n const backoff = computeBackoff(attempt, isRateLimit ? 429 : 500);\n await sleep(backoff);\n }\n }\n }\n\n throw lastError ?? new Error(`Failed to look up ${ecosystem}:${name}`);\n } finally {\n releaseHostSlot(fetcher.host);\n }\n}\n\n/**\n * Look up registry metadata for multiple packages in the same ecosystem.\n * Uses the same per-host concurrency limit for the batch.\n */\nexport async function lookupRegistryBatch(\n ecosystem: string,\n names: readonly string[],\n options: RegistryLookupOptions = {},\n): Promise<Map<string, RegistryEntry | undefined>> {\n const results = new Map<string, RegistryEntry | undefined>();\n\n // Process in parallel respecting concurrency limits\n const entries = await Promise.all(\n names.map(async (name) => {\n try {\n const entry = await lookupRegistry(ecosystem, name, options);\n return { name, entry } as const;\n } catch {\n return { name, entry: undefined } as const;\n }\n }),\n );\n\n for (const { name, entry } of entries) {\n results.set(name, entry);\n }\n\n return results;\n}\n\n/**\n * Get the list of supported ecosystem IDs for registry lookups.\n */\nexport function supportedRegistryEcosystems(): string[] {\n return Object.keys(ECOSYSTEM_FETCHERS);\n}\n\n/**\n * Clear the in-memory registry cache.\n * Useful for testing and when force-refreshing.\n */\nexport function clearRegistryCache(): void {\n registryCache.clear();\n hostConcurrency.clear();\n}\n", "/**\n * TechStack \u2014 OSV (Open Source Vulnerabilities) advisory client.\n *\n * Uses the OSV /v1/querybatch endpoint to batch-query vulnerability\n * information for lists of PackageURLs. Chunks requests into batches\n * of at most 500 packages per the OSV API limits.\n *\n * @see https://osv.dev/docs/\n */\n\nimport { get as httpsGet, type RequestOptions } from 'node:https';\nimport type { IncomingMessage } from 'node:http';\nimport type { Evidence } from '../types.js';\n\n// \u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/** OSV query batch request shape */\ninterface OsvQueryBatchRequest {\n readonly queries: ReadonlyArray<{\n readonly package: {\n readonly purl: string;\n };\n }>;\n}\n\n/** OSV query batch response shape */\ninterface OsvQueryBatchResponse {\n readonly results: ReadonlyArray<{\n readonly vulns?: ReadonlyArray<{\n readonly id: string;\n readonly summary?: string;\n readonly details?: string;\n readonly aliases?: readonly string[];\n readonly severity?: ReadonlyArray<{\n readonly type: string;\n readonly score: string;\n }>;\n readonly database_specific?: {\n readonly severity?: string;\n };\n readonly affected?: ReadonlyArray<{\n readonly database_specific?: {\n readonly severity?: string;\n };\n }>;\n }>;\n }>;\n}\n\n/** Parsed advisory for a single package */\nexport interface OsvAdvisory {\n readonly id: string;\n readonly summary: string;\n readonly severity: 'info' | 'low' | 'medium' | 'high' | 'critical';\n readonly aliases: readonly string[];\n}\n\n/** Result of querying OSV for a batch of packages */\nexport interface OsvBatchResult {\n /** Map from PURL to advisories found for that package */\n readonly advisories: Map<string, readonly OsvAdvisory[]>;\n readonly evidence: Evidence;\n}\n\n// \u2500\u2500 Constants \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst OSV_API_BASE = 'api.osv.dev';\nconst OSV_QUERY_BATCH_PATH = '/v1/querybatch';\nconst MAX_BATCH_SIZE = 500;\nconst MAX_RETRIES = 3;\nconst BASE_BACKOFF_MS = 1000;\n\n// \u2500\u2500 Severity mapping \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction mapSeverity(\n osvSeverity?: ReadonlyArray<{ readonly type: string; readonly score: string }>,\n databaseSeverity?: string,\n): 'info' | 'low' | 'medium' | 'high' | 'critical' {\n // Check CVSS score first\n if (osvSeverity && osvSeverity.length > 0) {\n for (const s of osvSeverity) {\n if (s.type === 'CVSS_V3' || s.type === 'CVSS_V2') {\n const score = parseFloat(s.score);\n if (score >= 9.0) return 'critical';\n if (score >= 7.0) return 'high';\n if (score >= 4.0) return 'medium';\n if (score >= 0.1) return 'low';\n }\n }\n }\n\n // Check database_specific severity\n if (databaseSeverity) {\n const ds = databaseSeverity.toLowerCase();\n if (ds === 'critical') return 'critical';\n if (ds === 'high') return 'high';\n if (ds === 'medium' || ds === 'moderate') return 'medium';\n if (ds === 'low') return 'low';\n }\n\n return 'info';\n}\n\n// \u2500\u2500 HTTP client \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction osvPostRequest(body: string, signal?: AbortSignal): Promise<{ statusCode: number; body: string }> {\n return new Promise((resolve, reject) => {\n const options: RequestOptions = {\n hostname: OSV_API_BASE,\n path: OSV_QUERY_BATCH_PATH,\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Content-Length': Buffer.byteLength(body).toString(),\n 'User-Agent': 'WrongStack-TechStack/1.0',\n },\n signal,\n timeout: 30000,\n };\n\n const req = httpsGet(options, (res: IncomingMessage) => {\n const statusCode = res.statusCode ?? 0;\n let responseBody = '';\n res.on('data', (chunk: string) => {\n responseBody += chunk;\n });\n res.on('end', () => {\n resolve({ statusCode, body: responseBody });\n });\n });\n\n req.on('error', (err: Error) => {\n reject(err);\n });\n\n req.on('timeout', () => {\n req.destroy();\n reject(new Error('OSV API request timeout'));\n });\n\n req.write(body);\n req.end();\n });\n}\n\n// \u2500\u2500 Sleep helper \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n// \u2500\u2500 Core function \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Query OSV for advisories matching a list of PackageURLs.\n *\n * Chunks requests into batches of at most 500 PURLs per the OSV API limits.\n * Returns a map from each queried PURL to its list of advisories (empty array\n * means no advisories found).\n */\nexport async function queryOsvBatch(\n purls: readonly string[],\n options: { signal?: AbortSignal | undefined } = {},\n): Promise<OsvBatchResult> {\n const advisories = new Map<string, readonly OsvAdvisory[]>();\n\n // Initialize empty arrays for all PURLs\n for (const purl of purls) {\n advisories.set(purl, []);\n }\n\n // Chunk PURLs into batches\n const batches: string[][] = [];\n for (let i = 0; i < purls.length; i += MAX_BATCH_SIZE) {\n batches.push(purls.slice(i, i + MAX_BATCH_SIZE));\n }\n\n let lastError: Error | undefined;\n\n for (const batch of batches) {\n const requestBody: OsvQueryBatchRequest = {\n queries: batch.map((purl) => ({\n package: { purl },\n })),\n };\n\n const jsonBody = JSON.stringify(requestBody);\n\n let success = false;\n for (let attempt = 0; attempt < MAX_RETRIES && !success; attempt++) {\n try {\n const response = await osvPostRequest(jsonBody, options.signal);\n\n if (response.statusCode === 200) {\n const result = JSON.parse(response.body) as OsvQueryBatchResponse;\n\n if (result.results && Array.isArray(result.results)) {\n for (let i = 0; i < result.results.length; i++) {\n const purl = batch[i];\n if (!purl) continue;\n\n const vulns = result.results[i]?.vulns;\n if (!vulns || vulns.length === 0) continue;\n\n const parsed: OsvAdvisory[] = [];\n for (const vuln of vulns) {\n // Determine severity\n const dbSpecific = vuln.database_specific;\n const affectedDbSpecific = vuln.affected?.[0]?.database_specific;\n const severitySource = dbSpecific?.severity ?? affectedDbSpecific?.severity;\n\n parsed.push({\n id: vuln.id,\n summary: vuln.summary ?? vuln.details ?? 'No summary available',\n severity: mapSeverity(vuln.severity, severitySource),\n aliases: vuln.aliases ?? [],\n });\n }\n\n advisories.set(purl, parsed);\n }\n }\n\n success = true;\n } else if (response.statusCode === 429 || response.statusCode >= 500) {\n // Rate limited or server error \u2014 retry with backoff\n if (attempt < MAX_RETRIES - 1) {\n const backoff = BASE_BACKOFF_MS * Math.pow(2, attempt) + Math.random() * 500;\n await sleep(backoff);\n } else {\n throw new Error(`OSV API returned ${response.statusCode} after ${MAX_RETRIES} attempts: ${response.body}`);\n }\n } else {\n // Other error \u2014 don't retry\n throw new Error(`OSV API returned ${response.statusCode}: ${response.body}`);\n }\n } catch (err) {\n lastError = err instanceof Error ? err : new Error(String(err));\n if (attempt < MAX_RETRIES - 1) {\n const backoff = BASE_BACKOFF_MS * Math.pow(2, attempt) + Math.random() * 500;\n await sleep(backoff);\n }\n }\n }\n\n if (!success && lastError) {\n // If a batch completely fails, we still have partial results from\n // earlier batches\n throw lastError;\n }\n }\n\n const evidence: Evidence = {\n kind: 'osv',\n source: 'https://api.osv.dev/v1/querybatch',\n retrievedAt: new Date().toISOString(),\n detail: `Queried ${purls.length} packages in ${batches.length} batch(es)`,\n };\n\n return { advisories, evidence };\n}\n\n/**\n * Query OSV for a single PURL.\n * Convenience wrapper around queryOsvBatch.\n */\nexport async function queryOsvSingle(\n purl: string,\n options: { signal?: AbortSignal | undefined } = {},\n): Promise<readonly OsvAdvisory[]> {\n const result = await queryOsvBatch([purl], options);\n return result.advisories.get(purl) ?? [];\n}\n", "/**\n * TechStack \u2014 Native audit command wrappers.\n *\n * Spawns ecosystem-native audit tools (npm audit, pip-audit, cargo-audit,\n * govulncheck, composer audit, dotnet package audit) and parses their\n * output into the TechStack advisory model.\n *\n * @see docs/specs/techstack-sdd.md \u00A76, \u00A77\n */\n\nimport { spawnSync, type SpawnSyncOptions } from 'node:child_process';\nimport { existsSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type { EcosystemId, Evidence } from '../types.js';\n\n// \u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface NativeAdvisory {\n readonly id: string;\n readonly packageName: string;\n readonly severity: 'info' | 'low' | 'medium' | 'high' | 'critical';\n readonly summary: string;\n readonly fixVersion?: string | undefined;\n readonly url?: string | undefined;\n readonly aliases: readonly string[];\n}\n\nexport interface NativeAuditResult {\n readonly advisories: readonly NativeAdvisory[];\n readonly evidence: Evidence;\n}\n\n// \u2500\u2500 Parse helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/** Map npm audit severity strings */\nfunction npmSeverity(s: string): NativeAdvisory['severity'] {\n switch (s.toLowerCase()) {\n case 'critical': return 'critical';\n case 'high': return 'high';\n case 'moderate':\n case 'medium': return 'medium';\n case 'low': return 'low';\n default: return 'info';\n }\n}\n\n/** Map cargo-audit severity strings */\nfunction cargoSeverity(s: string): NativeAdvisory['severity'] {\n switch (s.toLowerCase()) {\n case 'critical': return 'critical';\n case 'high': return 'high';\n case 'medium': return 'medium';\n case 'low': return 'low';\n default: return 'info';\n }\n}\n\n// \u2500\u2500 npm audit \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Run npm audit in the given workspace directory and parse JSON output.\n */\nexport function runNpmAudit(workspaceRoot: string): NativeAuditResult {\n const result = runAuditCommand('npm', ['audit', '--json'], workspaceRoot);\n const advisories: NativeAdvisory[] = [];\n let detailLines: string[] = [];\n\n if (result.status === 0 || result.status === 1) {\n // npm audit exits 0 if no vulns, 1 if vulns found, 2 if error\n try {\n const json = JSON.parse(result.stdout || '{}');\n const vulnerabilities = json.vulnerabilities as Record<string, Record<string, unknown>> | undefined;\n\n if (vulnerabilities) {\n for (const [pkg, info] of Object.entries(vulnerabilities)) {\n const via = info.via as Array<Record<string, unknown> | string> | undefined;\n if (!via) continue;\n\n for (const advisory of via) {\n if (typeof advisory === 'string') continue;\n const source = advisory.source as number | undefined;\n const name = advisory.name as string | undefined;\n // npm uses numeric source references \u2014 skip those\n if (typeof source === 'number') continue;\n\n advisories.push({\n id: (advisory.cve as string) ?? (advisory.ghsa as string) ?? `npm-${pkg}-${name ?? 'unknown'}`,\n packageName: pkg,\n severity: npmSeverity((info.severity as string) ?? 'info'),\n summary: (advisory.title as string) ?? (name ?? 'No summary'),\n fixVersion: (info.fixAvailable as string) ?? undefined,\n url: (advisory.url as string) ?? undefined,\n aliases: (advisory.cve as string) ? [(advisory.cve as string)] : [],\n });\n }\n }\n }\n\n const metadata = json.metadata as Record<string, unknown> | undefined;\n if (metadata) {\n detailLines = [\n `Total vulnerabilities: ${metadata.vulnerabilities as string ?? 'unknown'}`,\n `Total dependencies: ${metadata.totalDependencies as string ?? 'unknown'}`,\n ];\n }\n } catch {\n detailLines = ['Failed to parse npm audit JSON output'];\n }\n } else {\n detailLines = [`npm audit exited with code ${result.status}`];\n }\n\n const evidence: Evidence = {\n kind: 'audit',\n source: 'npm audit --json',\n retrievedAt: new Date().toISOString(),\n detail: detailLines.join('\\n') || `Found ${advisories.length} advisories`,\n };\n\n return { advisories, evidence };\n}\n\n// \u2500\u2500 pip-audit \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Run pip-audit in the given workspace directory.\n * pip-audit supports --requirement, --format json flags.\n */\nexport function runPipAudit(workspaceRoot: string): NativeAuditResult {\n // Try common requirements files\n const reqFiles = ['requirements.txt', 'requirements-dev.txt'];\n let reqFlag = '';\n for (const f of reqFiles) {\n if (existsSync(join(workspaceRoot, f))) {\n reqFlag = `--requirement ${f}`;\n break;\n }\n }\n\n const args = ['audit', '--format', 'json'];\n if (reqFlag) {\n args.push(...reqFlag.split(' '));\n }\n\n const result = runAuditCommand('pip-audit' in process.env ? 'pip-audit' : 'pip-audit', args, workspaceRoot);\n return parsePipAuditOutput(result);\n}\n\nfunction parsePipAuditOutput(result: AuditCommandResult): NativeAuditResult {\n const advisories: NativeAdvisory[] = [];\n let detailLines: string[] = [];\n\n if (result.status === 0) {\n try {\n const json = JSON.parse(result.stdout || '[]') as Array<Record<string, unknown>>;\n for (const entry of json) {\n advisories.push({\n id: (entry.id as string) ?? (entry.vulnerability_id as string) ?? 'unknown',\n packageName: (entry.name as string) ?? '',\n severity: npmSeverity((entry.severity as string) ?? 'info'),\n summary: (entry.description as string) ?? (entry.vulnerability_id as string) ?? 'No summary',\n fixVersion: (entry.fix_version as string) ?? undefined,\n url: (entry.advisory_url as string) ?? undefined,\n aliases: (entry.aliases as string[]) ?? [],\n });\n }\n } catch {\n detailLines = ['Failed to parse pip-audit JSON output'];\n }\n } else {\n detailLines = [`pip-audit exited with code ${result.status}: ${result.stderr}`];\n }\n\n const evidence: Evidence = {\n kind: 'audit',\n source: 'pip-audit --format json',\n retrievedAt: new Date().toISOString(),\n detail: detailLines.join('\\n') || `Found ${advisories.length} advisories`,\n };\n\n return { advisories, evidence };\n}\n\n// \u2500\u2500 cargo-audit \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Run cargo-audit in the given workspace directory.\n */\nexport function runCargoAudit(workspaceRoot: string): NativeAuditResult {\n const result = runAuditCommand('cargo', ['audit', '--json'], workspaceRoot);\n return parseCargoAuditOutput(result);\n}\n\nfunction parseCargoAuditOutput(result: AuditCommandResult): NativeAuditResult {\n const advisories: NativeAdvisory[] = [];\n let detailLines: string[] = [];\n\n if (result.status === 0) {\n try {\n const json = JSON.parse(result.stdout || '{}');\n const vulnerabilities = json.vulnerabilities as Record<string, unknown> | undefined;\n const advisoriesList = vulnerabilities?.list as Array<Record<string, unknown>> | undefined;\n\n if (advisoriesList) {\n for (const adv of advisoriesList) {\n const advisory = adv.advisory as Record<string, unknown> | undefined;\n const pkg = adv.package as Record<string, unknown> | undefined;\n if (!advisory) continue;\n\n advisories.push({\n id: (advisory.id as string) ?? 'unknown',\n packageName: (pkg?.name as string) ?? '',\n severity: cargoSeverity((advisory.cvss as string ?? '').split('/')?.[0] ?? 'info'),\n summary: (advisory.title as string) ?? (advisory.description as string) ?? 'No summary',\n fixVersion: (advisory.patched_versions as string) ?? undefined,\n url: (advisory.url as string) ?? undefined,\n aliases: (advisory.aliases as string[]) ?? [],\n });\n }\n }\n } catch {\n detailLines = ['Failed to parse cargo audit JSON output'];\n }\n } else {\n detailLines = [`cargo audit exited with code ${result.status}: ${result.stderr}`];\n }\n\n const evidence: Evidence = {\n kind: 'audit',\n source: 'cargo audit --json',\n retrievedAt: new Date().toISOString(),\n detail: detailLines.join('\\n') || `Found ${advisories.length} advisories`,\n };\n\n return { advisories, evidence };\n}\n\n// \u2500\u2500 govulncheck \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Run govulncheck in the given workspace directory.\n * Output is JSON with vulnerabilities in the format:\n * { vulns: [{ id, details, osv, ... }] }\n */\nexport function runGoVulncheck(workspaceRoot: string): NativeAuditResult {\n const result = runAuditCommand('govulncheck', ['-json'], workspaceRoot);\n const advisories: NativeAdvisory[] = [];\n let detailLines: string[] = [];\n\n if (result.status === 0 || result.status === 3) {\n // govulncheck exits 3 when vulnerabilities found\n try {\n const json = JSON.parse(result.stdout || '{}');\n const vulns = json.vulns as Array<Record<string, unknown>> | undefined;\n\n if (vulns) {\n for (const v of vulns) {\n const osv = v.osv as string | undefined;\n\n advisories.push({\n id: (v.id as string) ?? osv ?? 'unknown',\n packageName: (v.package as string) ?? (v.module_path as string) ?? '',\n severity: 'high', // govulncheck doesn't provide CVSS \u2014 default to high\n summary: (v.details as string) ?? (v.description as string) ?? osv ?? 'No summary',\n fixVersion: (v.fixed_version as string) ?? undefined,\n url: (v.url as string) ?? undefined,\n aliases: osv ? [osv] : [],\n });\n }\n }\n } catch {\n detailLines = ['Failed to parse govulncheck JSON output'];\n }\n } else if (result.status === 1) {\n detailLines = ['govulncheck: no vulnerabilities found'];\n } else {\n detailLines = [`govulncheck exited with code ${result.status}: ${result.stderr}`];\n }\n\n const evidence: Evidence = {\n kind: 'audit',\n source: 'govulncheck -json',\n retrievedAt: new Date().toISOString(),\n detail: detailLines.join('\\n') || `Found ${advisories.length} advisories`,\n };\n\n return { advisories, evidence };\n}\n\n// \u2500\u2500 composer audit \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Run composer audit in the given workspace directory.\n */\nexport function runComposerAudit(workspaceRoot: string): NativeAuditResult {\n const result = runAuditCommand('composer', ['audit', '--format=json'], workspaceRoot);\n const advisories: NativeAdvisory[] = [];\n let detailLines: string[] = [];\n\n if (result.status === 0) {\n try {\n const json = JSON.parse(result.stdout || '{}');\n const advisoriesJson = json.advisories as Record<string, Array<Record<string, unknown>>> | undefined;\n\n if (advisoriesJson) {\n for (const [pkg, advs] of Object.entries(advisoriesJson)) {\n for (const adv of advs) {\n advisories.push({\n id: (adv.cve as string) ?? (adv.reference as string) ?? `composer-${pkg}`,\n packageName: pkg,\n severity: npmSeverity((adv.severity as string) ?? 'medium'),\n summary: (adv.title as string) ?? (adv.description as string) ?? 'No summary',\n fixVersion: adv.link ? (adv.link as string).split('/').pop() : undefined,\n url: (adv.link as string) ?? undefined,\n aliases: (adv.cve as string) ? [(adv.cve as string)] : [],\n });\n }\n }\n }\n } catch {\n detailLines = ['Failed to parse composer audit JSON output'];\n }\n } else {\n detailLines = [`composer audit exited with code ${result.status}: ${result.stderr}`];\n }\n\n const evidence: Evidence = {\n kind: 'audit',\n source: 'composer audit --format=json',\n retrievedAt: new Date().toISOString(),\n detail: detailLines.join('\\n') || `Found ${advisories.length} advisories`,\n };\n\n return { advisories, evidence };\n}\n\n// \u2500\u2500 dotnet package audit \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Run `dotnet package audit` in the given workspace directory.\n * .NET 8+ supports `dotnet package audit --format json`.\n */\nexport function runDotnetAudit(workspaceRoot: string): NativeAuditResult {\n // Try new --format first, fall back to default output\n const result = runAuditCommand('dotnet', ['package', 'audit', '--format', 'json'], workspaceRoot);\n const advisories: NativeAdvisory[] = [];\n let detailLines: string[] = [];\n\n if (result.status === 0) {\n try {\n const json = JSON.parse(result.stdout || '{}');\n const vulnerabilities = json.vulnerabilities as Record<string, unknown> | undefined;\n const packages = json.packages as Record<string, Array<Record<string, unknown>>> | undefined;\n\n // Two possible shapes (different .NET SDK versions)\n if (vulnerabilities) {\n // Flat shape: { vulnerabilities: [{ packageName, severity, advisoryUrl, ... }] }\n const vulnList = Array.isArray(vulnerabilities)\n ? vulnerabilities as Array<Record<string, unknown>>\n : [];\n for (const v of vulnList) {\n advisories.push({\n id: (v.advisoryId as string) ?? (v.id as string) ?? 'unknown',\n packageName: (v.packageName as string) ?? '',\n severity: npmSeverity((v.severity as string) ?? 'info'),\n summary: (v.description as string) ?? (v.title as string) ?? 'No summary',\n fixVersion: (v.fixedVersion as string) ?? (v.patchedVersion as string) ?? undefined,\n url: (v.advisoryUrl as string) ?? (v.url as string) ?? undefined,\n aliases: (v.aliases as string[]) ?? [],\n });\n }\n } else if (packages) {\n // Nested shape: { packages: { \"pkgName\": [{ severity, advisoryUrl, ... }] } }\n for (const [pkg, entries] of Object.entries(packages)) {\n for (const entry of entries) {\n advisories.push({\n id: (entry.advisoryId as string) ?? (entry.id as string) ?? 'unknown',\n packageName: pkg,\n severity: npmSeverity((entry.severity as string) ?? 'info'),\n summary: (entry.description as string) ?? (entry.title as string) ?? 'No summary',\n fixVersion: (entry.fixedVersion as string) ?? (entry.patchedVersion as string) ?? undefined,\n url: (entry.advisoryUrl as string) ?? (entry.url as string) ?? undefined,\n aliases: (entry.aliases as string[]) ?? [],\n });\n }\n }\n }\n } catch {\n detailLines = ['Failed to parse dotnet package audit JSON output'];\n }\n } else {\n detailLines = [`dotnet package audit exited with code ${result.status}: ${result.stderr}`];\n }\n\n const evidence: Evidence = {\n kind: 'audit',\n source: 'dotnet package audit --format json',\n retrievedAt: new Date().toISOString(),\n detail: detailLines.join('\\n') || `Found ${advisories.length} advisories`,\n };\n\n return { advisories, evidence };\n}\n\n// \u2500\u2500 Common command runner \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ninterface AuditCommandResult {\n readonly status: number | null;\n readonly stdout: string;\n readonly stderr: string;\n}\n\nfunction runAuditCommand(\n command: string,\n args: readonly string[],\n cwd: string,\n): AuditCommandResult {\n try {\n const options: SpawnSyncOptions = {\n cwd,\n encoding: 'utf-8' as const,\n timeout: 60000,\n maxBuffer: 10 * 1024 * 1024, // 10MB\n windowsHide: true,\n };\n\n const result = spawnSync(command, args as string[], options);\n\n return {\n status: result.status,\n stdout: result.stdout?.toString() ?? '',\n stderr: result.stderr?.toString() ?? '',\n };\n } catch (err) {\n return {\n status: null,\n stdout: '',\n stderr: err instanceof Error ? err.message : String(err),\n };\n }\n}\n\n// \u2500\u2500 Ecosystem dispatch \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Run the native audit command for the given ecosystem.\n * Returns advisories found by the native tool.\n */\nexport function runNativeAudit(\n ecosystem: EcosystemId,\n workspaceRoot: string,\n): NativeAuditResult {\n switch (ecosystem) {\n case 'npm':\n return runNpmAudit(workspaceRoot);\n case 'python':\n return runPipAudit(workspaceRoot);\n case 'rust':\n return runCargoAudit(workspaceRoot);\n case 'go':\n return runGoVulncheck(workspaceRoot);\n case 'php':\n return runComposerAudit(workspaceRoot);\n case 'dotnet':\n return runDotnetAudit(workspaceRoot);\n // dart/pub doesn't have a standard audit command \u2014 use OSV instead\n default:\n return {\n advisories: [],\n evidence: {\n kind: 'audit',\n source: `native-audit:${ecosystem}`,\n retrievedAt: new Date().toISOString(),\n detail: `No native audit tool for ecosystem: ${ecosystem}`,\n },\n };\n }\n}\n\n// \u2500\u2500 Availability check \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Check if the native audit tool is available for the given ecosystem.\n */\nexport function isNativeAuditAvailable(ecosystem: EcosystemId): boolean {\n const result = runAuditCommand(\n ecosystem === 'npm' ? 'npm' :\n ecosystem === 'python' ? 'pip-audit' :\n ecosystem === 'rust' ? 'cargo' :\n ecosystem === 'go' ? 'govulncheck' :\n ecosystem === 'php' ? 'composer' :\n ecosystem === 'dotnet' ? 'dotnet' : '',\n ['--version'],\n process.cwd(),\n );\n\n return result.status === 0;\n}\n", "/**\n * TechStack \u2014 Status classification policy.\n *\n * Classifies a DependencyObservation's status based on registry metadata,\n * advisory data, and version comparison rules.\n *\n * Key contracts (per SDD R8/R9):\n * - Private/unresolved (404/401) \u2192 `private_or_unresolved` (never `dead` or `deprecated`)\n * - Offline/failed lookup \u2192 `unknown` (never `current`)\n * - Registry says deprecated \u2192 `deprecated`\n * - Registry says yanked \u2192 `yanked`\n * - Advisory found \u2192 `vulnerable`\n *\n * @see docs/specs/techstack-sdd.md \u00A77, R8, R9\n */\n\nimport type { DependencyObservation, DependencyStatus, Evidence } from '../types.js';\n\n// \u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/** Registry metadata used for status classification. */\nexport interface RegistryStatusData {\n readonly latestStable?: string | undefined;\n readonly deprecated?: boolean | undefined;\n readonly yanked?: boolean | undefined;\n /** Whether the registry lookup resulted in a 404/401 (private/unresolved). */\n readonly privateOrUnresolved?: boolean | undefined;\n /** Whether the registry lookup failed due to network error / timeout / offline. */\n readonly lookupFailed?: boolean | undefined;\n /** The evidence from the registry lookup. */\n readonly evidence?: readonly Evidence[] | undefined;\n}\n\n/** Advisory data used for status classification. */\nexport interface AdvisoryStatusData {\n /** Whether any advisory was found for this dependency. */\n readonly hasAdvisory: boolean;\n /** The evidence from the advisory lookup. */\n readonly evidence?: readonly Evidence[] | undefined;\n}\n\n// \u2500\u2500 Version comparison helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Check if a version string looks like a valid semver.\n */\nfunction isValidSemver(version: string): boolean {\n return /^\\d+\\.\\d+\\.\\d+/.test(version);\n}\n\n/**\n * Compare two semver-like version strings.\n * Returns -1 if a < b, 0 if a == b, 1 if a > b.\n * Handles prerelease tags per semver: `1.0.0-alpha < 1.0.0`.\n */\nexport function compareVersions(a: string, b: string): number {\n // Split off prerelease segment(s) from each version\n // Match: numeric segment | alphanumeric prerelease segment\n const aMatch = a.match(/^([^-]+)(?:-(.+))?$/);\n const bMatch = b.match(/^([^-]+)(?:-(.+))?$/);\n const aBase = aMatch?.[1] ?? a;\n const aPre = aMatch?.[2];\n const bBase = bMatch?.[1] ?? b;\n const bPre = bMatch?.[2];\n\n // Compare base segments numerically\n const aBaseParts = aBase.split('.').map(Number);\n const bBaseParts = bBase.split('.').map(Number);\n\n for (let i = 0; i < Math.max(aBaseParts.length, bBaseParts.length); i++) {\n const aNum = aBaseParts[i] ?? 0;\n const bNum = bBaseParts[i] ?? 0;\n if (aNum > bNum) return 1;\n if (aNum < bNum) return -1;\n }\n\n // Base parts equal \u2192 prerelease handling\n if (aPre === bPre) return 0;\n if (aPre === undefined) return 1; // no prerelease > has prerelease\n if (bPre === undefined) return -1; // has prerelease < no prerelease\n // Both have prerelease \u2014 compare dot-separated identifiers\n // Numeric identifiers compare numerically; non-numeric compare lexically.\n const aPreParts = aPre.split('.');\n const bPreParts = bPre.split('.');\n for (let i = 0; i < Math.max(aPreParts.length, bPreParts.length); i++) {\n const aId = aPreParts[i] ?? '';\n const bId = bPreParts[i] ?? '';\n if (aId === bId) continue;\n const aNum = Number(aId);\n const bNum = Number(bId);\n if (!Number.isNaN(aNum) && !Number.isNaN(bNum) && Number.isFinite(aNum) && Number.isFinite(bNum)) {\n if (aNum > bNum) return 1;\n if (aNum < bNum) return -1;\n } else {\n if (aId > bId) return 1;\n if (aId < bId) return -1;\n }\n return aId > bId ? 1 : -1;\n }\n return 0;\n}\n\n/**\n * Check if a constraint string is a simple semver range (^, ~, >=, exact).\n * Returns the expression boundary characters for comparison purposes.\n */\nfunction isSimpleConstraint(constraint: string): boolean {\n return (\n constraint.startsWith('^') ||\n constraint.startsWith('~') ||\n constraint.startsWith('>=') ||\n constraint.startsWith('>') ||\n isValidSemver(constraint)\n );\n}\n\n/**\n * Check if upgrading from `locked` to `latestStable` would be breaking\n * based on the constraint.\n *\n * Simple heuristic:\n * - ^ means compatible (major must match)\n * - ~ means approximately (minor must match)\n * - >= means compatible if major matches\n * - an exact pin is a manifest constraint, not a compatibility one \u2014 still\n * only breaking on a major bump\n */\nfunction isBreakingUpgrade(locked: string, latestStable: string, constraint?: string): boolean {\n const constraintNorm = constraint?.trim() ?? '';\n\n // Get major versions\n const lockedMajor = locked.split('.')[0];\n const latestMajor = latestStable.split('.')[0];\n\n if (!lockedMajor || !latestMajor) return true;\n\n // `^` \u2014 compatible, only breaking if major changes\n if (constraintNorm.startsWith('^')) {\n return lockedMajor !== latestMajor;\n }\n\n // `~` \u2014 approximately equivalent, breaking if minor changes (and we have it)\n if (constraintNorm.startsWith('~')) {\n const lockedMinor = locked.split('.')[1];\n const latestMinor = latestStable.split('.')[1];\n if (lockedMinor && latestMinor && lockedMajor !== latestMajor) return true;\n if (lockedMinor && latestMinor && lockedMinor !== latestMinor) return true;\n return false;\n }\n\n // `>=` \u2014 compatible if major matches\n if (constraintNorm.startsWith('>=') || constraintNorm.startsWith('>')) {\n return lockedMajor !== latestMajor;\n }\n\n // Exact pin (`\"biome\": \"2.5.3\"`). The pin means \"don't move without a\n // decision\", but that is a manifest question, not a compatibility one \u2014\n // breaking is still a major bump. Treating every pinned patch release as\n // breaking flags most of a pin-heavy repo as a major upgrade.\n if (isValidSemver(constraintNorm)) {\n return lockedMajor !== latestMajor;\n }\n\n // Unknown constraint type \u2014 assume breaking\n return lockedMajor !== latestMajor;\n}\n\n// \u2500\u2500 Main classification function \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Classify a dependency's status based on its current data plus optional\n * registry metadata and advisory information.\n *\n * @param dep - The dependency observation from the inventory pass.\n * @param registryData - Optional registry metadata (from lookupRegistry).\n * @param advisoryData - Optional advisory data (from OSV or native audit).\n * @returns The classified DependencyStatus.\n */\nexport function classifyStatus(\n dep: Pick<DependencyObservation, 'name' | 'sourceType' | 'status' | 'locked' | 'requested'>,\n registryData?: RegistryStatusData,\n advisoryData?: AdvisoryStatusData,\n): DependencyStatus {\n // \u2500\u2500 Source-type based classifications \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n // Local path deps\n if (dep.sourceType === 'path') {\n return 'local_path';\n }\n\n // Git deps\n if (dep.sourceType === 'git') {\n return 'git_dependency';\n }\n\n // \u2500\u2500 Private/unresolved (404/401) \u2014 never dead, never deprecated \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n if (registryData?.privateOrUnresolved) {\n return 'private_or_unresolved';\n }\n\n // \u2500\u2500 Lookup failed \u2014 never current, never up-to-date \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n if (registryData?.lookupFailed) {\n return 'unknown';\n }\n\n // \u2500\u2500 Deprecated / yanked \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n if (registryData?.deprecated) {\n return 'deprecated';\n }\n\n if (registryData?.yanked) {\n return 'yanked';\n }\n\n // \u2500\u2500 Vulnerable \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n if (advisoryData?.hasAdvisory) {\n return 'vulnerable';\n }\n\n // \u2500\u2500 Version comparison \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n const locked = dep.locked;\n const latestStable = registryData?.latestStable;\n\n if (locked && latestStable) {\n if (locked === latestStable) {\n return 'current';\n }\n\n try {\n const cmp = compareVersions(locked, latestStable);\n if (cmp < 0) {\n // locked < latestStable\n const constraint = dep.requested;\n if (constraint && isSimpleConstraint(constraint)) {\n const breaking = isBreakingUpgrade(locked, latestStable, constraint);\n return breaking ? 'update_available_breaking' : 'update_available_safe';\n }\n // No constraint or complex constraint \u2014 conservative: assume safe\n return 'update_available_safe';\n }\n // locked > latestStable \u2014 this shouldn't normally happen for registry deps\n // but handle gracefully as current\n return 'current';\n } catch {\n // Version comparison failed \u2014 fall through to registry-based status\n }\n }\n\n // \u2500\u2500 Holdover status from inventory pass \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n // If the adapter already classified this (e.g. local_path, git_dependency),\n // respect that classification\n if (dep.status === 'local_path' || dep.status === 'git_dependency') {\n return dep.status;\n }\n\n // \u2500\u2500 Default \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n return dep.status ?? 'current';\n}\n\n/**\n * Create a registry status data object indicating a private/unresolved package.\n */\nexport function privateOrUnresolvedStatus(\n source: string,\n detail?: string,\n): RegistryStatusData {\n return {\n privateOrUnresolved: true,\n evidence: [\n {\n kind: 'registry',\n source,\n retrievedAt: new Date().toISOString(),\n detail: detail ?? 'Package returned 404/401 \u2014 private or unresolved',\n },\n ],\n };\n}\n\n/**\n * Create a registry status data object indicating a failed/offline lookup.\n */\nexport function failedLookupStatus(\n source: string,\n error?: string,\n): RegistryStatusData {\n return {\n lookupFailed: true,\n evidence: [\n {\n kind: 'registry',\n source,\n retrievedAt: new Date().toISOString(),\n detail: error ?? 'Registry lookup failed \u2014 network error or timeout',\n },\n ],\n };\n}\n", "/**\n * TechStack \u2014 Public service API.\n *\n * Wires together inventory, online enrichment (registry + OSV + native audit),\n * status classification, and persistence into a single async job flow.\n *\n * @see docs/specs/techstack-sdd.md \u00A74.2\n */\n\nimport { randomUUID } from 'node:crypto';\nimport type {\n Coverage,\n DependencyObservation,\n EcosystemId,\n Evidence,\n Finding,\n Snapshot,\n TechStackJob,\n TechStackJobProgress,\n TechStackJobStatus,\n Workspace,\n} from './types.js';\nimport type { EcosystemAdapter } from './adapters/interface.js';\nimport { lookupRegistry } from './registry/client.js';\nimport type { RegistryEntry } from './registry/client.js';\nimport { queryOsvBatch } from './advisory/osv.js';\nimport { classifyStatus } from './policy/status.js';\nimport type { RegistryStatusData, AdvisoryStatusData } from './policy/status.js';\nimport type { TechStackStore } from './store/sqlite.js';\nimport { discoverWorkspaces } from './discovery/workspace.js';\nimport { triageCandidates } from './research/triage.js';\nimport type { TechStackResearcher } from './research/types.js';\n\n// \u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface EnrichOptions {\n /** Skip network calls; use only cached/offline data. */\n readonly online?: boolean | undefined;\n /** Abort signal for cancellation. */\n readonly signal?: AbortSignal | undefined;\n /** Force re-fetch registry data (ignore cache). */\n readonly forceRegistryRefresh?: boolean | undefined;\n}\n\nexport interface AnalyzeOptions {\n /** Where to start the analysis. */\n readonly targetRoot: string;\n /** Session ID for job tracking. */\n readonly sessionId?: string | undefined;\n /** Requesting entity. */\n readonly requestedBy?: string | undefined;\n /** Enable online enrichment (registry + advisory). */\n readonly online?: boolean | undefined;\n /** Auto-deliver report when complete. */\n readonly autoDeliver?: boolean | undefined;\n /** Optional caller-assigned id so HTTP/WS clients can track the job immediately. */\n readonly jobId?: string | undefined;\n /** Abort signal used by cancel endpoints. */\n readonly signal?: AbortSignal | undefined;\n /** Progress callback for WebSocket projection. */\n readonly onProgress?: ((phase: string, completed: number, total: number) => void) | undefined;\n /**\n * LLM interpretation stage. Omit it and `analyze()` stays a purely\n * deterministic tool \u2014 research is strictly additive enrichment.\n *\n * @see docs/specs/techstack-sdd.md \u00A731\n */\n readonly researcher?: TechStackResearcher | undefined;\n /** Cap on packages sent to research. Defaults to `DEFAULT_TRIAGE_LIMIT`. */\n readonly researchLimit?: number | undefined;\n}\n\n// \u2500\u2500 Adapter registry \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nimport { npmAdapter } from './adapters/npm.js';\nimport { pythonAdapter } from './adapters/python.js';\nimport { rustAdapter } from './adapters/rust.js';\nimport { goAdapter } from './adapters/go.js';\nimport { dotNetAdapter } from './adapters/dotnet.js';\nimport { phpAdapter } from './adapters/php.js';\nimport { dartAdapter } from './adapters/dart.js';\nimport { mavenAdapter } from './adapters/maven.js';\nimport { rubyAdapter } from './adapters/ruby.js';\nimport { elixirAdapter } from './adapters/elixir.js';\nimport { cppAdapter } from './adapters/cpp.js';\n\nfunction getAdapter(ecosystem: EcosystemId): EcosystemAdapter | undefined {\n switch (ecosystem) {\n case 'npm': return npmAdapter;\n case 'python': return pythonAdapter;\n case 'rust': return rustAdapter;\n case 'go': return goAdapter;\n case 'dotnet': return dotNetAdapter;\n case 'php': return phpAdapter;\n case 'dart': return dartAdapter;\n // Tier B \u2014 partial support\n case 'maven': return mavenAdapter;\n case 'gradle': return mavenAdapter; // reuse Maven adapter (same manifest family)\n case 'ruby': return rubyAdapter;\n case 'swift': return undefined; // no adapter yet\n case 'elixir': return elixirAdapter;\n // Tier C \u2014 best-effort\n case 'cpp': return cppAdapter;\n default: return undefined;\n }\n}\n\n// \u2500\u2500 Version constant \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst ADAPTER_VERSION = '0.1.0';\n\n// \u2500\u2500 TechStack Engine \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport class TechStackEngine {\n private store: TechStackStore;\n\n constructor(store: TechStackStore) {\n this.store = store;\n }\n\n // \u2500\u2500 Inventory \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n /**\n * Run an offline inventory: discover workspaces and parse dependencies\n * from manifests and lockfiles. No network calls.\n *\n * Returns a Snapshot with workspace and dependency data but no\n * registry/advisory enrichment.\n */\n async inventory(\n projectId: string,\n targetRoot: string,\n _jobId?: string,\n onProgress?: (phase: string, completed: number, total: number) => void,\n ): Promise<Snapshot> {\n const snapshotId = randomUUID();\n\n // Phase 1: Discover workspaces\n onProgress?.('discovering', 0, 1);\n const rawWorkspaces = await discoverWorkspaces(targetRoot);\n\n // Map into our Workspace type\n const workspaces: Workspace[] = rawWorkspaces.map((w) => ({\n id: w.id,\n relativeRoot: w.relativeRoot,\n ecosystem: w.ecosystem,\n packageManager: w.packageManager,\n manifests: [...w.manifests],\n lockfiles: [...w.lockfiles],\n confidence: w.confidence,\n coverage: w.coverage as Coverage,\n }));\n\n // Phase 2: Inventory per workspace\n onProgress?.('inventorying', 0, workspaces.length);\n const allDependencies: DependencyObservation[] = [];\n let totalCoverage: Coverage = 'full';\n\n for (let i = 0; i < workspaces.length; i++) {\n const ws = workspaces[i]!;\n const adapter = getAdapter(ws.ecosystem);\n let deps: readonly DependencyObservation[] = [];\n\n if (adapter) {\n try {\n // `targetRoot` is the absolute base every adapter resolves\n // `ws.relativeRoot` against. Without it they fall back to\n // `process.cwd()` and silently inventory nothing.\n deps = await adapter.inventory(ws, { projectRoot: targetRoot });\n } catch {\n deps = [];\n }\n }\n\n // Track coverage\n if (ws.coverage === 'unsupported') {\n totalCoverage = 'partial';\n }\n\n allDependencies.push(...deps);\n onProgress?.('inventorying', i + 1, workspaces.length);\n }\n\n // Compute fingerprint\n const fingerprint = computeFingerprint(allDependencies);\n\n const snapshot: Snapshot = {\n id: snapshotId,\n projectId,\n targetRoot,\n fingerprint,\n createdAt: new Date().toISOString(),\n workspaces,\n dependencies: allDependencies,\n findings: [],\n coverage: totalCoverage,\n adapterVersion: ADAPTER_VERSION,\n };\n\n // Persist\n this.store.saveSnapshot(snapshot);\n\n return snapshot;\n }\n\n // \u2500\u2500 Enrichment \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n /**\n * Enrich a snapshot with registry metadata and advisory data.\n *\n * This is the online pass: fetches latest versions, licenses, deprecation\n * status from registries, and queries OSV for advisories.\n *\n * Key contracts:\n * - 404/401 \u2192 status `private_or_unresolved` (never `dead` or `deprecated`)\n * - Network failure \u2192 status `unknown` with evidence detail (never `current`)\n */\n async enrich(\n snapshot: Snapshot,\n options: EnrichOptions = {},\n ): Promise<Snapshot> {\n const isOnline = options.online !== false;\n if (!isOnline || options.signal?.aborted) {\n return snapshot;\n }\n\n // Group dependencies by ecosystem for batch lookups\n const byEcosystem = new Map<EcosystemId, DependencyObservation[]>();\n for (const dep of snapshot.dependencies) {\n // Skip local/git deps \u2014 they have no registry metadata\n if (dep.sourceType === 'path' || dep.sourceType === 'git') continue;\n if (!dep.purl) continue;\n\n const list = byEcosystem.get(dep.ecosystem);\n if (list) {\n list.push(dep);\n } else {\n byEcosystem.set(dep.ecosystem, [dep]);\n }\n }\n\n // Enrich each ecosystem\n const enrichedDeps = new Map<string, DependencyObservation>();\n const allFindings: Finding[] = [...snapshot.findings];\n\n for (const [ecosystem, deps] of byEcosystem) {\n // \u2500\u2500 Registry lookup \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n const names = [...new Set(deps.map((d) => d.name))];\n\n for (const name of names) {\n const lookupOpts: { signal?: AbortSignal; force?: boolean } = {};\n if (options.signal) lookupOpts.signal = options.signal;\n if (options.forceRegistryRefresh) lookupOpts.force = true;\n\n let registryEntry: RegistryEntry | undefined;\n let registryStatus: RegistryStatusData | undefined;\n\n try {\n registryEntry = await lookupRegistry(ecosystem, name, lookupOpts);\n\n if (registryEntry) {\n // Successful lookup\n registryStatus = {\n latestStable: registryEntry.latestStable,\n deprecated: registryEntry.deprecated,\n yanked: registryEntry.yanked,\n evidence: [\n {\n kind: 'registry',\n source: registryEntry.source,\n retrievedAt: registryEntry.retrievedAt,\n detail: `latestStable: ${registryEntry.latestStable ?? 'N/A'}, license: ${registryEntry.license ?? 'N/A'}`,\n },\n ],\n };\n } else {\n // 401/403/404 \u2014 private or unresolved\n registryStatus = {\n privateOrUnresolved: true,\n evidence: [\n {\n kind: 'registry',\n source: `${ecosystem} registry for ${name}`,\n retrievedAt: new Date().toISOString(),\n detail: 'Package returned 404/401 \u2014 private or unresolved',\n },\n ],\n };\n }\n } catch (err) {\n // Network error / timeout / offline\n registryStatus = {\n lookupFailed: true,\n evidence: [\n {\n kind: 'registry',\n source: `${ecosystem} registry for ${name}`,\n retrievedAt: new Date().toISOString(),\n detail: err instanceof Error ? err.message : 'Registry lookup failed',\n },\n ],\n };\n }\n\n // \u2500\u2500 OSV advisory \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n let advisoryStatus: AdvisoryStatusData | undefined;\n\n try {\n // Build PURL for OSV query\n const depList = deps.filter((d) => d.name === name);\n const purls = depList\n .map((d) => d.purl)\n .filter((p): p is string => !!p);\n\n if (purls.length > 0) {\n const osvResult = await queryOsvBatch(purls, { signal: options.signal });\n\n const hasAdvisory = [...osvResult.advisories.values()].some(\n (advisories) => advisories.length > 0,\n );\n\n if (hasAdvisory) {\n advisoryStatus = {\n hasAdvisory: true,\n };\n }\n }\n } catch {\n // OSV failure \u2014 don't block enrichment, just skip advisory\n }\n\n // \u2500\u2500 Apply status classification to matching deps \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n for (const dep of deps) {\n if (dep.name !== name) continue;\n\n const newStatus = classifyStatus(dep, registryStatus, advisoryStatus);\n const newEvidence: Evidence[] = [\n ...dep.evidence,\n ...(registryStatus?.evidence ?? []),\n ...(advisoryStatus?.evidence ?? []),\n ];\n\n enrichedDeps.set(dep.id, {\n ...dep,\n latestStable: registryEntry?.latestStable ?? dep.latestStable,\n license: registryEntry?.license ?? dep.license,\n deprecated: registryEntry?.deprecated ?? dep.deprecated,\n yanked: registryEntry?.yanked ?? dep.yanked,\n status: newStatus,\n evidence: newEvidence,\n });\n\n // Generate findings for non-current statuses\n if (newStatus !== 'current' && newStatus !== 'local_path' && newStatus !== 'git_dependency') {\n allFindings.push(\n createFindingForStatus(dep.id, newStatus, registryEntry?.license),\n );\n }\n }\n }\n }\n\n // Build enriched dependency list, preserving un-enriched deps\n const finalDependencies = snapshot.dependencies.map(\n (dep) => enrichedDeps.get(dep.id) ?? dep,\n );\n\n return {\n ...snapshot,\n dependencies: finalDependencies,\n findings: allFindings,\n };\n }\n\n // \u2500\u2500 Research \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n /**\n * Interpret an enriched snapshot with the LLM: triage the problem cases,\n * research them, and append the resulting findings.\n *\n * Additive by construction. Two invariants hold here, and they are the whole\n * reason this is a separate pass rather than part of `enrich()`:\n *\n * 1. **`snapshot.dependencies` is returned untouched.** Version facts come\n * only from registry evidence \u2014 the LLM cannot fabricate a `latestStable`\n * because `Finding` has nowhere to put one (SDD \u00A7472).\n * 2. **Failure is not fatal.** No researcher, no candidates, a provider\n * outage, a dry web search \u2014 every one of them returns the input snapshot\n * unchanged. A deterministic report is the floor, never a casualty of the\n * optional stage above it.\n *\n * @see docs/specs/techstack-sdd.md \u00A731, \u00A7472\n */\n async research(\n snapshot: Snapshot,\n options: {\n researcher?: TechStackResearcher | undefined;\n researchLimit?: number | undefined;\n signal?: AbortSignal | undefined;\n /** Only ever emits the two research phases \u2014 narrow so callers can feed\n * `updateJob` without casting. */\n onProgress?:\n | ((phase: 'researching' | 'synthesizing', completed: number, total: number) => void)\n | undefined;\n } = {},\n ): Promise<Snapshot> {\n if (!options.researcher || options.signal?.aborted) return snapshot;\n\n const candidates = triageCandidates(snapshot.dependencies, {\n limit: options.researchLimit,\n });\n if (candidates.length === 0) return snapshot;\n\n options.onProgress?.('researching', 0, candidates.length);\n\n let findings: readonly Finding[];\n try {\n findings = await options.researcher.research(candidates, {\n signal: options.signal,\n onProgress: (completed, total) => {\n // Cluster-level progress, reported against the phase the UI shows.\n options.onProgress?.('researching', completed, total);\n },\n });\n } catch {\n // The deterministic snapshot stands on its own.\n return snapshot;\n }\n\n options.onProgress?.('synthesizing', 1, 1);\n if (findings.length === 0) return snapshot;\n\n // Deterministic findings first \u2014 facts outrank interpretations in the\n // order the report and the UI render them.\n return { ...snapshot, findings: [...snapshot.findings, ...findings] };\n }\n\n // \u2500\u2500 Analyze \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n /**\n * Run a full analysis: inventory + enrich + research + persist.\n * This is the main entry point for the analyze job flow.\n */\n async analyze(\n projectId: string,\n options: AnalyzeOptions,\n ): Promise<{ snapshot: Snapshot; job: TechStackJob }> {\n const jobId = options.jobId ?? randomUUID();\n const requestedBy = options.requestedBy ?? 'system';\n\n // Create job\n const job: TechStackJob = {\n id: jobId,\n projectId,\n targetRoot: options.targetRoot,\n kind: 'analyze',\n status: 'queued',\n fingerprint: '',\n requestedBy,\n sessionId: options.sessionId,\n createdAt: new Date().toISOString(),\n progress: { phase: 'queued', completed: 0, total: 0 },\n };\n this.store.saveJob(job);\n\n const updateJob = (status: TechStackJobStatus, progress?: TechStackJobProgress) => {\n this.store.updateJobStatus(jobId, status, progress);\n if (progress) options.onProgress?.(progress.phase, progress.completed, progress.total);\n };\n const throwIfAborted = (): void => {\n if (options.signal?.aborted) throw new DOMException('TechStack job cancelled', 'AbortError');\n };\n\n try {\n throwIfAborted();\n // Phase 1: Inventory (offline)\n updateJob('discovering', { phase: 'discovering', completed: 0, total: 1 });\n const snapshot = await this.inventory(\n projectId,\n options.targetRoot,\n jobId,\n (phase, completed, total) => {\n throwIfAborted();\n updateJob(phase as TechStackJobStatus, { phase, completed, total });\n },\n );\n throwIfAborted();\n\n // Phase 2: Enrich (online, if enabled)\n const isOnline = options.online !== false;\n if (isOnline) {\n updateJob('enriching', { phase: 'enriching', completed: 0, total: 1 });\n const enriched = await this.enrich(snapshot, {\n online: true,\n signal: options.signal,\n });\n throwIfAborted();\n\n // Phase 3: Research (LLM interpretation) \u2014 additive and optional.\n const researched = await this.research(enriched, {\n researcher: options.researcher,\n researchLimit: options.researchLimit,\n signal: options.signal,\n onProgress: (phase, completed, total) => {\n updateJob(phase, { phase, completed, total });\n },\n });\n throwIfAborted();\n\n // Persist enriched snapshot\n this.store.saveSnapshot(researched);\n\n updateJob('completed', { phase: 'completed', completed: 1, total: 1 });\n\n return {\n snapshot: researched,\n job: { ...job, status: 'completed', completedAt: new Date().toISOString() },\n };\n }\n\n // Offline mode \u2014 persist inventory-only snapshot\n this.store.saveSnapshot(snapshot);\n updateJob('completed', { phase: 'completed', completed: 1, total: 1 });\n\n return {\n snapshot,\n job: { ...job, status: 'completed', completedAt: new Date().toISOString() },\n };\n } catch (err) {\n if (options.signal?.aborted || (err instanceof DOMException && err.name === 'AbortError')) {\n updateJob('cancelled');\n } else {\n updateJob('failed');\n }\n throw err;\n }\n }\n // \u2500\u2500 Report generation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n /**\n * Generate a human-readable report from a snapshot.\n *\n * @param format 'md' for Markdown, 'json' for raw JSON.\n * @returns The report as a string.\n */\n generateReport(snapshot: Snapshot, format: 'md' | 'json' = 'md'): string {\n if (format === 'json') return JSON.stringify(snapshot, null, 2);\n\n const lines: string[] = [\n '# TechStack Report',\n '',\n `**Generated:** ${snapshot.createdAt}`,\n `**Target:** ${snapshot.targetRoot}`,\n `**Fingerprint:** ${snapshot.fingerprint}`,\n `**Workspaces:** ${snapshot.workspaces.length}`,\n `**Dependencies:** ${snapshot.dependencies.length}`,\n `**Findings:** ${snapshot.findings.length}`,\n `**Coverage:** ${snapshot.coverage}`,\n '',\n ];\n\n // Workspaces\n if (snapshot.workspaces.length > 0) {\n lines.push('## Workspaces', '');\n lines.push('| Workspace | Ecosystem | Coverage | Deps |');\n lines.push('|---|---|---|---|');\n for (const ws of snapshot.workspaces) {\n const depCount = snapshot.dependencies.filter((d) => d.workspaceId === ws.id).length;\n lines.push(`| ${ws.relativeRoot} | ${ws.ecosystem} | ${ws.coverage} | ${depCount} |`);\n }\n lines.push('');\n }\n\n // Findings by severity\n const findings = snapshot.findings as ReadonlyArray<{\n id: string;\n type: string;\n severity: string;\n action: string;\n rationale: string;\n dependencyId: string;\n }>;\n if (findings.length > 0) {\n lines.push('## Findings', '');\n const bySeverity = new Map<string, Array<(typeof findings)[number]>>();\n for (const f of findings) {\n const list = bySeverity.get(f.severity) ?? [];\n list.push(f);\n bySeverity.set(f.severity, list);\n }\n for (const sev of ['critical', 'high', 'medium', 'low', 'info']) {\n const items = bySeverity.get(sev);\n if (!items || items.length === 0) continue;\n lines.push(`### ${sev.charAt(0).toUpperCase() + sev.slice(1)} (${items.length})`, '');\n for (const f of items) {\n const dep = snapshot.dependencies.find((d) => d.id === f.dependencyId);\n lines.push(`- **${dep?.name ?? f.dependencyId}** \u2014 ${f.type} \u2014 ${f.rationale}`);\n }\n lines.push('');\n }\n }\n\n // Dependencies summary\n if (snapshot.dependencies.length > 0) {\n lines.push('## Dependencies', '');\n lines.push('| Name | Ecosystem | Status | Locked | Latest |');\n lines.push('|---|---|---|---|---|');\n for (const dep of snapshot.dependencies) {\n lines.push(\n `| ${dep.name} | ${dep.ecosystem} | ${dep.status} | ${dep.locked ?? '\u2014'} | ${dep.latestStable ?? '\u2014'} |`,\n );\n }\n }\n\n return lines.join('\\n');\n }\n}\n\n// \u2500\u2500 Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction computeFingerprint(dependencies: readonly DependencyObservation[]): string {\n const parts = dependencies\n .map((d) => `${d.name}@${d.locked ?? d.requested ?? 'unknown'}`)\n .sort()\n .join(',');\n let hash = 0;\n for (let i = 0; i < parts.length; i++) {\n const char = parts.charCodeAt(i);\n hash = ((hash << 5) - hash) + char;\n hash |= 0;\n }\n return `ts-${Math.abs(hash).toString(36)}`;\n}\n\nfunction createFindingForStatus(\n dependencyId: string,\n status: string,\n _license?: string,\n): Finding {\n switch (status) {\n case 'vulnerable':\n return {\n id: `finding-${dependencyId}-vuln`,\n dependencyId,\n type: 'vulnerability',\n severity: 'high',\n action: 'upgrade_patch',\n confidence: 1.0,\n rationale: 'Known security advisory found for this package',\n evidence: [],\n };\n case 'deprecated':\n return {\n id: `finding-${dependencyId}-dep`,\n dependencyId,\n type: 'deprecated',\n severity: 'medium',\n action: 'replace',\n confidence: 1.0,\n rationale: 'Package is deprecated in the registry',\n evidence: [],\n };\n case 'yanked':\n return {\n id: `finding-${dependencyId}-yank`,\n dependencyId,\n type: 'deprecated',\n severity: 'high',\n action: 'replace',\n confidence: 1.0,\n rationale: 'Package version has been yanked from the registry',\n evidence: [],\n };\n case 'update_available_safe':\n return {\n id: `finding-${dependencyId}-update`,\n dependencyId,\n type: 'upgrade',\n severity: 'info',\n action: 'upgrade_minor',\n confidence: 1.0,\n rationale: 'A newer compatible version is available',\n evidence: [],\n };\n case 'update_available_breaking':\n return {\n id: `finding-${dependencyId}-major`,\n dependencyId,\n type: 'upgrade',\n severity: 'low',\n action: 'upgrade_major',\n confidence: 1.0,\n rationale: 'A newer version is available that may require breaking changes',\n evidence: [],\n };\n default:\n return {\n id: `finding-${dependencyId}-investigate`,\n dependencyId,\n type: 'investigate',\n severity: 'info',\n action: 'investigate',\n confidence: 0.5,\n rationale: `Package status is \"${status}\" \u2014 may need investigation`,\n evidence: [],\n };\n }\n}\n", "/**\n * TechStack \u2014 Deterministic research triage.\n *\n * Decides *which* dependencies are worth an LLM call. No LLM, no network.\n *\n * This is the stage that makes research affordable: a monorepo resolves\n * thousands of dependencies, the overwhelming majority of which are `current`\n * and need no interpretation at all. Only statuses where the registry has\n * already told us something is wrong \u2014 but not what to do about it \u2014 earn a\n * research slot.\n *\n * @see docs/specs/techstack-sdd.md \u00A731, \u00A7557\n */\n\nimport type { DependencyObservation, DependencyStatus } from '../types.js';\nimport type { ResearchCluster, TriageCandidate } from './types.js';\n\n/** Default cap on researched packages. Keeps a full analyze bounded. */\nexport const DEFAULT_TRIAGE_LIMIT = 40;\n\n/**\n * Which cluster (if any) a status belongs to.\n *\n * Statuses deliberately absent: `current` (nothing to interpret),\n * `update_available_safe` (the registry already answered \u2014 a safe bump needs\n * no essay), `private_or_unresolved` / `unknown` (we have no facts to reason\n * from, so an LLM would only speculate), `local_path` / `git_dependency` /\n * `unsupported` (no registry identity to research).\n */\nconst CLUSTER_BY_STATUS: Partial<Record<DependencyStatus, ResearchCluster>> = {\n vulnerable: 'vulnerability',\n yanked: 'replacement',\n deprecated: 'replacement',\n unmaintained_suspected: 'replacement',\n update_available_breaking: 'breaking_change',\n};\n\n/**\n * Base priority per status \u2014 severity order. Tuned so that a transitive\n * vulnerability still outranks a direct major-version bump: being exploitable\n * matters more than being behind.\n */\nconst PRIORITY_BY_STATUS: Partial<Record<DependencyStatus, number>> = {\n vulnerable: 100,\n yanked: 80,\n deprecated: 60,\n update_available_breaking: 40,\n unmaintained_suspected: 30,\n};\n\nconst DIRECT_BONUS = 10;\nconst RUNTIME_BONUS = 5;\n\nfunction priorityFor(dep: DependencyObservation): number {\n const base = PRIORITY_BY_STATUS[dep.status] ?? 0;\n const direct = dep.direct ? DIRECT_BONUS : 0;\n const runtime = dep.scope === 'runtime' ? RUNTIME_BONUS : 0;\n return base + direct + runtime;\n}\n\n/**\n * Dedup key. A monorepo hoists the same package into many workspaces; they\n * share one registry identity and one answer, so researching `lodash@4.17.20`\n * twelve times would burn twelve LLM calls for one insight.\n *\n * Keyed on the resolved version too \u2014 `react@17` and `react@18` in the same\n * repo are genuinely different questions.\n */\nfunction dedupKey(dep: DependencyObservation): string {\n return `${dep.ecosystem}\u0000${dep.name}\u0000${dep.locked ?? dep.requested ?? ''}`;\n}\n\nexport interface TriageOptions {\n /** Max candidates returned. Defaults to {@link DEFAULT_TRIAGE_LIMIT}. */\n readonly limit?: number | undefined;\n}\n\n/**\n * Select and rank the dependencies worth researching.\n *\n * Ordering is fully deterministic \u2014 priority, then name, then version \u2014 so the\n * same snapshot always triages identically. Ties resolve by name rather than\n * by input order because adapter output order is not itself guaranteed stable\n * across platforms, and a wobbling triage would make the cap non-reproducible.\n */\nexport function triageCandidates(\n dependencies: readonly DependencyObservation[],\n options: TriageOptions = {},\n): readonly TriageCandidate[] {\n const limit = Math.max(0, options.limit ?? DEFAULT_TRIAGE_LIMIT);\n if (limit === 0) return [];\n\n const best = new Map<string, TriageCandidate>();\n\n for (const dependency of dependencies) {\n const cluster = CLUSTER_BY_STATUS[dependency.status];\n if (!cluster) continue;\n // Path/git deps have no registry identity to research even if some adapter\n // left a researchable status on them.\n if (dependency.sourceType === 'path' || dependency.sourceType === 'git') continue;\n\n const candidate: TriageCandidate = {\n dependency,\n cluster,\n priority: priorityFor(dependency),\n };\n\n const key = dedupKey(dependency);\n const existing = best.get(key);\n // Keep the highest-priority instance \u2014 the direct/runtime copy wins over\n // the transitive one, so the finding lands on the dep the user can act on.\n if (!existing || candidate.priority > existing.priority) {\n best.set(key, candidate);\n }\n }\n\n return [...best.values()]\n .sort(\n (a, b) =>\n b.priority - a.priority ||\n a.dependency.name.localeCompare(b.dependency.name) ||\n (a.dependency.locked ?? '').localeCompare(b.dependency.locked ?? ''),\n )\n .slice(0, limit);\n}\n\n/** Group triaged candidates by cluster, preserving triage order within each. */\nexport function clusterCandidates(\n candidates: readonly TriageCandidate[],\n): ReadonlyMap<ResearchCluster, readonly TriageCandidate[]> {\n const out = new Map<ResearchCluster, TriageCandidate[]>();\n for (const candidate of candidates) {\n const list = out.get(candidate.cluster);\n if (list) list.push(candidate);\n else out.set(candidate.cluster, [candidate]);\n }\n return out;\n}\n", "/**\n * TechStack \u2014 `Provider` \u2192 {@link ResearchLlm} adapter.\n *\n * Mirrors the capability-probing pattern established by the WebUI completion\n * handler (`packages/webui-server/src/server/completion-handlers.ts`,\n * `loadLlmSuggestions`): prefer strict structured output, fall back to JSON\n * mode, fall back again to prompt-only discipline. Providers differ, and a\n * research pass must not be exclusive to the ones with schema support.\n *\n * @see docs/specs/techstack-sdd.md \u00A74.2\n */\n\nimport type { Provider, Request } from '@wrongstack/core';\nimport type { ResearchLlm, ResearchLlmRequest } from './types.js';\n\n/** How the caller reaches a live provider. A getter, not a captured value \u2014\n * the user can switch model or rotate credentials mid-session, and a snapshot\n * of the provider would silently go stale. */\nexport type LlmAccessor = () => { provider: Provider; model: string } | undefined;\n\nconst DEFAULT_TIMEOUT_MS = 45_000;\n\n/**\n * Build a {@link ResearchLlm} from a provider accessor, or `undefined` when no\n * provider is currently wired \u2014 which is the signal `TechStackEngine` uses to\n * skip the research stage entirely and stay a deterministic tool.\n */\nexport function createProviderLlm(\n accessor: LlmAccessor,\n options: { readonly timeoutMs?: number | undefined } = {},\n): ResearchLlm | undefined {\n if (!accessor()) return undefined;\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n\n return async (req: ResearchLlmRequest): Promise<string> => {\n // Re-resolve per call so a mid-session model switch takes effect.\n const llm = accessor();\n if (!llm) throw new Error('TechStack research: no provider available');\n\n const request: Request = {\n model: llm.model,\n system: [{ type: 'text', text: req.system }],\n messages: [{ role: 'user', content: req.prompt }],\n maxTokens: req.maxTokens,\n };\n\n if (llm.provider.capabilities.structuredOutput) {\n request.responseFormat = {\n type: 'json_schema',\n jsonSchema: { name: req.schemaName, strict: false, schema: req.schema },\n };\n } else if (llm.provider.capabilities.jsonMode) {\n request.responseFormat = { type: 'json_object' };\n }\n\n const timer = new AbortController();\n const onAbort = () => {\n timer.abort(new Error('TechStack research: cancelled'));\n };\n req.signal?.addEventListener('abort', onAbort, { once: true });\n const to = setTimeout(() => {\n timer.abort(new Error('TechStack research: LLM timeout'));\n }, timeoutMs);\n to.unref?.();\n\n try {\n const res = await llm.provider.complete(request, { signal: timer.signal });\n return res.content\n .filter((block) => block.type === 'text')\n .map((block) => block.text)\n .join('\\n')\n .trim();\n } finally {\n req.signal?.removeEventListener('abort', onAbort);\n clearTimeout(to);\n timer.abort();\n }\n };\n}\n\n// \u2500\u2500 Response parsing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/** Strip one outer Markdown fence without touching inner fences. */\nfunction stripOuterFence(text: string): string {\n const trimmed = text.trim();\n const match = trimmed.match(/^```(?:[a-z0-9_-]+)?\\s*\\r?\\n([\\s\\S]*?)\\r?\\n```$/i);\n return (match?.[1] ?? trimmed).trim();\n}\n\n/**\n * Pull the outermost JSON object out of a response.\n *\n * Even with `json_object` set, models prepend prose often enough that a bare\n * `JSON.parse` is a coin flip. Same salvage the completion handler does\n * (`extractJson`).\n */\nfunction extractJsonObject(text: string): string {\n const trimmed = stripOuterFence(text);\n if (trimmed.startsWith('{')) return trimmed;\n const start = trimmed.indexOf('{');\n const end = trimmed.lastIndexOf('}');\n if (start !== -1 && end > start) return trimmed.slice(start, end + 1);\n return trimmed;\n}\n\n/**\n * Parse a research response into a plain object, or `null` when the model\n * returned something unusable.\n *\n * Never throws: an unparseable research response degrades that cluster to zero\n * findings, it does not fail the analyze job.\n */\nexport function parseResearchJson(text: string): Record<string, unknown> | null {\n if (!text.trim()) return null;\n try {\n const parsed: unknown = JSON.parse(extractJsonObject(text));\n return parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)\n ? (parsed as Record<string, unknown>)\n : null;\n } catch {\n return null;\n }\n}\n", "/**\n * TechStack \u2014 the research stage.\n *\n * Takes the deterministic snapshot's problem cases (see `triage.ts`), gathers\n * public context for them via web search, and asks the LLM the one question\n * the registry cannot answer: *what should I actually do about this?*\n *\n * Structure follows SDD \u00A7557 \u2014 one pass per finding-type cluster\n * (breaking-change / replacement / CVE-applicability), not one per package and\n * not one per ecosystem. That keeps the call count at \u22643 per analyze while\n * still letting each prompt be a focused specialist.\n *\n * @see docs/specs/techstack-sdd.md \u00A731, \u00A7472, \u00A7557\n */\n\nimport type { DependencyObservation, Evidence, Finding } from '../types.js';\nimport { parseResearchJson } from './llm.js';\nimport { clusterCandidates } from './triage.js';\nimport type {\n ResearchCluster,\n ResearchLlm,\n ResearchOptions,\n ResearchSearch,\n ResearchSearchResult,\n TechStackResearcher,\n TriageCandidate,\n} from './types.js';\n\n// \u2500\u2500 Tunables \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst SEARCH_CONCURRENCY = 4;\nconst MAX_SNIPPET_CHARS = 320;\nconst MAX_TOKENS_PER_CLUSTER = 2_000;\n\n/**\n * Confidence ceiling for anything the LLM produced. `1.0` is reserved for\n * deterministic findings (registry/OSV facts) \u2014 an interpretation must never\n * be able to present itself as a fact, however sure the model claims to be.\n */\nconst MAX_LLM_CONFIDENCE = 0.95;\nconst MIN_LLM_CONFIDENCE = 0.1;\nconst DEFAULT_LLM_CONFIDENCE = 0.5;\n\nconst FINDING_TYPE_BY_CLUSTER: Record<ResearchCluster, Finding['type']> = {\n breaking_change: 'upgrade',\n replacement: 'replacement',\n vulnerability: 'vulnerability',\n};\n\nconst VALID_SEVERITIES: ReadonlySet<string> = new Set([\n 'info',\n 'low',\n 'medium',\n 'high',\n 'critical',\n]);\n\nconst VALID_ACTIONS: ReadonlySet<string> = new Set([\n 'none',\n 'upgrade_patch',\n 'upgrade_minor',\n 'upgrade_major',\n 'replace',\n 'remove',\n 'investigate',\n]);\n\n// \u2500\u2500 Prompts \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst SHARED_RULES = [\n 'Return only JSON. No markdown, prose, or code fences.',\n 'Only reason about the packages listed in the input. Never introduce a package that is not listed.',\n 'Never state or guess version numbers other than the ones given to you; the version facts are already established.',\n 'If the provided sources do not support a conclusion, say so in the rationale and lower the confidence.',\n 'Be concrete and short. The reader is an engineer deciding what to do this afternoon.',\n].join('\\n');\n\nconst SYSTEM_BY_CLUSTER: Record<ResearchCluster, string> = {\n breaking_change: [\n 'You assess upgrade risk for software dependencies.',\n 'For each package, judge how disruptive moving to the latest version would be for a typical consumer,',\n 'and what the migration actually involves.',\n SHARED_RULES,\n ].join('\\n'),\n replacement: [\n 'You advise on deprecated, yanked, and unmaintained software dependencies.',\n 'For each package, say whether it should be replaced, what the community has moved to, and how urgent it is.',\n 'Prefer platform-native or well-maintained successors. If the package is fine to keep, say so plainly.',\n SHARED_RULES,\n ].join('\\n'),\n vulnerability: [\n 'You triage security advisories for software dependencies.',\n 'For each package, judge how exploitable the known advisory is in practice and what the fix is.',\n 'Distinguish advisories that require unusual usage from ones that affect every consumer.',\n SHARED_RULES,\n ].join('\\n'),\n};\n\nconst QUESTION_BY_CLUSTER: Record<ResearchCluster, string> = {\n breaking_change: 'How breaking is this upgrade, and what does the migration involve?',\n replacement: 'Should this be replaced, and with what?',\n vulnerability: 'Does this advisory realistically affect a consumer, and what is the fix?',\n};\n\nconst RESEARCH_JSON_SCHEMA: Record<string, unknown> = {\n type: 'object',\n additionalProperties: false,\n properties: {\n findings: {\n type: 'array',\n items: {\n type: 'object',\n additionalProperties: false,\n properties: {\n package: { type: 'string', description: 'Exact package name from the input list.' },\n severity: { type: 'string', enum: ['info', 'low', 'medium', 'high', 'critical'] },\n action: {\n type: 'string',\n enum: [\n 'none',\n 'upgrade_patch',\n 'upgrade_minor',\n 'upgrade_major',\n 'replace',\n 'remove',\n 'investigate',\n ],\n },\n confidence: { type: 'number', minimum: 0, maximum: 1 },\n rationale: { type: 'string' },\n breakingRisk: { type: 'string' },\n sources: { type: 'array', items: { type: 'string' } },\n },\n required: ['package', 'severity', 'action', 'confidence', 'rationale'],\n },\n },\n },\n required: ['findings'],\n};\n\n// \u2500\u2500 Search queries \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction searchQuery(candidate: TriageCandidate): string {\n const dep = candidate.dependency;\n const from = dep.locked ?? dep.requested ?? '';\n const to = dep.latestStable ?? '';\n switch (candidate.cluster) {\n case 'breaking_change':\n return `${dep.name} ${from} to ${to} migration guide breaking changes`;\n case 'replacement':\n return `${dep.name} ${dep.ecosystem} deprecated recommended alternative replacement`;\n case 'vulnerability':\n return `${dep.name} ${from} security advisory CVE affected versions`;\n }\n}\n\n/** Run `task` over `items` with bounded concurrency, preserving order. */\nasync function mapLimit<T, R>(\n items: readonly T[],\n limit: number,\n task: (item: T, index: number) => Promise<R>,\n): Promise<R[]> {\n const out = new Array<R>(items.length);\n let cursor = 0;\n const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {\n for (;;) {\n const index = cursor++;\n if (index >= items.length) return;\n out[index] = await task(items[index]!, index);\n }\n });\n await Promise.all(workers);\n return out;\n}\n\n// \u2500\u2500 Prompt assembly \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction describeDependency(dep: DependencyObservation): string {\n const bits = [\n `ecosystem: ${dep.ecosystem}`,\n `installed: ${dep.locked ?? dep.installed ?? dep.requested ?? 'unknown'}`,\n ];\n if (dep.latestStable) bits.push(`latest stable: ${dep.latestStable}`);\n if (dep.requested) bits.push(`constraint: ${dep.requested}`);\n bits.push(dep.direct ? 'direct dependency' : 'transitive dependency');\n bits.push(`scope: ${dep.scope}`);\n if (dep.license) bits.push(`license: ${dep.license}`);\n if (dep.deprecated) bits.push('registry flag: deprecated');\n if (dep.yanked) bits.push('registry flag: yanked');\n bits.push(`status: ${dep.status}`);\n return bits.join(', ');\n}\n\nfunction renderSources(results: readonly ResearchSearchResult[]): string {\n if (results.length === 0) return ' (no sources found \u2014 say so and lower confidence)';\n return results\n .map((r) => ` - ${r.title}\\n ${r.url}\\n ${truncate(r.snippet, MAX_SNIPPET_CHARS)}`)\n .join('\\n');\n}\n\nfunction truncate(value: string, max: number): string {\n const clean = value.replace(/\\s+/g, ' ').trim();\n return clean.length <= max ? clean : `${clean.slice(0, max)}\u2026`;\n}\n\nfunction buildPrompt(\n cluster: ResearchCluster,\n entries: readonly { candidate: TriageCandidate; sources: readonly ResearchSearchResult[] }[],\n): string {\n const blocks = entries.map(({ candidate, sources }, i) =>\n [\n `${i + 1}. ${candidate.dependency.name}`,\n ` ${describeDependency(candidate.dependency)}`,\n ' Web search results:',\n renderSources(sources),\n ].join('\\n'),\n );\n\n return [\n `Question for every package below: ${QUESTION_BY_CLUSTER[cluster]}`,\n '',\n 'Packages:',\n ...blocks,\n '',\n 'Return JSON shaped exactly as:',\n '{\"findings\":[{\"package\":\"exact-name-from-the-list\",\"severity\":\"medium\",\"action\":\"upgrade_major\",' +\n '\"confidence\":0.7,\"rationale\":\"one or two sentences\",\"breakingRisk\":\"optional short note\",' +\n '\"sources\":[\"https://\u2026\"]}]}',\n '',\n 'Emit one entry per package you can say something useful about. Omit packages you cannot.',\n ].join('\\n');\n}\n\n// \u2500\u2500 Response \u2192 Finding \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction optionalString(value: unknown): string | undefined {\n return typeof value === 'string' && value.trim() ? value.trim() : undefined;\n}\n\nfunction clampConfidence(value: unknown): number {\n if (typeof value !== 'number' || !Number.isFinite(value)) return DEFAULT_LLM_CONFIDENCE;\n return Math.min(MAX_LLM_CONFIDENCE, Math.max(MIN_LLM_CONFIDENCE, value));\n}\n\nfunction sourceEvidence(\n raw: unknown,\n sources: readonly ResearchSearchResult[],\n retrievedAt: string,\n): Evidence[] {\n const known = new Set(sources.map((s) => s.url));\n const cited = Array.isArray(raw)\n ? raw.filter((u): u is string => typeof u === 'string' && known.has(u))\n : [];\n // Fall back to everything we actually fetched when the model cited nothing \u2014\n // the user still deserves to see what the interpretation was based on.\n const urls = cited.length > 0 ? cited : sources.map((s) => s.url);\n return urls.map((url) => ({\n kind: 'agent' as const,\n source: url,\n retrievedAt,\n detail: sources.find((s) => s.url === url)?.title,\n }));\n}\n\nfunction toFinding(\n raw: Record<string, unknown>,\n cluster: ResearchCluster,\n byName: ReadonlyMap<string, { candidate: TriageCandidate; sources: readonly ResearchSearchResult[] }>,\n retrievedAt: string,\n): Finding | null {\n const name = optionalString(raw.package);\n if (!name) return null;\n\n // Anti-hallucination gate: a finding for a package we did not ask about is\n // discarded outright. The model does not get to expand the inventory.\n const entry = byName.get(name);\n if (!entry) return null;\n\n const rationale = optionalString(raw.rationale);\n if (!rationale) return null;\n\n const severity = VALID_SEVERITIES.has(raw.severity as string)\n ? (raw.severity as Finding['severity'])\n : 'info';\n const action = VALID_ACTIONS.has(raw.action as string)\n ? (raw.action as Finding['action'])\n : 'investigate';\n\n const breakingRisk = optionalString(raw.breakingRisk);\n\n return {\n id: `research-${entry.candidate.dependency.id}-${cluster}`,\n dependencyId: entry.candidate.dependency.id,\n type: FINDING_TYPE_BY_CLUSTER[cluster],\n severity,\n action,\n confidence: clampConfidence(raw.confidence),\n rationale,\n ...(breakingRisk ? { breakingRisk } : {}),\n evidence: sourceEvidence(raw.sources, entry.sources, retrievedAt),\n };\n}\n\nfunction parseFindings(\n parsed: Record<string, unknown> | null,\n cluster: ResearchCluster,\n byName: ReadonlyMap<string, { candidate: TriageCandidate; sources: readonly ResearchSearchResult[] }>,\n retrievedAt: string,\n): Finding[] {\n if (!parsed || !Array.isArray(parsed.findings)) return [];\n const seen = new Set<string>();\n const out: Finding[] = [];\n for (const raw of parsed.findings) {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) continue;\n const finding = toFinding(raw as Record<string, unknown>, cluster, byName, retrievedAt);\n if (!finding || seen.has(finding.id)) continue;\n seen.add(finding.id);\n out.push(finding);\n }\n return out;\n}\n\n// \u2500\u2500 Researcher \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface CreateResearcherOptions {\n readonly llm: ResearchLlm;\n readonly search: ResearchSearch;\n /** Injectable clock \u2014 keeps evidence timestamps deterministic in tests. */\n readonly now?: (() => Date) | undefined;\n}\n\n/**\n * Build a {@link TechStackResearcher} over the injected LLM and search ports.\n *\n * Failure policy throughout: a cluster that throws contributes zero findings\n * and does not disturb its siblings. Research is enrichment \u2014 a provider\n * outage or a dry search must degrade the report, never fail the analyze job\n * that already produced a complete deterministic inventory.\n */\nexport function createResearcher(options: CreateResearcherOptions): TechStackResearcher {\n const now = options.now ?? (() => new Date());\n\n return {\n async research(candidates, opts: ResearchOptions = {}): Promise<readonly Finding[]> {\n if (candidates.length === 0) return [];\n\n const clusters = [...clusterCandidates(candidates).entries()];\n const findings: Finding[] = [];\n let completed = 0;\n opts.onProgress?.(0, clusters.length);\n\n for (const [cluster, members] of clusters) {\n if (opts.signal?.aborted) break;\n\n try {\n const sources = await mapLimit(members, SEARCH_CONCURRENCY, (candidate) =>\n options.search(searchQuery(candidate), { signal: opts.signal }),\n );\n if (opts.signal?.aborted) break;\n\n const entries = members.map((candidate, i) => ({\n candidate,\n sources: sources[i] ?? [],\n }));\n const byName = new Map(entries.map((entry) => [entry.candidate.dependency.name, entry]));\n\n const text = await options.llm({\n system: SYSTEM_BY_CLUSTER[cluster],\n prompt: buildPrompt(cluster, entries),\n schema: RESEARCH_JSON_SCHEMA,\n schemaName: `techstack_${cluster}_findings`,\n maxTokens: MAX_TOKENS_PER_CLUSTER,\n signal: opts.signal,\n });\n\n findings.push(\n ...parseFindings(parseResearchJson(text), cluster, byName, now().toISOString()),\n );\n } catch {\n // This cluster contributed nothing. The others still run, and the\n // deterministic findings from `enrich()` are untouched.\n }\n\n completed++;\n opts.onProgress?.(completed, clusters.length);\n }\n\n return findings;\n },\n };\n}\n", "/**\n * TechStack \u2014 `searchTool` \u2192 {@link ResearchSearch} adapter.\n *\n * Wraps the built-in web search (`packages/tools/src/search.ts`) so the\n * researcher depends on a one-function port rather than the tool contract.\n *\n * Note on reliability: that tool scrapes DuckDuckGo/Google/Bing through\n * `guardedFetch` \u2014 there is no API key and no SLA. It can and does return\n * nothing. Every failure here resolves to `[]` so a dry search degrades a\n * finding to registry-only evidence instead of failing the analyze job.\n */\n\nimport type { Context } from '@wrongstack/core';\nimport { searchTool } from '@wrongstack/tools';\nimport type { ResearchSearch, ResearchSearchResult } from './types.js';\n\nconst DEFAULT_NUM_RESULTS = 5;\n\n/**\n * `searchTool` declares a `Context` parameter to satisfy the shared `Tool`\n * contract but never reads it \u2014 its implementation signature is\n * `executeStream(input, _ctx, opts)`. Research runs server-side with no agent\n * session to hand over, so we pass a placeholder rather than fabricate a\n * half-populated Context that would be more misleading than an obvious stub.\n */\nconst NO_CONTEXT = undefined as unknown as Context;\n\nexport interface SearchToolOptions {\n readonly numResults?: number | undefined;\n readonly source?: 'duckduckgo' | 'google' | 'bing' | undefined;\n}\n\n/** Build a {@link ResearchSearch} backed by the built-in `search` tool. */\nexport function createToolSearch(options: SearchToolOptions = {}): ResearchSearch {\n const numResults = options.numResults ?? DEFAULT_NUM_RESULTS;\n\n return async (query, opts): Promise<readonly ResearchSearchResult[]> => {\n if (opts.signal?.aborted) return [];\n try {\n const out = await searchTool.execute(\n {\n query,\n num_results: numResults,\n ...(options.source ? { source: options.source } : {}),\n },\n NO_CONTEXT,\n { signal: opts.signal ?? new AbortController().signal },\n );\n return out.results.map((result) => ({\n title: result.title,\n url: result.url,\n snippet: result.snippet,\n }));\n } catch {\n // Scraped search is best-effort. Interpretation without sources is still\n // better than no interpretation, and the finding's evidence list makes\n // the absence visible to the user.\n return [];\n }\n };\n}\n", "/**\n * TechStack \u2014 SQLite-backed store.\n *\n * Provides persistence for snapshots, jobs, and the delivery outbox\n * using Node 22.5+'s built-in `node:sqlite` module.\n *\n * Store path: ~/.wrongstack/projects/<slug>/techstack/techstack.db\n *\n * @see docs/specs/techstack-sdd.md \u00A73.2, \u00A74.1\n */\n\nimport { DatabaseSync } from 'node:sqlite';\nimport { mkdirSync, existsSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { homedir } from 'node:os';\nimport { applySchema } from './schema.js';\nimport type {\n DeliveryOutbox,\n DeliveryStatus,\n Snapshot,\n TechStackJob,\n TechStackJobStatus,\n TechStackJobProgress,\n} from '../types.js';\n\n// \u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface StoreOptions {\n /** Project slug used for the store directory path. */\n readonly projectSlug: string;\n /** Optional explicit dbPath override (for testing). */\n readonly dbPath?: string;\n}\n\n// \u2500\u2500 Store class \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport class TechStackStore {\n private db: DatabaseSync;\n private dbPath: string;\n\n constructor(options: StoreOptions) {\n this.dbPath =\n options.dbPath ??\n join(\n homedir(),\n '.wrongstack',\n 'projects',\n options.projectSlug,\n 'techstack',\n 'techstack.db',\n );\n\n // Ensure parent directory exists\n const dir = this.dbPath.slice(0, this.dbPath.lastIndexOf('\\\\'));\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n\n this.db = new DatabaseSync(this.dbPath);\n this.db.exec('PRAGMA journal_mode = WAL;');\n this.db.exec('PRAGMA foreign_keys = ON;');\n applySchema(this.db);\n }\n\n // \u2500\u2500 Lifecycle \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n /** Close the database connection. Idempotent. */\n close(): void {\n try {\n this.db.close();\n } catch {\n // Already closed \u2014 idempotent\n }\n }\n\n /** Get the database path (useful for tests). */\n get path(): string {\n return this.dbPath;\n }\n\n // \u2500\u2500 Snapshots \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n /** Persist a snapshot. */\n saveSnapshot(snapshot: Snapshot): void {\n const stmt = this.db.prepare(`\n INSERT OR REPLACE INTO snapshots (id, project_id, target_root, fingerprint, created_at, raw_json, adapter_version)\n VALUES (?, ?, ?, ?, ?, ?, ?)\n `);\n stmt.run(\n snapshot.id,\n snapshot.projectId,\n snapshot.targetRoot,\n snapshot.fingerprint,\n snapshot.createdAt,\n JSON.stringify(snapshot),\n snapshot.adapterVersion,\n );\n }\n\n /** Get a snapshot by project ID (latest). */\n getSnapshot(projectId: string): Snapshot | undefined {\n const stmt = this.db.prepare(`\n SELECT raw_json FROM snapshots\n WHERE project_id = ?\n ORDER BY created_at DESC\n LIMIT 1\n `);\n const row = stmt.get(projectId) as { raw_json: string } | undefined;\n if (!row) return undefined;\n try {\n return JSON.parse(row.raw_json) as Snapshot;\n } catch {\n return undefined;\n }\n }\n\n /** Get a snapshot by ID. */\n getSnapshotById(id: string): Snapshot | undefined {\n const stmt = this.db.prepare(`\n SELECT raw_json FROM snapshots WHERE id = ?\n `);\n const row = stmt.get(id) as { raw_json: string } | undefined;\n if (!row) return undefined;\n try {\n return JSON.parse(row.raw_json) as Snapshot;\n } catch {\n return undefined;\n }\n }\n\n /** List all snapshots for a project (newest first). */\n listSnapshots(projectId: string, limit = 20): Snapshot[] {\n const stmt = this.db.prepare(`\n SELECT raw_json FROM snapshots\n WHERE project_id = ?\n ORDER BY created_at DESC\n LIMIT ?\n `);\n const rows = stmt.all(projectId, limit) as Array<{ raw_json: string }>;\n return rows\n .map((r) => {\n try {\n return JSON.parse(r.raw_json) as Snapshot;\n } catch {\n return undefined;\n }\n })\n .filter((s): s is Snapshot => s !== undefined);\n }\n\n /** Delete snapshots older than a given timestamp. */\n deleteSnapshotsBefore(projectId: string, before: string): number {\n const stmt = this.db.prepare(`\n DELETE FROM snapshots WHERE project_id = ? AND created_at < ?\n `);\n const result = stmt.run(projectId, before);\n return Number(result.changes);\n }\n\n // \u2500\u2500 Jobs \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n /** Persist a job. */\n saveJob(job: TechStackJob): void {\n const stmt = this.db.prepare(`\n INSERT OR REPLACE INTO jobs\n (id, project_id, target_root, kind, status, fingerprint, requested_by, session_id, created_at, completed_at, error, progress_json)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n `);\n stmt.run(\n job.id,\n job.projectId,\n job.targetRoot,\n job.kind,\n job.status,\n job.fingerprint,\n job.requestedBy,\n job.sessionId ?? null,\n job.createdAt,\n job.completedAt ?? null,\n job.error ?? null,\n job.progress ? JSON.stringify(job.progress) : null,\n );\n }\n\n /** Get a job by ID. */\n getJob(id: string): TechStackJob | undefined {\n const stmt = this.db.prepare(`\n SELECT * FROM jobs WHERE id = ?\n `);\n const row = stmt.get(id) as Record<string, unknown> | undefined;\n if (!row) return undefined;\n return this.rowToJob(row);\n }\n\n /** Update job status and optional progress. */\n updateJobStatus(id: string, status: TechStackJobStatus, progress?: TechStackJobProgress): void {\n const progressJson = progress ? JSON.stringify(progress) : null;\n const completedAt = status === 'completed' || status === 'failed' || status === 'cancelled'\n ? new Date().toISOString()\n : null;\n\n const stmt = this.db.prepare(`\n UPDATE jobs\n SET status = ?, progress_json = ?, completed_at = COALESCE(?, completed_at)\n WHERE id = ?\n `);\n stmt.run(status, progressJson, completedAt, id);\n }\n\n /** List jobs for a project (newest first). */\n listJobs(projectId: string, limit = 50): TechStackJob[] {\n const stmt = this.db.prepare(`\n SELECT * FROM jobs WHERE project_id = ? ORDER BY created_at DESC LIMIT ?\n `);\n const rows = stmt.all(projectId, limit) as Array<Record<string, unknown>>;\n return rows.map((r) => this.rowToJob(r));\n }\n\n // \u2500\u2500 Outbox \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n /** Create an outbox entry. */\n createOutbox(deliveryId: string, reportId: string, sessionId: string): void {\n const stmt = this.db.prepare(`\n INSERT OR IGNORE INTO outbox (delivery_id, report_id, session_id, status, attempts)\n VALUES (?, ?, ?, 'pending', 0)\n `);\n stmt.run(deliveryId, reportId, sessionId);\n }\n\n /** Claim an outbox entry (atomic CAS). */\n claimOutbox(deliveryId: string, sessionId: string): boolean {\n const stmt = this.db.prepare(`\n UPDATE outbox\n SET status = 'claimed', claimed_at = datetime('now'), attempts = attempts + 1\n WHERE delivery_id = ? AND session_id = ? AND status = 'pending'\n `);\n const result = stmt.run(deliveryId, sessionId);\n return result.changes > 0;\n }\n\n /** Mark an outbox entry as delivered. */\n deliverOutbox(deliveryId: string): void {\n const stmt = this.db.prepare(`\n UPDATE outbox SET status = 'delivered', delivered_at = datetime('now')\n WHERE delivery_id = ?\n `);\n stmt.run(deliveryId);\n }\n\n /** Mark an outbox entry as failed. */\n failOutbox(deliveryId: string): void {\n const stmt = this.db.prepare(`\n UPDATE outbox SET status = 'failed' WHERE delivery_id = ?\n `);\n stmt.run(deliveryId);\n }\n\n /** List outbox entries by status. */\n listOutboxByStatus(status: DeliveryStatus): DeliveryOutbox[] {\n const stmt = this.db.prepare(`\n SELECT * FROM outbox WHERE status = ?\n `);\n const rows = stmt.all(status) as Array<Record<string, unknown>>;\n return rows.map((r) => this.rowToOutbox(r));\n }\n\n // \u2500\u2500 Row mapping helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n private rowToJob(row: Record<string, unknown>): TechStackJob {\n let progress: TechStackJobProgress | undefined;\n if (row.progress_json && typeof row.progress_json === 'string') {\n try {\n progress = JSON.parse(row.progress_json) as TechStackJobProgress;\n } catch {\n // ignore\n }\n }\n\n return {\n id: String(row.id),\n projectId: String(row.project_id),\n targetRoot: String(row.target_root),\n kind: row.kind as TechStackJob['kind'],\n status: row.status as TechStackJobStatus,\n fingerprint: String(row.fingerprint ?? ''),\n requestedBy: String(row.requested_by ?? ''),\n sessionId: row.session_id ? String(row.session_id) : undefined,\n createdAt: String(row.created_at),\n completedAt: row.completed_at ? String(row.completed_at) : undefined,\n error: row.error ? String(row.error) : undefined,\n ...(progress ? { progress } : {}),\n };\n }\n\n private rowToOutbox(row: Record<string, unknown>): DeliveryOutbox {\n return {\n deliveryId: String(row.delivery_id),\n reportId: String(row.report_id),\n sessionId: String(row.session_id),\n status: row.status as DeliveryStatus,\n attempts: Number(row.attempts),\n claimedAt: row.claimed_at ? String(row.claimed_at) : undefined,\n deliveredAt: row.delivered_at ? String(row.delivered_at) : undefined,\n };\n }\n}\n", "/**\n * TechStack \u2014 SQLite schema (DDL) and migration helpers.\n *\n * Defines the tables for snapshots, jobs, and the delivery outbox.\n * Uses `node:sqlite` (built-in since Node 22.5+).\n *\n * Tables:\n * - snapshots: persisted inventory snapshots\n * - jobs: async inventory/analyze job state\n * - outbox: idle-delivery tracking\n *\n * @see docs/specs/techstack-sdd.md \u00A73.2, \u00A74.1\n */\n\nexport const SCHEMA_VERSION = 1;\n\nexport const DDL = `\nCREATE TABLE IF NOT EXISTS techstack_schema_version (\n version INTEGER NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS snapshots (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL,\n target_root TEXT NOT NULL,\n fingerprint TEXT NOT NULL,\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n raw_json TEXT NOT NULL,\n adapter_version TEXT NOT NULL DEFAULT ''\n);\n\nCREATE INDEX IF NOT EXISTS idx_snapshots_project_id ON snapshots(project_id);\nCREATE INDEX IF NOT EXISTS idx_snapshots_created_at ON snapshots(created_at DESC);\n\nCREATE TABLE IF NOT EXISTS jobs (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL,\n target_root TEXT NOT NULL,\n kind TEXT NOT NULL CHECK(kind IN ('inventory', 'analyze')),\n status TEXT NOT NULL DEFAULT 'queued'\n CHECK(status IN ('queued','discovering','inventorying','enriching','researching','synthesizing','completed','failed','cancelled')),\n fingerprint TEXT NOT NULL DEFAULT '',\n requested_by TEXT NOT NULL DEFAULT '',\n session_id TEXT,\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n completed_at TEXT,\n error TEXT,\n progress_json TEXT\n);\n\nCREATE INDEX IF NOT EXISTS idx_jobs_project_id ON jobs(project_id);\nCREATE INDEX IF NOT EXISTS idx_jobs_status ON jobs(status);\n\nCREATE TABLE IF NOT EXISTS outbox (\n delivery_id TEXT PRIMARY KEY,\n report_id TEXT NOT NULL,\n session_id TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT 'pending'\n CHECK(status IN ('pending', 'claimed', 'delivered', 'failed')),\n attempts INTEGER NOT NULL DEFAULT 0,\n claimed_at TEXT,\n delivered_at TEXT\n);\n\nCREATE INDEX IF NOT EXISTS idx_outbox_status ON outbox(status);\n`;\n\n/**\n * Run the schema DDL and check/migrate version.\n */\nexport function applySchema(db: import('node:sqlite').DatabaseSync): void {\n // Execute DDL (IF NOT EXISTS makes it idempotent)\n for (const statement of DDL.split(';')) {\n const trimmed = statement.trim();\n if (trimmed) {\n db.exec(trimmed);\n }\n }\n\n // Check version\n const row = db.prepare('SELECT version FROM techstack_schema_version').get() as\n | { version: number }\n | undefined;\n\n if (!row) {\n db.prepare('INSERT INTO techstack_schema_version (version) VALUES (?)').run(SCHEMA_VERSION);\n } else if (row.version < SCHEMA_VERSION) {\n // Future: run migrations here\n db.prepare('UPDATE techstack_schema_version SET version = ?').run(SCHEMA_VERSION);\n }\n}\n", "/**\n * TechStack \u2014 DeliveryCoordinator.\n *\n * Per-session idle-delivery coordinator for TechStack reports.\n *\n * Guarantees (per SDD \u00A75):\n * 1. Idle-only: report is never injected while a run is in progress.\n * 2. Exactly-once visible delivery: duplicate outbox entries \u2192 single delivery.\n * 3. Durable: survives store close/reopen; pending entries retry on recovery.\n * 4. Bounded: at most one delivery per report per session.\n *\n * @see docs/specs/techstack-sdd.md \u00A75\n */\n\nimport type { TechStackStore } from '../store/sqlite.js';\nimport type { Snapshot } from '../types.js';\n\nexport interface DeliveryCoordinatorOptions {\n readonly store: TechStackStore;\n /** Check whether an agent run is currently in progress. */\n readonly isRunInProgress: () => boolean;\n /** Deliver the report summary to the chat session (append to journal). */\n readonly deliverToSession: (sessionId: string, reportId: string, summary: string) => Promise<boolean>;\n /** Called when a delivery completes (for WS event broadcasting). */\n readonly onDelivered?: ((deliveryId: string, sessionId: string) => void) | undefined;\n /** Poll interval in ms. Default: 2000. */\n readonly pollIntervalMs?: number | undefined;\n}\n\nexport interface DeliveryResult {\n deliveryId: string;\n sessionId: string;\n delivered: boolean;\n}\n\n/**\n * Attempt to deliver a single pending outbox entry.\n *\n * Returns `{ delivered: false }` if the run is still in progress or the\n * entry has already been delivered. Returns `{ delivered: true }` on success.\n *\n * This function is idempotent: calling it twice for the same deliveryId\n * will deliver exactly once (the CAS in claimOutbox prevents double-claim).\n */\nexport async function attemptDelivery(\n deliveryId: string,\n opts: DeliveryCoordinatorOptions,\n): Promise<DeliveryResult> {\n const { store, isRunInProgress, deliverToSession, onDelivered } = opts;\n\n // Idle-only gate\n if (isRunInProgress()) {\n return { deliveryId, sessionId: '', delivered: false };\n }\n\n // Find the outbox entry\n const pending = store.listOutboxByStatus('pending');\n const entry = pending.find((e: { deliveryId: string }) => e.deliveryId === deliveryId);\n if (!entry) {\n return { deliveryId, sessionId: '', delivered: false };\n }\n\n // Atomic CAS claim \u2014 prevents double-delivery\n const claimed = store.claimOutbox(deliveryId, entry.sessionId);\n if (!claimed) {\n return { deliveryId, sessionId: entry.sessionId, delivered: false };\n }\n\n // Generate a summary from the snapshot\n const snapshot = store.getSnapshotById(entry.reportId);\n const summary = snapshot ? buildSummary(snapshot) : `TechStack report ${entry.reportId} is ready.`;\n\n // Deliver to session\n const success = await deliverToSession(entry.sessionId, entry.reportId, summary);\n\n if (success) {\n store.deliverOutbox(deliveryId);\n onDelivered?.(deliveryId, entry.sessionId);\n return { deliveryId, sessionId: entry.sessionId, delivered: true };\n }\n\n store.failOutbox(deliveryId);\n return { deliveryId, sessionId: entry.sessionId, delivered: false };\n}\n\n/**\n * Process all pending outbox entries for a given session.\n * Called by the coordinator loop when the session becomes idle.\n */\nexport async function drainPendingDeliveries(\n sessionId: string,\n opts: DeliveryCoordinatorOptions,\n): Promise<number> {\n const { store, isRunInProgress } = opts;\n if (isRunInProgress()) return 0;\n\n const pending = store.listOutboxByStatus('pending');\n let delivered = 0;\n for (const entry of pending) {\n if (entry.sessionId !== sessionId) continue;\n const result = await attemptDelivery(entry.deliveryId, opts);\n if (result.delivered) delivered++;\n }\n return delivered;\n}\n\n/**\n * Build a concise chat-friendly summary from a snapshot.\n *\n * Format: counts + top findings + report link.\n */\nfunction buildSummary(snapshot: Snapshot): string {\n const lines: string[] = [\n `\uD83D\uDCCA **TechStack Report Ready**`,\n '',\n `**${snapshot.workspaces.length}** workspaces \u00B7 **${snapshot.dependencies.length}** dependencies \u00B7 **${snapshot.findings.length}** findings`,\n ];\n\n const findings = snapshot.findings as ReadonlyArray<{\n severity: string;\n type: string;\n rationale: string;\n dependencyId: string;\n }>;\n const critical = findings.filter((f) => f.severity === 'critical' || f.severity === 'high');\n if (critical.length > 0) {\n lines.push('', `**Top ${Math.min(5, critical.length)} urgent findings:**`);\n for (const f of critical.slice(0, 5)) {\n const dep = snapshot.dependencies.find((d: { id: string }) => d.id === f.dependencyId);\n lines.push(` \u2022 **${dep?.name ?? f.dependencyId}** \u2014 ${f.type}: ${f.rationale}`);\n }\n }\n\n lines.push('', `_Open the TechStack view for the full report._`);\n return lines.join('\\n');\n}\n"],
|
|
5
5
|
"mappings": ";AA8BA,IAAM,yBAAgE;AAAA,EACpE,KAAK;AAAA,EACL,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,IAAI;AAAA,EACJ,QAAQ;AAAA,EACR,KAAK;AAAA,EACL,MAAM;AAAA,EACN,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,KAAK;AACP;AAKA,IAAM,yBAAgE;AAAA,EACpE,KAAK;AAAA,EACL,MAAM;AAAA,EACN,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,UAAU;AAAA,EACV,KAAK;AAAA,EACL,OAAO;AAAA,EACP,KAAK;AAAA,EACL,OAAO;AAAA,EACP,KAAK;AAAA,EACL,OAAO;AACT;AAkBO,SAAS,UAAU,OAA0B;AAClD,QAAM,WAAqB,CAAC,QAAQ,MAAM,MAAM,GAAG;AAEnD,MAAI,MAAM,WAAW;AACnB,aAAS,KAAK,kBAAkB,MAAM,SAAS,GAAG,GAAG;AAAA,EACvD;AAEA,WAAS,KAAK,kBAAkB,MAAM,IAAI,CAAC;AAE3C,MAAI,MAAM,SAAS;AACjB,aAAS,KAAK,KAAK,kBAAkB,MAAM,OAAO,CAAC;AAAA,EACrD;AAEA,MAAI,MAAM,cAAc,MAAM,WAAW,OAAO,GAAG;AACjD,UAAM,KAAK,CAAC,GAAG,MAAM,WAAW,QAAQ,CAAC,EACtC,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,GAAG,mBAAmB,CAAC,CAAC,IAAI,mBAAmB,CAAC,CAAC,EAAE,EACnE,KAAK,GAAG;AACX,aAAS,KAAK,KAAK,EAAE;AAAA,EACvB;AAEA,MAAI,MAAM,SAAS;AACjB,aAAS,KAAK,KAAK,kBAAkB,MAAM,OAAO,CAAC;AAAA,EACrD;AAEA,SAAO,SAAS,KAAK,EAAE;AACzB;AAaO,SAAS,UAAU,MAAqC;AAC7D,MAAI,CAAC,KAAK,WAAW,MAAM,EAAG,QAAO;AAErC,QAAM,gBAAgB,KAAK,MAAM,CAAC;AAGlC,MAAI,OAAO;AACX,MAAI;AACJ,QAAM,UAAU,KAAK,QAAQ,GAAG;AAChC,MAAI,WAAW,GAAG;AAChB,cAAU,kBAAkB,KAAK,MAAM,UAAU,CAAC,CAAC;AACnD,WAAO,KAAK,MAAM,GAAG,OAAO;AAAA,EAC9B;AAGA,MAAI;AACJ,QAAM,OAAO,KAAK,QAAQ,GAAG;AAC7B,MAAI,QAAQ,GAAG;AACb,UAAM,KAAK,KAAK,MAAM,OAAO,CAAC;AAC9B,WAAO,KAAK,MAAM,GAAG,IAAI;AACzB,iBAAa,oBAAI,IAAoB;AACrC,eAAW,QAAQ,GAAG,MAAM,GAAG,GAAG;AAChC,YAAM,QAAQ,KAAK,QAAQ,GAAG;AAC9B,UAAI,QAAQ,GAAG;AACb,mBAAW;AAAA,UACT,mBAAmB,KAAK,MAAM,GAAG,KAAK,CAAC;AAAA,UACvC,mBAAmB,KAAK,MAAM,QAAQ,CAAC,CAAC;AAAA,QAC1C;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAGA,MAAI;AACJ,QAAM,QAAQ,KAAK,YAAY,GAAG;AAClC,MAAI,QAAQ,GAAG;AAEb,cAAU,kBAAkB,KAAK,MAAM,QAAQ,CAAC,CAAC;AACjD,WAAO,KAAK,MAAM,GAAG,KAAK;AAAA,EAC5B;AAGA,QAAM,WAAW,KAAK,QAAQ,GAAG;AACjC,MAAI,WAAW,EAAG,QAAO;AACzB,QAAM,OAAO,KAAK,MAAM,GAAG,QAAQ;AACnC,MAAI,YAAY,KAAK,MAAM,WAAW,CAAC;AAGvC,MAAI;AACJ,MAAI;AACJ,QAAM,aAAa,UAAU,QAAQ,GAAG;AACxC,MAAI,cAAc,KAAK,SAAS,OAAO;AAErC,gBAAY,kBAAkB,UAAU,MAAM,GAAG,UAAU,CAAC;AAC5D,WAAO,kBAAkB,UAAU,MAAM,aAAa,CAAC,CAAC;AAAA,EAC1D,WAAW,cAAc,KAAK,SAAS,SAAS,UAAU,WAAW,KAAK,GAAG;AAE3E,gBAAY,kBAAkB,UAAU,MAAM,GAAG,UAAU,CAAC;AAC5D,WAAO,kBAAkB,UAAU,MAAM,aAAa,CAAC,CAAC;AAAA,EAC1D,OAAO;AACL,WAAO,kBAAkB,SAAS;AAAA,EACpC;AAEA,MAAI,CAAC,QAAQ,CAAC,KAAM,QAAO;AAE3B,SAAO;AAAA,IACL;AAAA,IACA,GAAI,YAAY,EAAE,UAAU,IAAI,CAAC;AAAA,IACjC;AAAA,IACA,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC7B,GAAI,cAAc,WAAW,OAAO,IAAI,EAAE,WAAW,IAAI,CAAC;AAAA,IAC1D,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC/B;AACF;AAKO,SAAS,qBAAqB,WAAgC;AACnE,SAAO,uBAAuB,SAAS;AACzC;AAMO,SAAS,qBAAqB,MAAuC;AAC1E,SAAO,uBAAuB,IAAI;AACpC;AAuBO,SAAS,cACd,WACA,MACA,SACQ;AACR,QAAM,OAAO,qBAAqB,SAAS;AAM3C,MAAI,cAAc,MAAM;AACtB,UAAM,gBAAgB,YAAY,SAAY,IAAI,kBAAkB,OAAO,CAAC,KAAK;AACjF,WAAO,OAAO,IAAI,IAAI,IAAI,GAAG,aAAa;AAAA,EAC5C;AACA,QAAM,WAAW,KAAK,QAAQ,GAAG;AAGjC,MAAI,WAAW,GAAG;AAChB,UAAM,YAAY,KAAK,MAAM,GAAG,QAAQ;AACxC,UAAM,UAAU,KAAK,MAAM,WAAW,CAAC;AACvC,QAAI,QAAQ,SAAS,GAAG;AACtB,aAAO,UAAU;AAAA,QACf;AAAA,QACA;AAAA,QACA,MAAM;AAAA,QACN,GAAI,YAAY,SAAY,EAAE,QAAQ,IAAI,CAAC;AAAA,MAC7C,CAAC;AAAA,IACH;AAAA,EACF;AACA,SAAO,UAAU;AAAA,IACf;AAAA,IACA;AAAA,IACA,GAAI,YAAY,SAAY,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC7C,CAAC;AACH;AAwBO,SAAS,mBAAmB,MAA+C;AAChF,QAAM,QAAQ,UAAU,IAAI;AAC5B,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,YAAY,qBAAqB,MAAM,IAAI;AACjD,MAAI,CAAC,UAAW,QAAO;AACvB,QAAM,YAAY,MAAM;AACxB,QAAM,OAAO,YAAY,GAAG,SAAS,IAAI,MAAM,IAAI,KAAK,MAAM;AAC9D,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,GAAI,MAAM,YAAY,SAAY,EAAE,SAAS,MAAM,QAAQ,IAAI,CAAC;AAAA,EAClE;AACF;AAMA,SAAS,kBAAkB,SAAyB;AAGlD,SAAO,QACJ,QAAQ,MAAM,KAAK,EACnB,QAAQ,MAAM,KAAK,EACnB,QAAQ,OAAO,KAAK;AACzB;AAKA,SAAS,kBAAkB,SAAyB;AAClD,MAAI;AACF,WAAO,mBAAmB,OAAO;AAAA,EACnC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;;;AC/SA,SAAS,gCAAgC;AAiBzC,IAAM,+BAA6F;AAAA,EACjG,YAAY;AAAA,EACZ,YAAY;AAAA,EACZ,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,KAAK;AAAA,EACL,MAAM;AAAA,EACN,OAAO;AAAA,EACP,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,GAAG;AAAA,EACH,KAAK;AAAA,EACL,MAAM;AAAA;AAAA,EACN,OAAO;AACT;AAcA,IAAM,iBAA0D;AAAA,EAC9D,KAAK;AAAA,EACL,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,IAAI;AAAA,EACJ,QAAQ;AAAA,EACR,KAAK;AAAA,EACL,MAAM;AAAA,EACN,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,KAAK;AACP;AAEA,SAAS,qBAAqB,UAA2D;AACvF,aAAW,QAAQ,UAAU;AAC3B,QAAI,KAAK,SAAS,cAAc,KAAK,UAAU,kBAAmB,QAAO;AACzE,QAAI,KAAK,SAAS,eAAe,KAAK,UAAU,kBAAkB,KAAK,UAAU,sBAAsB,KAAK,UAAU,oBAAoB;AACxI,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,sBAAsB,UAAsD;AACnF,MAAI,SAAS,aAAa,QAAQ;AAChC,WAAO,qBAAqB,SAAS,QAAQ;AAAA,EAC/C;AACA,SAAO,6BAA6B,SAAS,QAAQ;AACvD;AAEA,SAAS,iBAAiB,UAAiD;AACzE,QAAM,OAAO,oBAAI,IAAY;AAC7B,QAAM,MAAgB,CAAC;AACvB,aAAW,QAAQ,UAAU;AAC3B,QAAI,KAAK,SAAS,WAAY;AAC9B,QAAI,KAAK,IAAI,KAAK,IAAI,EAAG;AACzB,SAAK,IAAI,KAAK,IAAI;AAClB,QAAI,KAAK,KAAK,IAAI;AAAA,EACpB;AACA,SAAO,IAAI,KAAK;AAClB;AAMO,SAAS,qBACd,UACA,aACuB;AACvB,QAAM,YAAY,sBAAsB,QAAQ;AAChD,MAAI,CAAC,UAAW,QAAO;AACvB,QAAM,eACJ,SAAS,SAAS,cACd,MACA,SAAS,KAAK,WAAW,GAAG,WAAW,GAAG,KAAK,SAAS,KAAK,WAAW,GAAG,WAAW,IAAI,IACxF,SAAS,KAAK,MAAM,YAAY,SAAS,CAAC,IAC1C,SAAS;AACjB,QAAM,YAAY,iBAAiB,SAAS,QAAQ;AACpD,SAAO;AAAA,IACL,IAAI,SAAS;AAAA,IACb;AAAA,IACA;AAAA,IACA,GAAI,SAAS,iBAAiB,EAAE,gBAAgB,SAAS,eAAe,IAAI,CAAC;AAAA,IAC7E,WAAW,CAAC,GAAG,SAAS,SAAS,EAAE,KAAK;AAAA,IACxC;AAAA,IACA,YAAY,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,SAAS,UAAU,CAAC;AAAA,IACxD,UAAU,eAAe,SAAS;AAAA,EACpC;AACF;AAUA,IAAM,2BAA8C;AAAA,EAClD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAiBA,eAAsB,mBACpB,aACA,SACsB;AACtB,QAAM,SAAS,MAAM,yBAAyB;AAAA,IAC5C,GAAI,WAAW,CAAC;AAAA,IAChB;AAAA,IACA,oBAAoB,CAAC,GAAG,0BAA0B,GAAI,SAAS,sBAAsB,CAAC,CAAE;AAAA,EAC1F,CAAC;AACD,QAAM,SAAsB,CAAC;AAC7B,aAAW,YAAY,OAAO,YAAY;AACxC,UAAM,YAAY,qBAAqB,UAAU,OAAO,WAAW;AACnE,QAAI,UAAW,QAAO,KAAK,SAAS;AAAA,EACtC;AACA,SAAO,KAAK,CAAC,GAAG,MAAM;AACpB,WACE,EAAE,UAAU,cAAc,EAAE,SAAS,KACrC,EAAE,aAAa,cAAc,EAAE,YAAY,KAC3C,EAAE,GAAG,cAAc,EAAE,EAAE;AAAA,EAE3B,CAAC;AACD,SAAO;AACT;AAMO,SAAS,qBAAqB,WAAkC;AACrE,SAAO,eAAe,SAAS;AACjC;;;ACjMA,SAAS,YAAY,oBAAoB;AACzC,SAAS,SAAS,MAAM,UAAU,WAAAA,gBAAe;;;ACKjD,SAAS,eAAe;AAUjB,SAAS,cAAc,WAAsB,SAAmC;AACrF,QAAMC,YAAW,UAAU,gBAAgB;AAC3C,SAAO,QAAQ,cAAc,QAAQ,QAAQ,aAAaA,SAAQ,IAAI,QAAQA,SAAQ;AACxF;AAYO,SAAS,UAAU,MAAc,WAA2B;AACjE,SAAO,QAAQ,MAAM,SAAS;AAChC;;;ADiBA,SAAS,eAAe,cAAsB,QAA+B;AAC3E,QAAM,aAA0D;AAAA,IAC9D,EAAE,MAAM,kBAAkB,MAAM,OAAO;AAAA,IACvC,EAAE,MAAM,qBAAqB,MAAM,MAAM;AAAA,IACzC,EAAE,MAAM,aAAa,MAAM,OAAO;AAAA,IAClC,EAAE,MAAM,aAAa,MAAM,MAAM;AAAA,EACnC;AAEA,QAAM,UAAU,SAASC,SAAQ,MAAM,IAAI;AAC3C,MAAI,MAAMA,SAAQ,YAAY;AAE9B,aAAS;AACP,eAAW,KAAK,YAAY;AAC1B,YAAM,YAAY,KAAK,KAAK,EAAE,IAAI;AAClC,UAAI,WAAW,SAAS,EAAG,QAAO,EAAE,MAAM,EAAE,MAAM,MAAM,UAAU;AAAA,IACpE;AACA,QAAI,WAAW,QAAQ,QAAS;AAChC,UAAM,SAAS,QAAQ,GAAG;AAC1B,QAAI,WAAW,IAAK;AAEpB,QAAI,CAAC,QAAS;AACd,UAAM;AAAA,EACR;AACA,SAAO,EAAE,MAAM,QAAQ,MAAM,GAAG;AAClC;AAGA,SAAS,gBAAgB,SAAyB;AAChD,QAAM,QAAQ,QAAQ,QAAQ,GAAG;AACjC,UAAQ,UAAU,KAAK,UAAU,QAAQ,MAAM,GAAG,KAAK,GAAG,KAAK;AACjE;AAuBA,SAAS,0BAA0B,aAAqB,cAA2C;AACjG,QAAM,WAAW,oBAAI,IAAoB;AACzC,QAAM,QAAQ,YAAY,MAAM,OAAO;AAEvC,MAAI,cAAc;AAClB,MAAI,mBAAmB;AACvB,MAAI;AAEJ,aAAW,OAAO,OAAO;AACvB,QAAI,IAAI,KAAK,MAAM,MAAM,IAAI,UAAU,EAAE,WAAW,GAAG,EAAG;AAG1D,QAAI,CAAC,MAAM,KAAK,GAAG,GAAG;AACpB,UAAI,YAAa;AACjB,oBAAc,IAAI,WAAW,YAAY;AACzC;AAAA,IACF;AACA,QAAI,CAAC,YAAa;AAElB,UAAM,SAAS,IAAI,SAAS,IAAI,UAAU,EAAE;AAC5C,UAAM,OAAO,IAAI,KAAK;AAEtB,QAAI,WAAW,GAAG;AAEhB,YAAM,MAAM,KAAK,SAAS,GAAG,IAAI,QAAQ,KAAK,MAAM,GAAG,EAAE,CAAC,IAAI;AAC9D,yBAAmB,QAAQ;AAC3B,uBAAiB;AACjB;AAAA,IACF;AACA,QAAI,CAAC,iBAAkB;AAEvB,QAAI,WAAW,GAAG;AAChB,uBAAiB;AACjB;AAAA,IACF;AACA,QAAI,WAAW,KAAK,KAAK,SAAS,GAAG,GAAG;AACtC,uBAAiB,QAAQ,KAAK,MAAM,GAAG,EAAE,CAAC;AAC1C;AAAA,IACF;AACA,QAAI,UAAU,KAAK,kBAAkB,KAAK,WAAW,UAAU,GAAG;AAChE,YAAM,UAAU,gBAAgB,QAAQ,KAAK,MAAM,WAAW,MAAM,EAAE,KAAK,CAAC,CAAC;AAG7E,UAAI,WAAW,CAAC,QAAQ,WAAW,OAAO,KAAK,CAAC,QAAQ,WAAW,OAAO,GAAG;AAC3E,iBAAS,IAAI,gBAAgB,OAAO;AAAA,MACtC;AACA,uBAAiB;AAAA,IACnB;AAAA,EACF;AAEA,SAAO;AACT;AAEA,SAAS,QAAQ,OAAuB;AACtC,QAAM,UAAU,MAAM,KAAK;AAC3B,MACG,QAAQ,WAAW,GAAG,KAAK,QAAQ,SAAS,GAAG,KAC/C,QAAQ,WAAW,GAAG,KAAK,QAAQ,SAAS,GAAG,GAChD;AACA,WAAO,QAAQ,MAAM,GAAG,EAAE;AAAA,EAC5B;AACA,SAAO;AACT;AAKA,SAAS,qBAAqB,aAA0C;AACtE,QAAM,WAAW,oBAAI,IAAoB;AACzC,MAAI;AACF,UAAM,OAAO,KAAK,MAAM,WAAW;AAEnC,UAAM,OAAO,KAAK,gBAAgB,CAAC;AACnC,eAAW,CAAC,MAAM,IAAI,KAAK,OAAO,QAAQ,IAAI,GAAG;AAC/C,YAAM,UAAU;AAChB,UAAI,QAAQ,SAAS;AAEnB,cAAM,eAAe,QAAQ,QAAQ,QAAQ,YAAY,EAAE;AAC3D,iBAAS,IAAI,MAAM,YAAY;AAAA,MACjC;AAAA,IACF;AAEA,UAAM,WAAW,KAAK,YAAY,CAAC;AACnC,eAAW,OAAO,OAAO,KAAK,QAAQ,GAAG;AACvC,YAAM,UAAU,SAAS,GAAG;AAC5B,UAAI,QAAQ,SAAS;AAEnB,cAAM,OAAO,IAAI,QAAQ,mBAAmB,EAAE;AAC9C,YAAI,CAAC,SAAS,IAAI,IAAI,GAAG;AACvB,mBAAS,IAAI,MAAM,QAAQ,OAAO;AAAA,QACpC;AAAA,MACF;AAAA,IACF;AAAA,EACF,QAAQ;AAAA,EAER;AACA,SAAO;AACT;AAKA,SAAS,iBAAiB,MAAwB;AAChD,SAAO;AAAA,IACL,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,EACtC;AACF;AAKA,SAAS,iBAAiB,MAAwB;AAChD,SAAO;AAAA,IACL,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,EACtC;AACF;AAKA,SAAS,gBAAgB,SAAkC;AACzD,UAAQ,SAAS;AAAA,IACf,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT;AACE,aAAO;AAAA,EACX;AACF;AAMA,SAAS,cAAc,MAAgC;AACrD,MAAI,KAAK,WAAW,OAAO,KAAK,KAAK,WAAW,OAAO,KAAK,KAAK,WAAW,YAAY,GAAG;AACzF,WAAO;AAAA,EACT;AACA,MAAI,KAAK,WAAW,MAAM,KAAK,KAAK,WAAW,SAAS,KAAK,KAAK,WAAW,MAAM,GAAG;AACpF,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAKA,SAAS,eAAe,MAAuB;AAC7C,SACE,CAAC,KAAK,WAAW,OAAO,KACxB,CAAC,KAAK,WAAW,OAAO,KACxB,CAAC,KAAK,WAAW,YAAY,KAC7B,CAAC,KAAK,WAAW,MAAM,KACvB,CAAC,KAAK,WAAW,SAAS,KAC1B,CAAC,KAAK,WAAW,MAAM;AAE3B;AAIO,IAAM,aAAN,MAA6C;AAAA,EACzC,YAAyB;AAAA,EAElC,MAAM,UACJ,WACA,SAC2C;AAC3C,UAAM,eAAwC,CAAC;AAC/C,UAAM,OAAO,cAAc,WAAW,OAAO;AAG7C,UAAM,eAAe;AAAA,MACnB;AAAA,MACA,UAAU,UAAU,KAAK,CAAC,MAAM,EAAE,SAAS,cAAc,CAAC,KAAK;AAAA,IACjE;AAGA,QAAI;AACJ,QAAI;AACJ,QAAI;AACF,wBAAkB,aAAa,cAAc,OAAO;AACpD,YAAM,KAAK,MAAM,eAAe;AAAA,IAClC,QAAQ;AACN,aAAO,CAAC;AAAA,IACV;AAEA,UAAM,aAAa,iBAAiB,YAAY;AAGhD,UAAM,WAAW,eAAe,MAAM,QAAQ,WAAW;AACzD,UAAM,mBAAmB,oBAAI,IAAoB;AACjD,QAAI;AACJ,QAAI,SAAS,SAAS,QAAQ;AAC5B,UAAI;AACF,cAAM,cAAc,aAAa,SAAS,MAAM,OAAO;AAGvD,cAAM,eACJ,SAAS,QAAQ,SAAS,IAAI,GAAG,IAAI,EAAE,MAAM,OAAO,EAAE,OAAO,OAAO,EAAE,KAAK,GAAG,KAAK;AACrF,cAAM,SAAS,0BAA0B,aAAa,YAAY;AAClE,mBAAW,CAAC,GAAG,CAAC,KAAK,OAAQ,kBAAiB,IAAI,GAAG,CAAC;AACtD,YAAI,OAAO,OAAO,EAAG,UAAS,iBAAiB,SAAS,IAAI;AAAA,MAC9D,QAAQ;AAAA,MAER;AAAA,IACF,WAAW,SAAS,SAAS,OAAO;AAClC,UAAI;AACF,cAAM,cAAc,aAAa,SAAS,MAAM,OAAO;AACvD,cAAM,SAAS,qBAAqB,WAAW;AAC/C,mBAAW,CAAC,GAAG,CAAC,KAAK,OAAQ,kBAAiB,IAAI,GAAG,CAAC;AACtD,iBAAS,iBAAiB,SAAS,IAAI;AAAA,MACzC,QAAQ;AAAA,MAER;AAAA,IACF;AAGA,UAAM,WAAuE;AAAA,MAC3E,EAAE,MAAM,gBAAgB,MAAM,IAAI,aAAa;AAAA,MAC/C,EAAE,MAAM,mBAAmB,MAAM,IAAI,gBAAgB;AAAA,MACrD,EAAE,MAAM,oBAAoB,MAAM,IAAI,iBAAiB;AAAA,MACvD,EAAE,MAAM,wBAAwB,MAAM,IAAI,qBAAqB;AAAA,IACjE;AAEA,UAAM,OAAO,oBAAI,IAAY;AAE7B,eAAW,WAAW,UAAU;AAC9B,UAAI,CAAC,QAAQ,KAAM;AACnB,YAAM,QAAQ,gBAAgB,QAAQ,IAAI;AAE1C,iBAAW,CAAC,MAAM,SAAS,KAAK,OAAO,QAAQ,QAAQ,IAAI,GAAG;AAC5D,cAAMC,YAAW,GAAG,IAAI;AACxB,YAAI,KAAK,IAAIA,SAAQ,EAAG;AACxB,aAAK,IAAIA,SAAQ;AAEjB,cAAM,aAAa,eAAe,SAAS;AAC3C,cAAM,SAAS,cAAc,SAAS;AAGtC,cAAM,SAAS,iBAAiB,IAAI,IAAI;AAGxC,cAAM,OAAO,cAAc,SACvB,UAAU,EAAE,MAAM,OAAO,MAAM,SAAS,OAAO,CAAC,IAChD,aACE,UAAU,EAAE,MAAM,OAAO,KAAK,CAAC,IAC/B;AAEN,cAAM,WAAuB,CAAC,UAAU;AACxC,YAAI,UAAU,OAAQ,UAAS,KAAK,MAAM;AAE1C,qBAAa,KAAK;AAAA,UAChB,IAAI,OAAO,UAAU,EAAE,IAAI,IAAI;AAAA,UAC/B,aAAa,UAAU;AAAA,UACvB,GAAI,OAAO,EAAE,KAAK,IAAI,CAAC;AAAA,UACvB,WAAW;AAAA,UACX;AAAA,UACA,YAAY,aAAa,aAAa,WAAW,eAAe,SAAS;AAAA,UACzE,QAAQ;AAAA,UACR;AAAA,UACA;AAAA,UACA,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;AAAA,UAC3B;AAAA,UACA;AAAA,QACF,CAAC;AAAA,MACH;AAAA,IACF;AAKA,WAAO;AAAA,EACT;AACF;AAKO,IAAM,aAAa,IAAI,WAAW;;;AExYzC,SAAS,gBAAAC,qBAAoB;AAC7B,SAAS,QAAAC,aAAY;AAiBrB,SAASC,kBAAiB,MAAwB;AAChD,SAAO,EAAE,MAAM,YAAY,QAAQ,MAAM,cAAa,oBAAI,KAAK,GAAE,YAAY,EAAE;AACjF;AAEA,SAASC,kBAAiB,MAAwB;AAChD,SAAO,EAAE,MAAM,YAAY,QAAQ,MAAM,cAAa,oBAAI,KAAK,GAAE,YAAY,EAAE;AACjF;AASA,SAAS,kBAAkB,SAAgC;AACzD,QAAM,WAA0B,CAAC;AACjC,MAAI,iBAAiB;AACrB,MAAI,eAAyB,CAAC;AAC9B,aAAW,OAAO,QAAQ,MAAM,IAAI,GAAG;AACrC,UAAM,OAAO,IAAI,KAAK;AACtB,QAAI,KAAK,WAAW,GAAG,KAAK,SAAS,GAAI;AACzC,UAAM,eAAe,KAAK,MAAM,gBAAgB;AAChD,QAAI,cAAc;AAChB,UAAI,aAAa,SAAS,EAAG,UAAS,KAAK,EAAE,MAAM,gBAAgB,OAAO,aAAa,CAAC;AACxF,uBAAiB,aAAa,CAAC;AAC/B,qBAAe,CAAC;AAAA,IAClB,OAAO;AACL,mBAAa,KAAK,GAAG;AAAA,IACvB;AAAA,EACF;AACA,MAAI,aAAa,SAAS,EAAG,UAAS,KAAK,EAAE,MAAM,gBAAgB,OAAO,aAAa,CAAC;AACxF,SAAO;AACT;AAEA,SAAS,iBAAiB,cAAwB,KAAuB;AACvE,QAAM,SAAmB,CAAC;AAC1B,MAAI,UAAU;AACd,aAAW,QAAQ,cAAc;AAC/B,UAAM,UAAU,KAAK,KAAK;AAC1B,QAAI,CAAC,SAAS;AACZ,YAAM,QAAQ,QAAQ,MAAM,IAAI,OAAO,IAAI,GAAG,cAAc,CAAC;AAC7D,UAAI,OAAO;AACT,kBAAU;AACV,cAAM,OAAO,QAAQ,MAAM,MAAM,CAAC,EAAE,MAAM;AAC1C,YAAI,KAAK,SAAS,GAAG,GAAG;AACtB,gBAAM,QAAQ,KAAK,QAAQ,eAAe,EAAE,EAAE,KAAK;AACnD,qBAAW,QAAQ,MAAM,MAAM,GAAG,GAAG;AACnC,kBAAM,UAAU,KAAK,KAAK,EAAE,QAAQ,UAAU,EAAE,EAAE,KAAK;AACvD,gBAAI,QAAS,QAAO,KAAK,OAAO;AAAA,UAClC;AACA,oBAAU;AAAA,QACZ;AAAA,MACF;AAAA,IACF,OAAO;AACL,YAAM,WAAW,QAAQ,QAAQ,GAAG;AACpC,UAAI,YAAY,GAAG;AACjB,cAAM,QAAQ,QAAQ,MAAM,GAAG,QAAQ,EAAE,KAAK;AAC9C,mBAAW,QAAQ,MAAM,MAAM,GAAG,GAAG;AACnC,gBAAM,UAAU,KAAK,KAAK,EAAE,QAAQ,UAAU,EAAE,EAAE,KAAK;AACvD,cAAI,QAAS,QAAO,KAAK,OAAO;AAAA,QAClC;AACA,kBAAU;AAAA,MACZ,OAAO;AACL,cAAM,UAAU,QAAQ,QAAQ,MAAM,EAAE,EAAE,KAAK,EAAE,QAAQ,UAAU,EAAE,EAAE,KAAK;AAC5E,YAAI,QAAS,QAAO,KAAK,OAAO;AAAA,MAClC;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,YAAY,MAAgE;AACnF,MAAI,IAAI,KAAK,KAAK;AAClB,MAAI,EAAE,QAAQ,YAAY,EAAE;AAC5B,QAAM,QAAQ,EAAE,MAAM,uCAAuC;AAC7D,MAAI,CAAC,MAAO,QAAO,EAAE,MAAM,GAAG,YAAY,OAAU;AACpD,SAAO,EAAE,MAAM,MAAM,CAAC,GAAI,YAAY,MAAM,CAAC,GAAG,KAAK,KAAK,OAAU;AACtE;AAIA,SAAS,mBAAmB,SAAkG;AAC5H,QAAM,OAAwF,CAAC;AAC/F,QAAM,WAAW,kBAAkB,OAAO;AAE1C,QAAM,iBAAiB,SAAS,KAAK,CAAC,MAAM,EAAE,SAAS,SAAS;AAChE,MAAI,gBAAgB;AAClB,UAAM,WAAW,iBAAiB,eAAe,OAAO,cAAc;AACtE,eAAW,QAAQ,UAAU;AAC3B,YAAM,EAAE,MAAM,WAAW,IAAI,YAAY,IAAI;AAC7C,UAAI,KAAM,MAAK,KAAK,EAAE,MAAM,YAAY,OAAO,UAAU,CAAC;AAAA,IAC5D;AAAA,EACF;AAEA,aAAW,WAAW,UAAU;AAC9B,QAAI,QAAQ,SAAS,iCAAiC;AAEpD,iBAAW,QAAQ,QAAQ,OAAO;AAChC,cAAM,UAAU,KAAK,KAAK;AAC1B,cAAM,aAAa,QAAQ,MAAM,4BAA4B;AAC7D,YAAI,YAAY;AACd,gBAAM,WAAW,iBAAiB,QAAQ,OAAO,WAAW,CAAC,CAAE;AAC/D,qBAAW,QAAQ,UAAU;AAC3B,kBAAM,EAAE,MAAM,WAAW,IAAI,YAAY,IAAI;AAC7C,gBAAI,KAAM,MAAK,KAAK,EAAE,MAAM,YAAY,OAAO,WAAW,CAAC;AAAA,UAC7D;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;AAIA,SAAS,qBAAqB,SAA0E;AACtG,QAAM,OAAgE,CAAC;AACvE,aAAW,OAAO,QAAQ,MAAM,IAAI,GAAG;AACrC,UAAM,OAAO,IAAI,KAAK;AACtB,QAAI,CAAC,QAAQ,KAAK,WAAW,GAAG,KAAK,KAAK,WAAW,GAAG,EAAG;AAC3D,UAAM,EAAE,MAAM,WAAW,IAAI,YAAY,IAAI;AAC7C,QAAI,KAAM,MAAK,KAAK,EAAE,MAAM,WAAW,CAAC;AAAA,EAC1C;AACA,SAAO;AACT;AAIA,SAAS,iBAAiB,SAAkG;AAC1H,QAAM,OAAwF,CAAC;AAC/F,QAAM,WAAW,kBAAkB,OAAO;AAC1C,aAAW,WAAW,UAAU;AAC9B,UAAM,QAAyB,QAAQ,SAAS,iBAAiB,gBAAgB;AACjF,eAAW,QAAQ,QAAQ,OAAO;AAChC,YAAM,UAAU,KAAK,KAAK;AAC1B,UAAI,QAAQ,WAAW,GAAG,EAAG;AAC7B,YAAM,QAAQ,QAAQ,MAAM,gDAAgD;AAC5E,UAAI,OAAO;AACT,cAAM,aAAa,MAAM,CAAC,MAAO,MAAM,SAAY,MAAM,CAAC;AAC1D,aAAK,KAAK,EAAE,MAAM,MAAM,CAAC,GAAI,YAAY,MAAM,CAAC;AAAA,MAClD;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,8BAA8B,SAAsC;AAC3E,QAAM,WAAW,oBAAI,IAAoB;AACzC,aAAW,OAAO,QAAQ,MAAM,IAAI,GAAG;AACrC,UAAM,OAAO,IAAI,KAAK;AACtB,QAAI,CAAC,QAAQ,KAAK,WAAW,GAAG,KAAK,KAAK,WAAW,GAAG,EAAG;AAC3D,UAAM,QAAQ,KAAK,MAAM,gDAAgD;AACzE,QAAI,MAAO,UAAS,IAAI,MAAM,CAAC,GAAI,MAAM,CAAC,CAAE;AAAA,EAC9C;AACA,SAAO;AACT;AAsCO,IAAM,gBAAN,MAAgD;AAAA,EAC5C,YAAyB;AAAA,EAElC,MAAM,UAAU,WAAsB,SAAsE;AAC1G,UAAM,eAAwC,CAAC;AAC/C,UAAM,OAAO,cAAc,WAAW,OAAO;AAC7C,UAAM,OAAO,oBAAI,IAAY;AAE7B,UAAM,eAAe,UAAU,UAAU,KAAK,CAAC,MAAM,EAAE,SAAS,gBAAgB,CAAC,KAAK,KAAK,WAAWC,MAAK,MAAM,gBAAgB,CAAC;AAClI,UAAM,kBAAkB,UAAU,UAAU,KAAK,CAAC,MAAM,EAAE,SAAS,kBAAkB,CAAC,KAAK,KAAK,WAAWA,MAAK,MAAM,kBAAkB,CAAC;AACzI,UAAM,aAAa,UAAU,UAAU,KAAK,CAAC,MAAM,EAAE,SAAS,SAAS,CAAC,KAAK,KAAK,WAAWA,MAAK,MAAM,SAAS,CAAC;AAElH,UAAM,eAAe,KAAK,eAAe,IAAI;AAE7C,QAAI,UAA2G,CAAC;AAChH,QAAI;AAEJ,QAAI,cAAc;AAChB,UAAI;AACF,cAAM,UAAUC,cAAaD,MAAK,MAAM,gBAAgB,GAAG,OAAO;AAClE,sBAAcE,kBAAiBF,MAAK,MAAM,gBAAgB,CAAC;AAC3D,cAAM,SAAS,mBAAmB,OAAO;AACzC,mBAAW,KAAK,OAAQ,SAAQ,KAAK,EAAE,GAAG,GAAG,QAAQ,iBAAiB,CAAC;AAAA,MACzE,QAAQ;AAAA,MAAe;AAAA,IACzB;AAEA,QAAI,kBAAkB,oBAAI,IAAoB;AAC9C,QAAI;AAEJ,QAAI,iBAAiB;AACnB,UAAI;AACF,cAAM,UAAUC,cAAaD,MAAK,MAAM,kBAAkB,GAAG,OAAO;AACpE,yBAAiBE,kBAAiBF,MAAK,MAAM,kBAAkB,CAAC;AAChE,cAAM,SAAS,qBAAqB,OAAO;AAC3C,mBAAW,KAAK,QAAQ;AACtB,cAAI,CAAC,QAAQ,KAAK,CAAC,aAAa,SAAS,SAAS,EAAE,IAAI,GAAG;AACzD,oBAAQ,KAAK,EAAE,GAAG,GAAG,OAAO,WAAW,QAAQ,mBAAmB,CAAC;AAAA,UACrE;AAAA,QACF;AACA,0BAAkB,8BAA8B,OAAO;AAAA,MACzD,QAAQ;AAAA,MAAe;AAAA,IACzB;AAEA,QAAI,YAAY;AACd,UAAI;AACF,cAAM,UAAUC,cAAaD,MAAK,MAAM,SAAS,GAAG,OAAO;AAC3D,YAAI,CAAC,YAAa,eAAcE,kBAAiBF,MAAK,MAAM,SAAS,CAAC;AACtE,cAAM,SAAS,iBAAiB,OAAO;AACvC,mBAAW,KAAK,QAAQ;AACtB,cAAI,CAAC,QAAQ,KAAK,CAAC,aAAa,SAAS,SAAS,EAAE,IAAI,GAAG;AACzD,oBAAQ,KAAK,EAAE,GAAG,GAAG,QAAQ,UAAU,CAAC;AAAA,UAC1C;AAAA,QACF;AAAA,MACF,QAAQ;AAAA,MAAe;AAAA,IACzB;AAEA,QAAI;AACJ,QAAI,cAAc;AAChB,UAAI;AACF,QAAAC,cAAa,cAAc,OAAO;AAClC,iBAASE,kBAAiB,YAAY;AAAA,MACxC,QAAQ;AAAA,MAAe;AAAA,IACzB;AAEA,UAAM,aAAa,eAAe;AAElC,eAAW,OAAO,SAAS;AACzB,UAAI,KAAK,IAAI,IAAI,IAAI,EAAG;AACxB,WAAK,IAAI,IAAI,IAAI;AAEjB,YAAM,SAAS,gBAAgB,IAAI,IAAI,IAAI,KAAK;AAChD,YAAM,aAAa,CAAC,IAAI,cAAe,CAAC,IAAI,WAAW,WAAW,OAAO,KAAK,CAAC,IAAI,WAAW,WAAW,MAAM,KAAK,CAAC,IAAI,WAAW,WAAW,IAAI;AAEnJ,YAAM,OAAO,cAAc,SAAS,UAAU,EAAE,MAAM,UAAU,MAAM,IAAI,MAAM,SAAS,OAAO,CAAC,IAC7F,aAAa,UAAU,EAAE,MAAM,UAAU,MAAM,IAAI,KAAK,CAAC,IAAI;AAEjE,YAAM,WAAuB,CAAC;AAC9B,UAAI,WAAY,UAAS,KAAK,UAAU;AACxC,UAAI,UAAU,OAAQ,UAAS,KAAK,MAAM;AAC1C,UAAI,SAAS,WAAW,GAAG;AACzB,iBAAS,KAAK,EAAE,MAAM,YAAY,QAAQ,IAAI,QAAQ,cAAa,oBAAI,KAAK,GAAE,YAAY,EAAE,CAAC;AAAA,MAC/F;AAEA,YAAM,SACJ,IAAI,eAAe,IAAI,WAAW,WAAW,OAAO,KAAK,IAAI,WAAW,WAAW,IAAI,KAAK,eAC1F,IAAI,YAAY,WAAW,MAAM,IAAI,mBAAmB;AAE5D,mBAAa,KAAK;AAAA,QAChB,IAAI,OAAO,UAAU,EAAE,IAAI,IAAI,IAAI;AAAA,QACnC,aAAa,UAAU;AAAA,QACvB,GAAI,OAAO,EAAE,KAAK,IAAI,CAAC;AAAA,QACvB,WAAW;AAAA,QACX,MAAM,IAAI;AAAA,QACV,YAAY,aAAa,aAAa,WAAW,eAAe,SAAS;AAAA,QACzE,QAAQ;AAAA,QACR,OAAO,IAAI;AAAA,QACX,GAAI,IAAI,aAAa,EAAE,WAAW,IAAI,WAAW,IAAI,CAAC;AAAA,QACtD,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;AAAA,QAC3B;AAAA,QACA;AAAA,MACF,CAAC;AAAA,IACH;AAEA,WAAO;AAAA,EACT;AAAA,EAEQ,WAAW,UAA2B;AAC5C,QAAI;AAAE,MAAAF,cAAa,UAAU,OAAO;AAAG,aAAO;AAAA,IAAM,QAAQ;AAAE,aAAO;AAAA,IAAO;AAAA,EAC9E;AAAA,EAEQ,eAAeG,gBAA2C;AAChE,eAAW,QAAQ,CAAC,gBAAgB,eAAe,SAAS,GAAG;AAC7D,UAAI;AAAE,QAAAH,cAAaD,MAAKI,gBAAe,IAAI,GAAG,OAAO;AAAG,eAAOJ,MAAKI,gBAAe,IAAI;AAAA,MAAG,QAAQ;AAAA,MAAkB;AAAA,IACtH;AACA,WAAO;AAAA,EACT;AACF;AAEO,IAAM,gBAAgB,IAAI,cAAc;;;AC3U/C,SAAS,gBAAAC,qBAAoB;AAiB7B,SAASC,kBAAiB,MAAwB;AAChD,SAAO,EAAE,MAAM,YAAY,QAAQ,MAAM,cAAa,oBAAI,KAAK,GAAE,YAAY,EAAE;AACjF;AAEA,SAASC,kBAAiB,MAAwB;AAChD,SAAO,EAAE,MAAM,YAAY,QAAQ,MAAM,cAAa,oBAAI,KAAK,GAAE,YAAY,EAAE;AACjF;AASA,SAASC,mBAAkB,SAAgC;AACzD,QAAM,WAA0B,CAAC;AACjC,MAAI,iBAAiB;AACrB,MAAI,eAAyB,CAAC;AAC9B,aAAW,OAAO,QAAQ,MAAM,IAAI,GAAG;AACrC,UAAM,OAAO,IAAI,KAAK;AACtB,QAAI,KAAK,WAAW,GAAG,KAAK,SAAS,GAAI;AACzC,UAAM,eAAe,KAAK,MAAM,gBAAgB;AAChD,QAAI,cAAc;AAChB,UAAI,aAAa,SAAS,EAAG,UAAS,KAAK,EAAE,MAAM,gBAAgB,OAAO,aAAa,CAAC;AACxF,uBAAiB,aAAa,CAAC;AAC/B,qBAAe,CAAC;AAAA,IAClB,OAAO;AACL,mBAAa,KAAK,GAAG;AAAA,IACvB;AAAA,EACF;AACA,MAAI,aAAa,SAAS,EAAG,UAAS,KAAK,EAAE,MAAM,gBAAgB,OAAO,aAAa,CAAC;AACxF,SAAO;AACT;AAOA,SAAS,kBAAkB,MAA0D;AACnF,QAAM,UAAU,KAAK,KAAK;AAE1B,MAAI,QAAQ,WAAW,GAAG,EAAG,QAAO;AACpC,QAAM,QAAQ,QAAQ,MAAM,+BAA+B;AAC3D,MAAI,CAAC,MAAO,QAAO;AACnB,SAAO,EAAE,KAAK,MAAM,CAAC,GAAI,OAAO,MAAM,CAAC,EAAG,KAAK,EAAE;AACnD;AAOA,SAAS,gBAAgB,cAA8E;AACrG,QAAM,OAA6D,CAAC;AACpE,aAAW,OAAO,cAAc;AAC9B,UAAM,OAAO,IAAI,KAAK;AACtB,QAAI,KAAK,WAAW,GAAG,KAAK,SAAS,GAAI;AAGzC,UAAM,aAAa,KAAK,MAAM,0CAA0C;AACxE,QAAI,YAAY;AACd,YAAM,OAAO,WAAW,CAAC;AACzB,YAAM,QAAQ,WAAW,CAAC;AAC1B,YAAM,eAAe,MAAM,MAAM,yBAAyB;AAC1D,WAAK,KAAK,EAAE,MAAM,SAAS,eAAe,aAAa,CAAC,IAAK,OAAU,CAAC;AACxE;AAAA,IACF;AAGA,UAAM,cAAc,KAAK,MAAM,oCAAoC;AACnE,QAAI,aAAa;AACf,WAAK,KAAK,EAAE,MAAM,YAAY,CAAC,GAAI,SAAS,YAAY,CAAC,KAAM,OAAU,CAAC;AAC1E;AAAA,IACF;AAGA,UAAM,eAAe,kBAAkB,IAAI;AAC3C,QAAI,gBAAgB,CAAC,aAAa,MAAM,WAAW,GAAG,KAAK,CAAC,aAAa,MAAM,WAAW,GAAG,GAAG;AAAA,IAGhG;AAAA,EACF;AACA,SAAO;AACT;AAMA,SAAS,eAAe,SAAsC;AAC5D,QAAM,WAAW,oBAAI,IAAoB;AACzC,QAAM,QAAQ,QAAQ,MAAM,IAAI;AAChC,MAAI;AACJ,MAAI;AACJ,MAAI,YAAY;AAEhB,aAAW,OAAO,OAAO;AACvB,UAAM,OAAO,IAAI,KAAK;AACtB,QAAI,KAAK,WAAW,GAAG,KAAK,SAAS,GAAI;AAEzC,QAAI,KAAK,WAAW,IAAI,KAAK,KAAK,SAAS,SAAS,GAAG;AAErD,UAAI,aAAa,eAAe,gBAAgB;AAC9C,iBAAS,IAAI,aAAa,cAAc;AAAA,MAC1C;AACA,oBAAc;AACd,uBAAiB;AACjB,kBAAY;AACZ;AAAA,IACF;AAEA,QAAI,WAAW;AACb,UAAI,KAAK,WAAW,MAAM,GAAG;AAC3B,cAAM,IAAI,KAAK,MAAM,uBAAuB;AAC5C,YAAI,EAAG,eAAc,EAAE,CAAC;AAAA,MAC1B,WAAW,KAAK,WAAW,SAAS,GAAG;AACrC,cAAM,IAAI,KAAK,MAAM,0BAA0B;AAC/C,YAAI,EAAG,kBAAiB,EAAE,CAAC;AAAA,MAC7B;AAAA,IACF;AAAA,EACF;AAGA,MAAI,aAAa,eAAe,gBAAgB;AAC9C,aAAS,IAAI,aAAa,cAAc;AAAA,EAC1C;AAEA,SAAO;AACT;AAIA,SAAS,qBAAqB,SAAkC;AAC9D,UAAQ,SAAS;AAAA,IACf,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT;AACE,aAAO;AAAA,EACX;AACF;AAIO,IAAM,cAAN,MAA8C;AAAA,EAC1C,YAAyB;AAAA,EAElC,MAAM,UACJ,WACA,SAC2C;AAC3C,UAAM,eAAwC,CAAC;AAC/C,UAAM,OAAO,cAAc,WAAW,OAAO;AAC7C,UAAM,OAAO,oBAAI,IAAY;AAG7B,UAAM,gBAAgB,UAAU,UAAU,KAAK,CAAC,MAAM,EAAE,SAAS,YAAY,CAAC,MACxE,KAAK,WAAW,UAAU,MAAM,YAAY,CAAC,IAAI,eAAe;AAEtE,QAAI,CAAC,cAAe,QAAO,CAAC;AAE5B,UAAM,mBAAmB,UAAU,MAAM,aAAa;AACtD,QAAI;AACJ,QAAI;AACF,qBAAeC,cAAa,kBAAkB,OAAO;AAAA,IACvD,QAAQ;AACN,aAAO,CAAC;AAAA,IACV;AAEA,UAAM,aAAaH,kBAAiB,gBAAgB;AAGpD,UAAM,gBAAgB,UAAU,MAAM,YAAY;AAClD,QAAI,eAAe,oBAAI,IAAoB;AAC3C,QAAI;AACJ,QAAI;AACF,YAAM,cAAcG,cAAa,eAAe,OAAO;AACvD,qBAAe,eAAe,WAAW;AACzC,eAASF,kBAAiB,aAAa;AAAA,IACzC,QAAQ;AAAA,IAER;AAGA,UAAM,WAAWC,mBAAkB,YAAY;AAC/C,UAAM,cAAc,CAAC,gBAAgB,oBAAoB,oBAAoB;AAE7E,eAAW,WAAW,UAAU;AAG9B,YAAM,cAAc,QAAQ;AAC5B,UAAI;AAEJ,iBAAW,UAAU,aAAa;AAChC,YAAI,gBAAgB,UAAU,YAAY,SAAS,IAAI,MAAM,EAAE,GAAG;AAChE,yBAAe;AACf;AAAA,QACF;AAAA,MACF;AAEA,UAAI,CAAC,aAAc;AAEnB,YAAM,QAAQ,qBAAqB,YAAY;AAC/C,YAAM,OAAO,gBAAgB,QAAQ,KAAK;AAE1C,iBAAW,OAAO,MAAM;AACtB,YAAI,KAAK,IAAI,IAAI,IAAI,EAAG;AACxB,aAAK,IAAI,IAAI,IAAI;AAEjB,cAAM,SAAS,aAAa,IAAI,IAAI,IAAI,KAAK,IAAI;AACjD,cAAM,aAAa,CAAC,IAAI,WACtB,CAAC,IAAI,QAAQ,WAAW,OAAO,KAC/B,CAAC,IAAI,QAAQ,WAAW,MAAM,KAC9B,CAAC,IAAI,QAAQ,WAAW,KAAK;AAG/B,cAAM,OAAO,cAAc,SACvB,UAAU,EAAE,MAAM,QAAQ,MAAM,IAAI,MAAM,SAAS,OAAO,CAAC,IAC3D,aACE,UAAU,EAAE,MAAM,QAAQ,MAAM,IAAI,KAAK,CAAC,IAC1C;AAEN,cAAM,WAAuB,CAAC,UAAU;AACxC,YAAI,UAAU,UAAU,aAAa,IAAI,IAAI,IAAI,EAAG,UAAS,KAAK,MAAM;AAExE,cAAM,SACJ,IAAI,YAAY,IAAI,QAAQ,WAAW,OAAO,KAAK,IAAI,QAAQ,WAAW,MAAM,KAC5E,IAAI,QAAQ,WAAW,MAAM,IAC3B,mBACA,eACF;AAEN,qBAAa,KAAK;AAAA,UAChB,IAAI,OAAO,UAAU,EAAE,IAAI,IAAI,IAAI;AAAA,UACnC,aAAa,UAAU;AAAA,UACvB,GAAI,OAAO,EAAE,KAAK,IAAI,CAAC;AAAA,UACvB,WAAW;AAAA,UACX,MAAM,IAAI;AAAA,UACV,YAAY,aAAa,aAAa,WAAW,eAAe,SAAS;AAAA,UACzE,QAAQ;AAAA,UACR;AAAA,UACA,GAAI,IAAI,UAAU,EAAE,WAAW,IAAI,QAAQ,IAAI,CAAC;AAAA,UAChD,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;AAAA,UAC3B;AAAA,UACA;AAAA,QACF,CAAC;AAAA,MACH;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA,EAEQ,WAAW,UAA2B;AAC5C,QAAI;AACF,MAAAC,cAAa,UAAU,OAAO;AAC9B,aAAO;AAAA,IACT,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AACF;AAKO,IAAM,cAAc,IAAI,YAAY;;;AC/R3C,SAAS,gBAAAC,qBAAoB;AAiB7B,SAASC,kBAAiB,MAAwB;AAChD,SAAO,EAAE,MAAM,YAAY,QAAQ,MAAM,cAAa,oBAAI,KAAK,GAAE,YAAY,EAAE;AACjF;AAEA,SAASC,kBAAiB,MAAwB;AAChD,SAAO,EAAE,MAAM,YAAY,QAAQ,MAAM,cAAa,oBAAI,KAAK,GAAE,YAAY,EAAE;AACjF;AAEA,SAAS,eAAe,GAAmB;AACzC,SAAO,EAAE,QAAQ,OAAO,EAAE;AAC5B;AAqBA,SAAS,WAAW,SAAkC;AACpD,QAAM,OAAwB,CAAC;AAC/B,QAAM,QAAQ,QAAQ,MAAM,IAAI;AAChC,MAAI,iBAAiB;AAErB,aAAW,OAAO,OAAO;AACvB,UAAM,OAAO,IAAI,KAAK;AAGtB,QAAI,SAAS,MAAM,KAAK,WAAW,IAAI,EAAG;AAG1C,QAAI,KAAK,WAAW,WAAW,KAAK,KAAK,SAAS,GAAG,GAAG;AACtD,uBAAiB;AACjB;AAAA,IACF;AACA,QAAI,KAAK,WAAW,UAAU,KAAK,CAAC,KAAK,SAAS,GAAG,GAAG;AAEtD,YAAM,IAAI,KAAK,MAAM,0BAA0B;AAC/C,UAAI,GAAG;AACL,cAAM,WAAW,IAAI,SAAS,aAAa;AAC3C,aAAK,KAAK,EAAE,YAAY,EAAE,CAAC,GAAI,SAAS,eAAe,EAAE,CAAC,CAAE,GAAG,SAAS,CAAC;AAAA,MAC3E;AACA;AAAA,IACF;AAEA,QAAI,gBAAgB;AAClB,UAAI,SAAS,KAAK;AAChB,yBAAiB;AACjB;AAAA,MACF;AAEA,YAAM,IAAI,KAAK,MAAM,gBAAgB;AACrC,UAAI,GAAG;AACL,cAAM,WAAW,IAAI,SAAS,aAAa;AAC3C,aAAK,KAAK,EAAE,YAAY,EAAE,CAAC,GAAI,SAAS,eAAe,EAAE,CAAC,CAAE,GAAG,SAAS,CAAC;AAAA,MAC3E;AACA;AAAA,IACF;AAGA,QAAI,KAAK,WAAW,SAAS,KAAK,KAAK,WAAW,SAAS,KAAK,KAAK,WAAW,SAAS,GAAG;AAC1F;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;AAOA,SAAS,WAAW,SAAsC;AACxD,QAAM,WAAW,oBAAI,IAAoB;AACzC,aAAW,OAAO,QAAQ,MAAM,IAAI,GAAG;AACrC,UAAM,OAAO,IAAI,KAAK;AACtB,QAAI,CAAC,KAAM;AAEX,UAAM,IAAI,KAAK,MAAM,sBAAsB;AAC3C,QAAI,GAAG;AACL,YAAM,aAAa,EAAE,CAAC;AACtB,YAAM,UAAU,eAAe,EAAE,CAAC,CAAE;AAEpC,UAAI,CAAC,SAAS,IAAI,UAAU,GAAG;AAE7B,iBAAS,IAAI,YAAY,OAAO;AAAA,MAClC;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAKA,SAAS,kBAAkB,SAAqC;AAC9D,aAAW,OAAO,QAAQ,MAAM,IAAI,GAAG;AACrC,UAAM,OAAO,IAAI,KAAK;AACtB,UAAM,IAAI,KAAK,MAAM,iBAAiB;AACtC,QAAI,EAAG,QAAO,EAAE,CAAC;AAAA,EACnB;AACA,SAAO;AACT;AAIO,IAAM,YAAN,MAA4C;AAAA,EACxC,YAAyB;AAAA,EAElC,MAAM,UACJ,WACA,SAC2C;AAC3C,UAAM,eAAwC,CAAC;AAC/C,UAAM,OAAO,cAAc,WAAW,OAAO;AAC7C,UAAM,OAAO,oBAAI,IAAY;AAG7B,UAAM,YAAY,UAAU,UAAU,KAAK,CAAC,MAAM,EAAE,SAAS,QAAQ,CAAC,MAChE,KAAK,WAAW,UAAU,MAAM,QAAQ,CAAC,IAAI,WAAW;AAC9D,QAAI,CAAC,UAAW,QAAO,CAAC;AAExB,UAAM,mBAAmB,UAAU,MAAM,SAAS;AAClD,QAAI;AACJ,QAAI;AACF,qBAAeC,cAAa,kBAAkB,OAAO;AAAA,IACvD,QAAQ;AACN,aAAO,CAAC;AAAA,IACV;AAEA,UAAM,aAAaF,kBAAiB,gBAAgB;AAGpD,UAAM,WAAW,WAAW,YAAY;AACxC,UAAM,UAAU,kBAAkB,YAAY;AAG9C,UAAM,YAAY,UAAU,MAAM,QAAQ;AAC1C,QAAI,eAAe,oBAAI,IAAoB;AAC3C,QAAI;AACJ,QAAI;AACF,YAAM,aAAaE,cAAa,WAAW,OAAO;AAClD,qBAAe,WAAW,UAAU;AACpC,eAASD,kBAAiB,SAAS;AAAA,IACrC,QAAQ;AAAA,IAER;AAEA,eAAW,OAAO,UAAU;AAC1B,UAAI,KAAK,IAAI,IAAI,UAAU,EAAG;AAC9B,WAAK,IAAI,IAAI,UAAU;AAGvB,UAAI,IAAI,eAAe,QAAS;AAKhC,YAAM,QAAyB,IAAI,WAAW,eAAe;AAC7D,YAAM,SAAS,CAAC,IAAI;AAGpB,YAAM,SAAS,aAAa,IAAI,IAAI,UAAU,KAAK,IAAI;AAIvD,YAAM,OAAO,UAAU,EAAE,MAAM,MAAM,MAAM,IAAI,YAAY,SAAS,OAAO,CAAC;AAE5E,YAAM,WAAuB,CAAC,UAAU;AACxC,UAAI,UAAU,aAAa,IAAI,IAAI,UAAU,EAAG,UAAS,KAAK,MAAM;AAEpE,mBAAa,KAAK;AAAA,QAChB,IAAI,OAAO,UAAU,EAAE,IAAI,IAAI,UAAU;AAAA,QACzC,aAAa,UAAU;AAAA,QACvB;AAAA,QACA,WAAW;AAAA,QACX,MAAM,IAAI;AAAA,QACV,YAAY;AAAA,QACZ;AAAA,QACA;AAAA,QACA,WAAW,IAAI;AAAA,QACf,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;AAAA,QAC3B,QAAQ;AAAA,QACR;AAAA,MACF,CAAC;AAAA,IACH;AAEA,WAAO;AAAA,EACT;AAAA,EAEQ,WAAW,UAA2B;AAC5C,QAAI;AACF,MAAAC,cAAa,UAAU,OAAO;AAC9B,aAAO;AAAA,IACT,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AACF;AAKO,IAAM,YAAY,IAAI,UAAU;;;ACzOvC,SAAS,gBAAAC,eAAc,mBAAmB;AAC1C,SAAS,QAAAC,aAAY;AAgBrB,SAASC,kBAAiB,MAAwB;AAChD,SAAO,EAAE,MAAM,YAAY,QAAQ,MAAM,cAAa,oBAAI,KAAK,GAAE,YAAY,EAAE;AACjF;AAEA,SAASC,kBAAiB,MAAwB;AAChD,SAAO,EAAE,MAAM,YAAY,QAAQ,MAAM,cAAa,oBAAI,KAAK,GAAE,YAAY,EAAE;AACjF;AAmBA,SAAS,YAAY,SAAqC;AACxD,QAAM,OAA2B,CAAC;AAElC,QAAM,QAAQ;AACd,MAAI;AACJ,UAAQ,QAAQ,MAAM,KAAK,OAAO,OAAO,MAAM;AAC7C,UAAM,OAAO,MAAM,CAAC;AACpB,UAAM,UAAU,MAAM,CAAC,KAAK;AAC5B,SAAK,KAAK,EAAE,MAAM,QAAQ,CAAC;AAAA,EAC7B;AACA,SAAO;AACT;AAWA,SAAS,uBAAuB,SAAsC;AACpE,QAAM,WAAW,oBAAI,IAAoB;AACzC,MAAI;AACF,UAAM,OAAO,KAAK,MAAM,OAAO;AAG/B,QAAI,KAAK,WAAW;AAClB,iBAAW,OAAO,OAAO,KAAK,KAAK,SAAS,GAAG;AAE7C,cAAM,WAAW,IAAI,YAAY,GAAG;AACpC,YAAI,YAAY,GAAG;AACjB,gBAAM,OAAO,IAAI,MAAM,GAAG,QAAQ;AAClC,gBAAM,UAAU,IAAI,MAAM,WAAW,CAAC;AACtC,cAAI,QAAQ,SAAS;AACnB,qBAAS,IAAI,MAAM,OAAO;AAAA,UAC5B;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF,QAAQ;AAAA,EAER;AACA,SAAO;AACT;AAIO,IAAM,gBAAN,MAAgD;AAAA,EAC5C,YAAyB;AAAA,EAElC,MAAM,UACJ,WACA,SAC2C;AAC3C,UAAM,eAAwC,CAAC;AAC/C,UAAM,OAAO,cAAc,WAAW,OAAO;AAC7C,UAAM,OAAO,oBAAI,IAAY;AAG7B,QAAI;AACJ,QAAI;AACF,YAAM,QAAQ,YAAY,IAAI;AAC9B,YAAM,SAAS,MAAM,KAAK,CAAC,MAAc,EAAE,SAAS,SAAS,CAAC;AAC9D,UAAI,OAAQ,cAAaC,MAAK,MAAM,MAAM;AAAA,IAC5C,QAAQ;AAAA,IAER;AAEA,QAAI,CAAC,WAAY,QAAO,CAAC;AAEzB,QAAI;AACJ,QAAI;AACF,sBAAgBC,cAAa,YAAY,OAAO;AAAA,IAClD,QAAQ;AACN,aAAO,CAAC;AAAA,IACV;AAEA,UAAM,aAAaH,kBAAiB,UAAU;AAG9C,UAAM,OAAO,YAAY,aAAa;AAGtC,UAAM,aAAaE,MAAK,MAAM,qBAAqB;AACnD,QAAI,eAAe,oBAAI,IAAoB;AAC3C,QAAI;AACJ,QAAI;AACF,YAAM,gBAAgBC,cAAa,YAAY,OAAO;AACtD,qBAAe,uBAAuB,aAAa;AACnD,eAASF,kBAAiB,UAAU;AAAA,IACtC,QAAQ;AAAA,IAER;AAEA,eAAW,OAAO,MAAM;AACtB,UAAI,KAAK,IAAI,IAAI,IAAI,EAAG;AACxB,WAAK,IAAI,IAAI,IAAI;AAEjB,YAAM,SAAS,aAAa,IAAI,IAAI,IAAI,KAAK,IAAI;AAGjD,YAAM,OAAO,SACT,UAAU,EAAE,MAAM,UAAU,MAAM,IAAI,MAAM,SAAS,OAAO,CAAC,IAC7D,UAAU,EAAE,MAAM,UAAU,MAAM,IAAI,KAAK,CAAC;AAEhD,YAAM,WAAuB,CAAC,UAAU;AACxC,UAAI,UAAU,aAAa,IAAI,IAAI,IAAI,EAAG,UAAS,KAAK,MAAM;AAE9D,mBAAa,KAAK;AAAA,QAChB,IAAI,OAAO,UAAU,EAAE,IAAI,IAAI,IAAI;AAAA,QACnC,aAAa,UAAU;AAAA,QACvB;AAAA,QACA,WAAW;AAAA,QACX,MAAM,IAAI;AAAA,QACV,YAAY;AAAA,QACZ,QAAQ;AAAA,QACR,OAAO;AAAA,QACP,GAAI,IAAI,UAAU,EAAE,WAAW,IAAI,QAAQ,IAAI,CAAC;AAAA,QAChD,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;AAAA,QAC3B,QAAQ;AAAA,QACR;AAAA,MACF,CAAC;AAAA,IACH;AAEA,WAAO;AAAA,EACT;AACF;AAKO,IAAM,gBAAgB,IAAI,cAAc;;;AC/K/C,SAAS,gBAAAG,qBAAoB;AAC7B,SAAS,QAAAC,aAAY;AAiBrB,SAASC,kBAAiB,MAAwB;AAChD,SAAO,EAAE,MAAM,YAAY,QAAQ,MAAM,cAAa,oBAAI,KAAK,GAAE,YAAY,EAAE;AACjF;AAEA,SAASC,kBAAiB,MAAwB;AAChD,SAAO,EAAE,MAAM,YAAY,QAAQ,MAAM,cAAa,oBAAI,KAAK,GAAE,YAAY,EAAE;AACjF;AA2BA,SAAS,kBAAkB,SAAsC;AAC/D,QAAM,WAAW,oBAAI,IAAoB;AACzC,MAAI;AACF,UAAM,OAAO,KAAK,MAAM,OAAO;AAC/B,eAAW,OAAO,CAAC,GAAI,KAAK,YAAY,CAAC,GAAI,GAAI,KAAK,cAAc,KAAK,CAAC,CAAE,GAAG;AAC7E,eAAS,IAAI,IAAI,MAAM,IAAI,OAAO;AAAA,IACpC;AAAA,EACF,QAAQ;AAAA,EAER;AACA,SAAO;AACT;AAKA,SAAS,sBAAsB,MAA+C;AAC5E,MAAI,KAAK,WAAW,OAAO,KAAK,KAAK,WAAW,OAAO,EAAG,QAAO;AACjE,MAAI,KAAK,WAAW,MAAM,KAAK,KAAK,WAAW,MAAM,KAAK,KAAK,WAAW,MAAM,EAAG,QAAO;AAC1F,SAAO;AACT;AAKA,SAAS,0BAA0B,MAAuE;AACxG,MAAI,KAAK,WAAW,OAAO,KAAK,KAAK,WAAW,OAAO,EAAG,QAAO;AACjE,MAAI,KAAK,WAAW,MAAM,KAAK,KAAK,WAAW,MAAM,KAAK,KAAK,WAAW,MAAM,EAAG,QAAO;AAC1F,SAAO;AACT;AAIO,IAAM,aAAN,MAA6C;AAAA,EACzC,YAAyB;AAAA,EAElC,MAAM,UACJ,WACA,SAC2C;AAC3C,UAAM,eAAwC,CAAC;AAC/C,UAAM,OAAO,cAAc,WAAW,OAAO;AAC7C,UAAM,OAAO,oBAAI,IAAY;AAG7B,UAAM,mBAAmB,UAAU,UAAU,KAAK,CAAC,MAAM,EAAE,SAAS,eAAe,CAAC,MAC9E,KAAK,WAAWC,MAAK,MAAM,eAAe,CAAC,IAAIA,MAAK,MAAM,eAAe,IAAI;AACnF,QAAI,CAAC,iBAAkB,QAAO,CAAC;AAE/B,QAAI;AACJ,QAAI;AACF,gBAAUC,cAAa,kBAAkB,OAAO;AAAA,IAClD,QAAQ;AACN,aAAO,CAAC;AAAA,IACV;AAEA,UAAM,aAAaH,kBAAiB,gBAAgB;AAGpD,QAAI;AACJ,QAAI;AACF,qBAAe,KAAK,MAAM,OAAO;AAAA,IACnC,QAAQ;AACN,aAAO,CAAC;AAAA,IACV;AAGA,UAAM,WAAWE,MAAK,MAAM,eAAe;AAC3C,QAAI,eAAe,oBAAI,IAAoB;AAC3C,QAAI;AACJ,QAAI;AACF,YAAM,cAAcC,cAAa,UAAU,OAAO;AAClD,qBAAe,kBAAkB,WAAW;AAC5C,eAASF,kBAAiB,QAAQ;AAAA,IACpC,QAAQ;AAAA,IAER;AAGA,UAAM,WAAwF;AAAA,MAC5F,EAAE,MAAM,aAAa,SAAS,OAAO,UAAU;AAAA,MAC/C,EAAE,MAAM,aAAa,aAAa,GAAG,OAAO,cAAc;AAAA,IAC5D;AAEA,eAAW,EAAE,MAAM,MAAM,KAAK,UAAU;AACtC,UAAI,CAAC,KAAM;AACX,iBAAW,CAAC,MAAM,UAAU,KAAK,OAAO,QAAQ,IAAI,GAAG;AACrD,YAAI,KAAK,IAAI,IAAI,EAAG;AACpB,aAAK,IAAI,IAAI;AAEb,cAAM,SAAS,aAAa,IAAI,IAAI;AACpC,cAAM,SAAS,sBAAsB,UAAU;AAC/C,cAAM,aAAa,0BAA0B,UAAU;AACvD,cAAM,aAAa,eAAe;AAElC,cAAM,OAAO,cAAc,SACvB,UAAU,EAAE,MAAM,OAAO,MAAM,SAAS,OAAO,CAAC,IAChD,aACE,UAAU,EAAE,MAAM,OAAO,KAAK,CAAC,IAC/B;AAEN,cAAM,WAAuB,CAAC,UAAU;AACxC,YAAI,UAAU,OAAQ,UAAS,KAAK,MAAM;AAE1C,qBAAa,KAAK;AAAA,UAChB,IAAI,OAAO,UAAU,EAAE,IAAI,IAAI;AAAA,UAC/B,aAAa,UAAU;AAAA,UACvB,GAAI,OAAO,EAAE,KAAK,IAAI,CAAC;AAAA,UACvB,WAAW;AAAA,UACX;AAAA,UACA;AAAA,UACA,QAAQ;AAAA,UACR;AAAA,UACA,WAAW;AAAA,UACX,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;AAAA,UAC3B;AAAA,UACA;AAAA,QACF,CAAC;AAAA,MACH;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA,EAEQ,WAAW,UAA2B;AAC5C,QAAI;AACF,MAAAE,cAAa,UAAU,OAAO;AAC9B,aAAO;AAAA,IACT,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AACF;AAKO,IAAM,aAAa,IAAI,WAAW;;;AC5LzC,SAAS,gBAAAC,qBAAoB;AAC7B,SAAS,QAAAC,aAAY;AAiBrB,SAASC,kBAAiB,MAAwB;AAChD,SAAO,EAAE,MAAM,YAAY,QAAQ,MAAM,cAAa,oBAAI,KAAK,GAAE,YAAY,EAAE;AACjF;AAEA,SAASC,kBAAiB,MAAwB;AAChD,SAAO,EAAE,MAAM,YAAY,QAAQ,MAAM,cAAa,oBAAI,KAAK,GAAE,YAAY,EAAE;AACjF;AAgBA,SAAS,iBAAiB,SAAmD;AAC3E,QAAM,WAAW,oBAAI,IAAiC;AACtD,MAAI;AACJ,MAAI;AAEJ,aAAW,OAAO,QAAQ,MAAM,IAAI,GAAG;AACrC,UAAM,OAAO,IAAI,QAAQ;AACzB,UAAM,UAAU,KAAK,KAAK;AAC1B,QAAI,YAAY,MAAM,QAAQ,WAAW,GAAG,EAAG;AAG/C,UAAM,eAAe,QAAQ,MAAM,kBAAkB;AACrD,QAAI,gBAAgB,KAAK,WAAW,aAAa,CAAC,CAAE,GAAG;AACrD,uBAAiB,aAAa,CAAC;AAC/B,oBAAc;AACd,UAAI,CAAC,SAAS,IAAI,cAAc,GAAG;AACjC,iBAAS,IAAI,gBAAgB,oBAAI,IAAI,CAAC;AAAA,MACxC;AACA;AAAA,IACF;AAEA,QAAI,CAAC,eAAgB;AAIrB,UAAM,WAAW,QAAQ,MAAM,sBAAsB;AACrD,QAAI,YAAY,KAAK,WAAW,IAAI,KAAK,CAAC,KAAK,WAAW,MAAM,GAAG;AACjE,oBAAc,SAAS,CAAC,EAAG,KAAK;AAChC,UAAI,aAAa,SAAS,CAAC,EAAG,KAAK;AAEnC,UAAI,CAAC,cAAc,WAAW,WAAW,GAAG,GAAG;AAC7C,qBAAa;AAAA,MACf;AACA,YAAM,MAAM,SAAS,IAAI,cAAc;AACvC,UAAI,IAAI,aAAa,UAAU;AAAA,IACjC;AAAA,EACF;AAEA,SAAO;AACT;AAYA,SAAS,iBAAiB,SAAsC;AAC9D,QAAM,WAAW,oBAAI,IAAoB;AACzC,QAAM,QAAQ,QAAQ,MAAM,IAAI;AAChC,MAAI;AACJ,MAAI,aAAa;AAEjB,aAAW,OAAO,OAAO;AACvB,UAAM,UAAU,IAAI,KAAK;AACzB,QAAI,YAAY,GAAI;AAEpB,QAAI,YAAY,aAAa;AAC3B,mBAAa;AACb;AAAA,IACF;AAEA,QAAI,CAAC,WAAY;AAGjB,UAAM,WAAW,QAAQ,MAAM,iBAAiB;AAChD,QAAI,YAAY,IAAI,WAAW,IAAI,KAAK,CAAC,IAAI,WAAW,MAAM,GAAG;AAC/D,uBAAiB,SAAS,CAAC,EAAG,KAAK;AACnC;AAAA,IACF;AAGA,QAAI,gBAAgB;AAClB,YAAM,WAAW,QAAQ,MAAM,+BAA+B;AAC9D,UAAI,YAAY,IAAI,WAAW,MAAM,GAAG;AACtC,iBAAS,IAAI,gBAAgB,SAAS,CAAC,CAAE;AACzC,yBAAiB;AAAA,MACnB;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;AAIO,IAAM,cAAN,MAA8C;AAAA,EAC1C,YAAyB;AAAA,EAElC,MAAM,UACJ,WACA,SAC2C;AAC3C,UAAM,eAAwC,CAAC;AAC/C,UAAM,OAAO,cAAc,WAAW,OAAO;AAC7C,UAAM,OAAO,oBAAI,IAAY;AAG7B,UAAM,cAAc,UAAU,UAAU,KAAK,CAAC,MAAM,EAAE,SAAS,cAAc,CAAC,MACxE,KAAK,WAAWC,MAAK,MAAM,cAAc,CAAC,IAAIA,MAAK,MAAM,cAAc,IAAI;AACjF,QAAI,CAAC,YAAa,QAAO,CAAC;AAE1B,QAAI;AACJ,QAAI;AACF,gBAAUC,cAAa,aAAa,OAAO;AAAA,IAC7C,QAAQ;AACN,aAAO,CAAC;AAAA,IACV;AAEA,UAAM,aAAaH,kBAAiB,WAAW;AAG/C,UAAM,WAAW,iBAAiB,OAAO;AAGzC,UAAM,WAAWE,MAAK,MAAM,cAAc;AAC1C,QAAI,eAAe,oBAAI,IAAoB;AAC3C,QAAI;AACJ,QAAI;AACF,YAAM,cAAcC,cAAa,UAAU,OAAO;AAClD,qBAAe,iBAAiB,WAAW;AAC3C,eAASF,kBAAiB,QAAQ;AAAA,IACpC,QAAQ;AAAA,IAER;AAGA,UAAM,iBAAyE;AAAA,MAC7E,EAAE,aAAa,gBAAgB,OAAO,UAAU;AAAA,MAChD,EAAE,aAAa,oBAAoB,OAAO,cAAc;AAAA,MACxD,EAAE,aAAa,wBAAwB,OAAO,UAAU;AAAA,IAC1D;AAEA,eAAW,EAAE,aAAa,MAAM,KAAK,gBAAgB;AACnD,YAAM,OAAO,SAAS,IAAI,WAAW;AACrC,UAAI,CAAC,KAAM;AAEX,iBAAW,CAAC,MAAM,UAAU,KAAK,MAAM;AACrC,YAAI,KAAK,IAAI,IAAI,EAAG;AACpB,aAAK,IAAI,IAAI;AAGb,YAAI,eAAe,OAAO,WAAW,WAAW,GAAG,EAAG;AAEtD,cAAM,SAAS,aAAa,IAAI,IAAI;AAGpC,YAAI,SAA0C;AAC9C,YAAI,aAAsE;AAE1E,YAAI,WAAW,WAAW,OAAO,GAAG;AAClC,mBAAS;AACT,uBAAa;AAAA,QACf,WAAW,WAAW,WAAW,MAAM,GAAG;AACxC,mBAAS;AACT,uBAAa;AAAA,QACf,WAAW,WAAW,WAAW,GAAG,GAAG;AAErC,mBAAS;AACT,uBAAa;AAAA,QACf;AAEA,cAAM,aAAa,eAAe;AAElC,cAAM,OAAO,eAAe,UAAU,cAClC,UAAU,EAAE,MAAM,QAAQ,MAAM,SAAS,UAAU,WAAW,QAAQ,gBAAgB,EAAE,EAAE,CAAC,IAC3F,aACE,UAAU,EAAE,MAAM,QAAQ,KAAK,CAAC,IAChC;AAEN,cAAM,WAAuB,CAAC,UAAU;AACxC,YAAI,UAAU,OAAQ,UAAS,KAAK,MAAM;AAE1C,qBAAa,KAAK;AAAA,UAChB,IAAI,OAAO,UAAU,EAAE,IAAI,IAAI;AAAA,UAC/B,aAAa,UAAU;AAAA,UACvB,GAAI,OAAO,EAAE,KAAK,IAAI,CAAC;AAAA,UACvB,WAAW;AAAA,UACX;AAAA,UACA;AAAA,UACA,QAAQ;AAAA,UACR;AAAA,UACA,GAAI,cAAc,eAAe,MAAM,EAAE,WAAW,WAAW,IAAI,CAAC;AAAA,UACpE,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;AAAA,UAC3B;AAAA,UACA;AAAA,QACF,CAAC;AAAA,MACH;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA,EAEQ,WAAW,UAA2B;AAC5C,QAAI;AACF,MAAAE,cAAa,UAAU,OAAO;AAC9B,aAAO;AAAA,IACT,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AACF;AAKO,IAAM,cAAc,IAAI,YAAY;;;ACzP3C,SAAS,gBAAAC,qBAAoB;AAkB7B,SAASC,kBAAiB,MAAwB;AAChD,SAAO,EAAE,MAAM,YAAY,QAAQ,MAAM,cAAa,oBAAI,KAAK,GAAE,YAAY,EAAE;AACjF;AAMA,SAAS,qBAAqB,KAAgC;AAC5D,QAAM,OAA0B,CAAC;AACjC,QAAM,WAAW;AACjB,MAAI;AACJ,UAAQ,QAAQ,SAAS,KAAK,GAAG,OAAO,MAAM;AAC5C,UAAM,QAAQ,MAAM,CAAC;AACrB,UAAM,UAAU,MAAM,MAAM,6BAA6B,IAAI,CAAC,GAAG,KAAK;AACtE,UAAM,aAAa,MAAM,MAAM,mCAAmC,IAAI,CAAC,GAAG,KAAK;AAC/E,UAAM,UAAU,MAAM,MAAM,6BAA6B,IAAI,CAAC,GAAG,KAAK;AACtE,UAAM,QAAQ,MAAM,MAAM,yBAAyB,IAAI,CAAC,GAAG,KAAK;AAChE,QAAI,WAAW,YAAY;AACzB,WAAK,KAAK,EAAE,SAAS,YAAY,SAAS,MAAM,CAAC;AAAA,IACnD;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,kBAAkB,OAA4C;AACrE,UAAQ,OAAO;AAAA,IACb,KAAK;AAAQ,aAAO;AAAA,IACpB,KAAK;AAAY,aAAO;AAAA,IACxB,KAAK;AAAW,aAAO;AAAA,IACvB,KAAK;AAAW,aAAO;AAAA,IACvB;AAAS,aAAO;AAAA,EAClB;AACF;AAEO,IAAM,eAAN,MAA+C;AAAA,EAC3C,YAAyB;AAAA,EAElC,MAAM,UACJ,WACA,UAC2C;AAC3C,UAAM,eAAwC,CAAC;AAC/C,UAAM,UAAU,UAAU,UAAU,KAAK,CAAC,MAAM,EAAE,SAAS,SAAS,CAAC;AACrE,QAAI,CAAC,QAAS,QAAO,CAAC;AAEtB,QAAI;AACJ,QAAI;AACF,gBAAUC,cAAa,SAAS,OAAO;AAAA,IACzC,QAAQ;AACN,aAAO,CAAC;AAAA,IACV;AAEA,UAAM,aAAaD,kBAAiB,OAAO;AAC3C,UAAM,OAAO,qBAAqB,OAAO;AACzC,UAAM,OAAO,oBAAI,IAAY;AAE7B,eAAW,OAAO,MAAM;AACtB,YAAM,OAAO,GAAG,IAAI,OAAO,IAAI,IAAI,UAAU;AAC7C,UAAI,KAAK,IAAI,IAAI,EAAG;AACpB,WAAK,IAAI,IAAI;AAEb,YAAM,OAAO,IAAI,UACb,UAAU,EAAE,MAAM,SAAS,MAAM,SAAS,IAAI,QAAQ,CAAC,IACvD,UAAU,EAAE,MAAM,SAAS,KAAK,CAAC;AAErC,mBAAa,KAAK;AAAA,QAChB,IAAI,OAAO,UAAU,EAAE,IAAI,IAAI;AAAA,QAC/B,aAAa,UAAU;AAAA,QACvB;AAAA,QACA,WAAW;AAAA,QACX;AAAA,QACA,YAAY;AAAA,QACZ,QAAQ;AAAA,QACR,OAAO,kBAAkB,IAAI,KAAK;AAAA,QAClC,GAAI,IAAI,UAAU,EAAE,WAAW,IAAI,QAAQ,IAAI,CAAC;AAAA,QAChD,QAAQ;AAAA,QACR,UAAU,CAAC,UAAU;AAAA,MACvB,CAAC;AAAA,IACH;AAEA,WAAO;AAAA,EACT;AACF;AAEO,IAAM,eAAe,IAAI,aAAa;;;ACxG7C,SAAS,gBAAAE,qBAAoB;AAW7B,SAASC,kBAAiB,MAAwB;AAChD,SAAO,EAAE,MAAM,YAAY,QAAQ,MAAM,cAAa,oBAAI,KAAK,GAAE,YAAY,EAAE;AACjF;AAEA,SAASC,kBAAiB,MAAwB;AAChD,SAAO,EAAE,MAAM,YAAY,QAAQ,MAAM,cAAa,oBAAI,KAAK,GAAE,YAAY,EAAE;AACjF;AAKA,SAAS,aAAa,SAAwE;AAC5F,QAAM,OAA8D,CAAC;AACrE,QAAM,WAAW;AACjB,MAAI;AACJ,UAAQ,QAAQ,SAAS,KAAK,OAAO,OAAO,MAAM;AAChD,UAAM,OAAO,MAAM,CAAC;AAEpB,QAAI,SAAS,WAAW,SAAS,OAAQ;AACzC,SAAK,KAAK,EAAE,MAAM,SAAS,MAAM,CAAC,EAAE,CAAC;AAAA,EACvC;AACA,SAAO;AACT;AAMA,SAAS,iBAAiB,SAAsC;AAC9D,QAAM,WAAW,oBAAI,IAAoB;AACzC,QAAM,QAAQ,QAAQ,MAAM,IAAI;AAChC,MAAI,UAAU;AACd,aAAW,QAAQ,OAAO;AACxB,QAAI,KAAK,WAAW,KAAK,GAAG;AAAE,gBAAU;AAAM;AAAA,IAAU;AACxD,QAAI,WAAW,SAAS,KAAK,IAAI,KAAK,CAAC,KAAK,WAAW,GAAG,GAAG;AAAE,gBAAU;AAAO;AAAA,IAAU;AAC1F,QAAI,CAAC,QAAS;AACd,UAAM,QAAQ,gCAAgC,KAAK,IAAI;AACvD,QAAI,OAAO;AACT,YAAM,UAAU,MAAM,CAAC,EAAG,MAAM,GAAG,EAAE,CAAC,KAAK,MAAM,CAAC;AAClD,eAAS,IAAI,MAAM,CAAC,GAAI,OAAO;AAAA,IACjC;AAAA,EACF;AACA,SAAO;AACT;AAEO,IAAM,cAAN,MAA8C;AAAA,EAC1C,YAAyB;AAAA,EAElC,MAAM,UACJ,WACA,UAC2C;AAC3C,UAAM,eAAwC,CAAC;AAC/C,UAAM,cAAc,UAAU,UAAU,KAAK,CAAC,MAAM,EAAE,SAAS,SAAS,CAAC;AACzE,QAAI,CAAC,YAAa,QAAO,CAAC;AAE1B,QAAI;AACJ,QAAI;AACF,gBAAUC,cAAa,aAAa,OAAO;AAAA,IAC7C,QAAQ;AACN,aAAO,CAAC;AAAA,IACV;AAEA,UAAM,aAAaF,kBAAiB,WAAW;AAC/C,UAAM,OAAO,aAAa,OAAO;AACjC,UAAM,OAAO,oBAAI,IAAY;AAG7B,UAAM,eAAe,UAAU,UAAU,KAAK,CAAC,MAAM,EAAE,SAAS,cAAc,CAAC;AAC/E,QAAI,eAAe,oBAAI,IAAoB;AAC3C,QAAI;AACJ,QAAI,cAAc;AAChB,UAAI;AACF,cAAM,cAAcE,cAAa,cAAc,OAAO;AACtD,uBAAe,iBAAiB,WAAW;AAC3C,iBAASD,kBAAiB,YAAY;AAAA,MACxC,QAAQ;AAAA,MAER;AAAA,IACF;AAEA,eAAW,OAAO,MAAM;AACtB,UAAI,KAAK,IAAI,IAAI,IAAI,EAAG;AACxB,WAAK,IAAI,IAAI,IAAI;AAEjB,YAAM,SAAS,aAAa,IAAI,IAAI,IAAI;AACxC,YAAM,UAAU,UAAU,IAAI;AAC9B,YAAM,OAAO,UACT,UAAU,EAAE,MAAM,OAAO,MAAM,IAAI,MAAM,QAAQ,CAAC,IAClD,UAAU,EAAE,MAAM,OAAO,MAAM,IAAI,KAAK,CAAC;AAE7C,YAAM,WAAuB,CAAC,UAAU;AACxC,UAAI,UAAU,OAAQ,UAAS,KAAK,MAAM;AAE1C,mBAAa,KAAK;AAAA,QAChB,IAAI,OAAO,UAAU,EAAE,IAAI,IAAI,IAAI;AAAA,QACnC,aAAa,UAAU;AAAA,QACvB;AAAA,QACA,WAAW;AAAA,QACX,MAAM,IAAI;AAAA,QACV,YAAY;AAAA,QACZ,QAAQ;AAAA,QACR,OAAO;AAAA,QACP,GAAI,IAAI,UAAU,EAAE,WAAW,IAAI,QAAQ,IAAI,CAAC;AAAA,QAChD,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;AAAA,QAC3B,QAAQ;AAAA,QACR;AAAA,MACF,CAAC;AAAA,IACH;AAEA,WAAO;AAAA,EACT;AACF;AAEO,IAAM,cAAc,IAAI,YAAY;;;AC7H3C,SAAS,gBAAAE,sBAAoB;AAW7B,SAASC,mBAAiB,MAAwB;AAChD,SAAO,EAAE,MAAM,YAAY,QAAQ,MAAM,cAAa,oBAAI,KAAK,GAAE,YAAY,EAAE;AACjF;AAEA,SAASC,kBAAiB,MAAwB;AAChD,SAAO,EAAE,MAAM,YAAY,QAAQ,MAAM,cAAa,oBAAI,KAAK,GAAE,YAAY,EAAE;AACjF;AAKA,SAAS,gBAAgB,SAAwE;AAC/F,QAAM,OAA8D,CAAC;AAErE,QAAM,WAAW;AACjB,MAAI;AACJ,UAAQ,QAAQ,SAAS,KAAK,OAAO,OAAO,MAAM;AAChD,SAAK,KAAK,EAAE,MAAM,MAAM,CAAC,GAAI,SAAS,MAAM,CAAC,EAAE,CAAC;AAAA,EAClD;AACA,SAAO;AACT;AAMA,SAAS,aAAa,SAAsC;AAC1D,QAAM,WAAW,oBAAI,IAAoB;AACzC,QAAM,YAAY;AAClB,MAAI;AACJ,UAAQ,QAAQ,UAAU,KAAK,OAAO,OAAO,MAAM;AACjD,aAAS,IAAI,MAAM,CAAC,GAAI,MAAM,CAAC,CAAE;AAAA,EACnC;AACA,SAAO;AACT;AAEO,IAAM,gBAAN,MAAgD;AAAA,EAC5C,YAAyB;AAAA,EAElC,MAAM,UACJ,WACA,UAC2C;AAC3C,UAAM,eAAwC,CAAC;AAC/C,UAAM,aAAa,UAAU,UAAU,KAAK,CAAC,MAAM,EAAE,SAAS,SAAS,CAAC;AACxE,QAAI,CAAC,WAAY,QAAO,CAAC;AAEzB,QAAI;AACJ,QAAI;AACF,gBAAUC,eAAa,YAAY,OAAO;AAAA,IAC5C,QAAQ;AACN,aAAO,CAAC;AAAA,IACV;AAEA,UAAM,aAAaF,mBAAiB,UAAU;AAC9C,UAAM,OAAO,gBAAgB,OAAO;AACpC,UAAM,OAAO,oBAAI,IAAY;AAG7B,UAAM,eAAe,UAAU,UAAU,KAAK,CAAC,MAAM,EAAE,SAAS,UAAU,CAAC;AAC3E,QAAI,eAAe,oBAAI,IAAoB;AAC3C,QAAI;AACJ,QAAI,cAAc;AAChB,UAAI;AACF,cAAM,cAAcE,eAAa,cAAc,OAAO;AACtD,uBAAe,aAAa,WAAW;AACvC,iBAASD,kBAAiB,YAAY;AAAA,MACxC,QAAQ;AAAA,MAER;AAAA,IACF;AAEA,eAAW,OAAO,MAAM;AACtB,UAAI,KAAK,IAAI,IAAI,IAAI,EAAG;AACxB,WAAK,IAAI,IAAI,IAAI;AAEjB,YAAM,SAAS,aAAa,IAAI,IAAI,IAAI;AACxC,YAAM,UAAU,UAAU,IAAI;AAC9B,YAAM,OAAO,UACT,UAAU,EAAE,MAAM,OAAO,MAAM,IAAI,MAAM,QAAQ,CAAC,IAClD,UAAU,EAAE,MAAM,OAAO,MAAM,IAAI,KAAK,CAAC;AAE7C,YAAM,WAAuB,CAAC,UAAU;AACxC,UAAI,UAAU,OAAQ,UAAS,KAAK,MAAM;AAE1C,mBAAa,KAAK;AAAA,QAChB,IAAI,OAAO,UAAU,EAAE,IAAI,IAAI,IAAI;AAAA,QACnC,aAAa,UAAU;AAAA,QACvB;AAAA,QACA,WAAW;AAAA,QACX,MAAM,IAAI;AAAA,QACV,YAAY;AAAA,QACZ,QAAQ;AAAA,QACR,OAAO;AAAA,QACP,GAAI,IAAI,UAAU,EAAE,WAAW,IAAI,QAAQ,IAAI,CAAC;AAAA,QAChD,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;AAAA,QAC3B,QAAQ;AAAA,QACR;AAAA,MACF,CAAC;AAAA,IACH;AAEA,WAAO;AAAA,EACT;AACF;AAEO,IAAM,gBAAgB,IAAI,cAAc;;;ACpH/C,SAAS,gBAAAE,sBAAoB;AAU7B,SAASC,mBAAiB,MAAwB;AAChD,SAAO,EAAE,MAAM,YAAY,QAAQ,MAAM,cAAa,oBAAI,KAAK,GAAE,YAAY,EAAE;AACjF;AAKA,SAAS,cAAc,SAAwE;AAC7F,QAAM,OAA8D,CAAC;AACrE,QAAM,gBAAgB;AACtB,QAAM,QAAQ,cAAc,KAAK,OAAO,IAAI,CAAC;AAC7C,MAAI,CAAC,MAAO,QAAO;AAEnB,aAAW,QAAQ,MAAM,MAAM,IAAI,GAAG;AACpC,UAAM,UAAU,KAAK,KAAK;AAC1B,QAAI,CAAC,WAAW,QAAQ,WAAW,GAAG,EAAG;AACzC,UAAM,QAAQ,QAAQ,MAAM,GAAG;AAC/B,QAAI,MAAM,UAAU,GAAG;AACrB,WAAK,KAAK,EAAE,MAAM,MAAM,CAAC,GAAI,SAAS,MAAM,CAAC,EAAE,CAAC;AAAA,IAClD,OAAO;AACL,WAAK,KAAK,EAAE,MAAM,QAAQ,CAAC;AAAA,IAC7B;AAAA,EACF;AACA,SAAO;AACT;AAKA,SAAS,eAAe,SAAwE;AAC9F,QAAM,OAA8D,CAAC;AACrE,MAAI;AACF,UAAM,OAAO,KAAK,MAAM,OAAO;AAC/B,eAAW,OAAO,KAAK,gBAAgB,CAAC,GAAG;AACzC,UAAI,OAAO,QAAQ,UAAU;AAC3B,aAAK,KAAK,EAAE,MAAM,IAAI,CAAC;AAAA,MACzB,OAAO;AACL,aAAK,KAAK,EAAE,MAAM,IAAI,MAAM,SAAS,IAAI,QAAQ,CAAC;AAAA,MACpD;AAAA,IACF;AAAA,EACF,QAAQ;AAAA,EAER;AACA,SAAO;AACT;AAEO,IAAM,aAAN,MAA6C;AAAA,EACzC,YAAyB;AAAA,EAElC,MAAM,UACJ,WACA,UAC2C;AAC3C,UAAM,eAAwC,CAAC;AAC/C,UAAM,OAAO,oBAAI,IAAY;AAE7B,eAAW,gBAAgB,UAAU,WAAW;AAC9C,UAAI;AACJ,UAAI;AACF,kBAAUC,eAAa,cAAc,OAAO;AAAA,MAC9C,QAAQ;AACN;AAAA,MACF;AAEA,YAAM,aAAaD,mBAAiB,YAAY;AAChD,UAAI,OAA8D,CAAC;AAEnE,UAAI,aAAa,SAAS,WAAW,GAAG;AACtC,eAAO,cAAc,OAAO;AAAA,MAC9B,WAAW,aAAa,SAAS,YAAY,GAAG;AAC9C,eAAO,eAAe,OAAO;AAAA,MAC/B,OAAO;AACL;AAAA,MACF;AAEA,iBAAW,OAAO,MAAM;AACtB,YAAI,KAAK,IAAI,IAAI,IAAI,EAAG;AACxB,aAAK,IAAI,IAAI,IAAI;AAEjB,cAAM,OAAO,IAAI,UACb,UAAU,EAAE,MAAM,SAAS,MAAM,IAAI,MAAM,SAAS,IAAI,QAAQ,CAAC,IACjE,UAAU,EAAE,MAAM,SAAS,MAAM,IAAI,KAAK,CAAC;AAE/C,qBAAa,KAAK;AAAA,UAChB,IAAI,OAAO,UAAU,EAAE,IAAI,IAAI,IAAI;AAAA,UACnC,aAAa,UAAU;AAAA,UACvB;AAAA,UACA,WAAW;AAAA,UACX,MAAM,IAAI;AAAA,UACV,YAAY;AAAA,UACZ,QAAQ;AAAA,UACR,OAAO;AAAA,UACP,GAAI,IAAI,UAAU,EAAE,WAAW,IAAI,QAAQ,IAAI,CAAC;AAAA;AAAA,UAEhD,QAAQ;AAAA,UACR,UAAU,CAAC,UAAU;AAAA,QACvB,CAAC;AAAA,MACH;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AACF;AAEO,IAAM,aAAa,IAAI,WAAW;;;AC/FlC,SAAS,cAAc,aAAuB,aAAqC;AACxF,QAAM,WAAW,oBAAI,IAAmC;AACxD,aAAW,OAAO,YAAY,cAAc;AAC1C,aAAS,IAAI,GAAG,IAAI,SAAS,IAAI,IAAI,IAAI,IAAI,GAAG;AAAA,EAClD;AAEA,QAAM,WAAW,oBAAI,IAAmC;AACxD,aAAW,OAAO,YAAY,cAAc;AAC1C,aAAS,IAAI,GAAG,IAAI,SAAS,IAAI,IAAI,IAAI,IAAI,GAAG;AAAA,EAClD;AAEA,QAAM,QAAiC,CAAC;AACxC,QAAM,UAAmC,CAAC;AAC1C,QAAM,UAAmC,CAAC;AAG1C,aAAW,CAAC,KAAK,MAAM,KAAK,UAAU;AACpC,UAAM,SAAS,SAAS,IAAI,GAAG;AAC/B,QAAI,CAAC,QAAQ;AACX,YAAM,KAAK,MAAM;AACjB;AAAA,IACF;AAGA,UAAM,SAA6C,CAAC,UAAU,aAAa,UAAU,cAAc;AACnG,eAAW,SAAS,QAAQ;AAC1B,YAAM,SAAS,OAAO,OAAO,KAAK,KAAK,EAAE;AACzC,YAAM,SAAS,OAAO,OAAO,KAAK,KAAK,EAAE;AACzC,UAAI,WAAW,QAAQ;AACrB,gBAAQ,KAAK;AAAA,UACX,MAAM,OAAO;AAAA,UACb,WAAW,OAAO;AAAA,UAClB,OAAO,OAAO,KAAK;AAAA,UACnB,MAAM;AAAA,UACN,IAAI;AAAA,QACN,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAGA,aAAW,CAAC,KAAK,MAAM,KAAK,UAAU;AACpC,QAAI,CAAC,SAAS,IAAI,GAAG,GAAG;AACtB,cAAQ,KAAK,MAAM;AAAA,IACrB;AAAA,EACF;AAEA,SAAO,EAAE,OAAO,SAAS,QAAQ;AACnC;;;AC7CO,SAAS,OAAO,UAAkC;AACvD,QAAM,UAAU,SAAS;AACzB,SAAO;AAAA,IACL,aAAa;AAAA,IACb,aAAa;AAAA,IACb,QAAQ;AAAA,IACR,MAAM,kBAAkB,SAAS,SAAS;AAAA,IAC1C,mBAAmB,+BAA+B,SAAS,EAAE;AAAA,IAC7D,cAAc;AAAA,MACZ;AAAA,MACA,UAAU,CAAC,mCAAmC;AAAA,IAChD;AAAA,IACA,UAAU,SAAS,aAAa,IAAI,CAAC,KAAK,WAAW;AAAA,MACnD,MAAM,IAAI;AAAA,MACV,QAAQ,mBAAmB,KAAK;AAAA,MAChC,aAAa,IAAI,UAAU,IAAI;AAAA,MAC/B,kBAAkB,IAAI,OAAO,mBAAmB,IAAI,IAAI,KAAK;AAAA,MAC7D,kBAAkB,IAAI,WAAW;AAAA,IACnC,EAAE;AAAA,EACJ;AACF;AAqBO,SAAS,YAAY,UAAkC;AAC5D,SAAO;AAAA,IACL,WAAW;AAAA,IACX,aAAa;AAAA,IACb,SAAS;AAAA,IACT,UAAU;AAAA,MACR,WAAW,SAAS;AAAA,MACpB,OAAO,CAAC,EAAE,MAAM,+BAA+B,SAAS,SAAS,eAAe,CAAC;AAAA,IACnF;AAAA,IACA,YAAY,SAAS,aAAa,IAAI,CAAC,SAAS;AAAA,MAC9C,MAAM;AAAA,MACN,MAAM,IAAI;AAAA,MACV,SAAS,IAAI,UAAU,IAAI;AAAA,MAC3B,GAAI,IAAI,OAAO,EAAE,MAAM,IAAI,KAAK,IAAI,CAAC;AAAA,MACrC,GAAI,IAAI,UACJ,EAAE,UAAU,CAAC,EAAE,SAAS,EAAE,IAAI,IAAI,QAAQ,EAAE,CAAC,EAAE,IAC/C,CAAC;AAAA,IACP,EAAE;AAAA,EACJ;AACF;;;AC5CA,SAAS,eACP,WACA,MACA,QACA,eACoB;AACpB,QAAM,MAAM,gBAAgB,IAAI,aAAa,KAAK;AAClD,UAAQ,WAAW;AAAA,IACjB,KAAK;AACH,UAAI,WAAW,SAAU,QAAO,iBAAiB,IAAI;AACrD,aAAO,eAAe,IAAI,GAAG,GAAG;AAAA,IAClC,KAAK;AACH,UAAI,WAAW,SAAU,QAAO,iBAAiB,IAAI;AACrD,aAAO,eAAe,IAAI,GAAG,GAAG;AAAA,IAClC,KAAK;AACH,UAAI,WAAW,SAAU,QAAO,gBAAgB,IAAI;AACpD,aAAO,aAAa,IAAI,IAAI,iBAAiB,QAAQ;AAAA,IACvD,KAAK;AACH,UAAI,WAAW,SAAU,QAAO,UAAU,IAAI;AAC9C,aAAO,UAAU,IAAI,IAAI,iBAAiB,QAAQ;AAAA,IACpD,KAAK;AACH,UAAI,WAAW,SAAU,QAAO,mBAAmB,IAAI;AACvD,aAAO,oBAAoB,IAAI,IAAI,iBAAiB,QAAQ;AAAA,IAC9D,KAAK;AACH,UAAI,WAAW,SAAU,QAAO,yBAAyB,IAAI;AAC7D,aAAO,sBAAsB,IAAI;AAAA,IACnC;AACE,aAAO;AAAA,EACX;AACF;AAYO,SAAS,oBAAoB,UAAiC;AACnE,QAAM,WAAW,SAAS;AAC1B,QAAM,QAA2B,CAAC;AAElC,aAAW,WAAW,UAAU;AAC9B,QAAI,QAAQ,WAAW,OAAQ;AAE/B,UAAM,MAAM,SAAS,aAAa;AAAA,MAChC,CAAC,MAA6B,EAAE,OAAO,QAAQ;AAAA,IACjD;AACA,QAAI,CAAC,IAAK;AAEV,UAAM,gBAAgB,IAAI,gBAAgB,IAAI,cAAc,IAAI;AAEhE,UAAM,KAAK;AAAA,MACT,gBAAgB,IAAI;AAAA,MACpB,WAAW,IAAI;AAAA,MACf,aAAa,IAAI;AAAA,MACjB,gBAAgB,IAAI,UAAU,IAAI,aAAa,IAAI;AAAA,MACnD;AAAA,MACA,QAAQ,QAAQ;AAAA,MAChB,UAAU,QAAQ;AAAA,MAClB,WAAW,QAAQ;AAAA,MACnB,cAAc,QAAQ;AAAA,MACtB,kBAAkB,eAAe,IAAI,WAAW,IAAI,MAAM,QAAQ,QAAQ,aAAa;AAAA,IACzF,CAAC;AAAA,EACH;AAGA,QAAM,gBAAgB,oBAAI,IAAI;AAAA,IAC5B,CAAC,YAAY,CAAC;AAAA,IACd,CAAC,QAAQ,CAAC;AAAA,IACV,CAAC,UAAU,CAAC;AAAA,IACZ,CAAC,OAAO,CAAC;AAAA,IACT,CAAC,QAAQ,CAAC;AAAA,EACZ,CAAC;AACD,QAAM,KAAK,CAAC,GAAG,MAAM;AACnB,UAAM,KAAK,cAAc,IAAI,EAAE,QAAQ,KAAK;AAC5C,UAAM,KAAK,cAAc,IAAI,EAAE,QAAQ,KAAK;AAC5C,WAAO,KAAK;AAAA,EACd,CAAC;AAED,QAAM,UAAU;AAAA,IACd,OAAO,MAAM;AAAA,IACb,OAAO,MAAM,OAAO,CAAC,MAAM,EAAE,WAAW,eAAe,EAAE;AAAA,IACzD,OAAO,MAAM,OAAO,CAAC,MAAM,EAAE,WAAW,eAAe,EAAE;AAAA,IACzD,OAAO,MAAM,OAAO,CAAC,MAAM,EAAE,WAAW,eAAe,EAAE;AAAA,IACzD,SAAS,MAAM,OAAO,CAAC,MAAM,EAAE,WAAW,SAAS,EAAE;AAAA,IACrD,QAAQ,MAAM,OAAO,CAAC,MAAM,EAAE,WAAW,QAAQ,EAAE;AAAA,IACnD,aAAa,MAAM,OAAO,CAAC,MAAM,EAAE,WAAW,aAAa,EAAE;AAAA,EAC/D;AAEA,SAAO;AAAA,IACL,YAAY,SAAS;AAAA,IACrB,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,IACpC;AAAA,IACA;AAAA,IACA,SACE;AAAA,EACJ;AACF;AAKO,SAAS,mBAAmB,MAA2B;AAC5D,QAAM,QAAkB;AAAA,IACtB;AAAA,IACA;AAAA,IACA,kBAAkB,KAAK,WAAW;AAAA,IAClC,iBAAiB,KAAK,UAAU;AAAA,IAChC,oBAAoB,KAAK,QAAQ,KAAK;AAAA,IACtC;AAAA,IACA,kBAAQ,KAAK,OAAO;AAAA,IACpB;AAAA,EACF;AAEA,MAAI,KAAK,MAAM,WAAW,GAAG;AAC3B,UAAM,KAAK,sEAAiE;AAC5E,WAAO,MAAM,KAAK,IAAI;AAAA,EACxB;AAGA,QAAM,KAAK,cAAc,EAAE;AAC3B,QAAM,KAAK,oBAAoB;AAC/B,QAAM,KAAK,WAAW;AACtB,QAAM,KAAK,qBAAqB,KAAK,QAAQ,KAAK,IAAI;AACtD,QAAM,KAAK,qBAAqB,KAAK,QAAQ,KAAK,IAAI;AACtD,QAAM,KAAK,qBAAqB,KAAK,QAAQ,KAAK,IAAI;AACtD,QAAM,KAAK,eAAe,KAAK,QAAQ,OAAO,IAAI;AAClD,QAAM,KAAK,cAAc,KAAK,QAAQ,MAAM,IAAI;AAChD,QAAM,KAAK,mBAAmB,KAAK,QAAQ,WAAW,IAAI;AAC1D,QAAM,KAAK,EAAE;AAGb,QAAM,KAAK,YAAY,EAAE;AACzB,aAAW,QAAQ,KAAK,OAAO;AAC7B,UAAM,OACJ,KAAK,aAAa,aAAa,cAC/B,KAAK,aAAa,SAAS,cAC3B,KAAK,aAAa,WAAW,cAC7B,KAAK,aAAa,QAAQ,cAAO;AAEnC,UAAM,KAAK,OAAO,IAAI,IAAI,KAAK,cAAc,KAAK,KAAK,SAAS,KAAK,EAAE;AACvE,UAAM,KAAK,iBAAiB,KAAK,MAAM,EAAE;AACzC,UAAM,KAAK,kBAAkB,KAAK,kBAAkB,SAAS,EAAE;AAC/D,UAAM,KAAK,iBAAiB,KAAK,iBAAiB,QAAQ,EAAE;AAC5D,UAAM,KAAK,mBAAmB,KAAK,QAAQ,EAAE;AAC7C,UAAM,KAAK,oBAAoB,KAAK,SAAS,EAAE;AAC/C,QAAI,KAAK,aAAc,OAAM,KAAK,wBAAwB,KAAK,YAAY,EAAE;AAC7E,QAAI,KAAK,kBAAkB;AACzB,YAAM,KAAK,8BAA8B,KAAK,gBAAgB,IAAI;AAAA,IACpE;AACA,UAAM,KAAK,EAAE;AAAA,EACf;AAEA,SAAO,MAAM,KAAK,IAAI;AACxB;;;ACnMA,SAAS,OAAO,gBAAqC;AAErD,SAAS,OAAO,eAAe;AA0B/B,IAAM,iBAAiB,KAAK,KAAK;AACjC,IAAM,2BAA2B;AACjC,IAAM,cAAc;AACpB,IAAM,kBAAkB;AAIxB,IAAM,gBAAgB,oBAAI,IAAwB;AAClD,IAAM,kBAAkB,oBAAI,IAA6B;AAEzD,SAAS,YAAY,MAAc,MAAsB;AACvD,SAAO,GAAG,IAAI,GAAG,IAAI;AACvB;AAEA,SAAS,UAAU,KAAwC;AACzD,QAAM,QAAQ,cAAc,IAAI,GAAG;AACnC,MAAI,CAAC,MAAO,QAAO;AACnB,MAAI,KAAK,IAAI,IAAI,MAAM,WAAW;AAChC,kBAAc,OAAO,GAAG;AACxB,WAAO;AAAA,EACT;AACA,SAAO,MAAM;AACf;AAEA,SAAS,SACP,KACA,MACA,MACA,QAAQ,gBACF;AACN,gBAAc,IAAI,KAAK;AAAA,IACrB;AAAA,IACA;AAAA,IACA,WAAW,KAAK,IAAI,IAAI;AAAA,EAC1B,CAAC;AACH;AAIA,SAAS,gBAAgB,MAA6B;AACpD,MAAI,cAAc,gBAAgB,IAAI,IAAI;AAC1C,MAAI,CAAC,aAAa;AAChB,kBAAc,EAAE,QAAQ,GAAG,OAAO,CAAC,EAAE;AACrC,oBAAgB,IAAI,MAAM,WAAW;AAAA,EACvC;AAEA,MAAI,YAAY,SAAS,0BAA0B;AACjD,gBAAY;AACZ,WAAO,QAAQ,QAAQ;AAAA,EACzB;AAEA,SAAO,IAAI,QAAc,CAACE,aAAY;AACpC,gBAAa,MAAM,KAAKA,QAAO;AAAA,EACjC,CAAC;AACH;AAEA,SAAS,gBAAgB,MAAoB;AAC3C,QAAM,cAAc,gBAAgB,IAAI,IAAI;AAC5C,MAAI,CAAC,YAAa;AAElB,cAAY;AAEZ,MAAI,YAAY,MAAM,SAAS,GAAG;AAChC,UAAM,OAAO,YAAY,MAAM,MAAM;AACrC,QAAI,MAAM;AACR,kBAAY;AACZ,WAAK;AAAA,IACP;AAAA,EACF;AACF;AAIA,SAAS,MAAM,IAA2B;AACxC,SAAO,IAAI,QAAQ,CAACA,aAAY,WAAWA,UAAS,EAAE,CAAC;AACzD;AAEA,SAAS,eAAe,SAAiB,YAA4B;AACnE,QAAM,OAAO,eAAe,MAAM,kBAAkB,IAAI;AACxD,SAAO,OAAO,KAAK,IAAI,GAAG,OAAO,IAAI,KAAK,OAAO,IAAI;AACvD;AAWA,SAAS,WACP,UACA,MACA,MACA,QACwB;AACxB,SAAO,IAAI,QAAQ,CAACA,UAAS,WAAW;AACtC,UAAM,UAA0B;AAAA,MAC9B;AAAA,MACA;AAAA,MACA,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,QAAQ;AAAA,QACR,cAAc;AAAA,QACd,GAAI,OAAO,EAAE,iBAAiB,KAAK,IAAI,CAAC;AAAA,MAC1C;AAAA,MACA;AAAA,MACA,SAAS;AAAA,IACX;AAEA,UAAM,MAAM,aAAa,eAAe,aAAa,cAAc,UAAU;AAE7E,UAAM,MAAM,IAAI,SAAS,CAAC,QAAyB;AACjD,YAAM,aAAa,IAAI,cAAc;AACrC,YAAM,kBAAkB,IAAI;AAE5B,UAAI,OAAO;AACX,UAAI,GAAG,QAAQ,CAAC,UAAkB;AAChC,gBAAQ;AAAA,MACV,CAAC;AACD,UAAI,GAAG,OAAO,MAAM;AAClB,QAAAA,SAAQ;AAAA,UACN;AAAA,UACA,SAAS;AAAA,UACT;AAAA,UACA,aAAa;AAAA,QACf,CAAC;AAAA,MACH,CAAC;AAAA,IACH,CAAC;AAED,QAAI,GAAG,SAAS,CAAC,QAAe;AAC9B,aAAO,GAAG;AAAA,IACZ,CAAC;AAED,QAAI,GAAG,WAAW,MAAM;AACtB,UAAI,QAAQ;AACZ,aAAO,IAAI,MAAM,uBAAuB,QAAQ,GAAG,IAAI,EAAE,CAAC;AAAA,IAC5D,CAAC;AAED,QAAI,IAAI;AAAA,EACV,CAAC;AACH;AAkBO,SAAS,kBAAkB,MAA+B,MAA6B;AAC5F,QAAM,gBAAiB,KAAK,WAAW,IAA2C,QAAQ;AAU1F,MAAI;AACJ,MAAI,iBAAiB,KAAK,YAAY,OAAO,KAAK,aAAa,UAAU;AACvE,UAAM,WAAW,KAAK;AACtB,iBAAa,SAAS,aAAa,GAAG,aAAa,OAAO;AAAA,EAC5D;AAGA,SAAO;AAAA,IACL,cAAc;AAAA,IACd,SAAU,KAAK,WAAsB;AAAA,IACrC,YAAY,cAAc;AAAA,IAC1B,QAAQ;AAAA,IACR,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,IACpC,QAAQ,8BAA8B,IAAI;AAAA,EAC5C;AACF;AAIA,IAAM,qBAAiE;AAAA,EACrE,KAAK;AAAA,IACH,MAAM;AAAA,IACN,MAAM,CAAC,SAAiB;AAEtB,YAAM,UAAU,KAAK,WAAW,GAAG,IAAI,KAAK,QAAQ,KAAK,KAAK,IAAI;AAClE,aAAO,IAAI,OAAO;AAAA,IACpB;AAAA,IACA,QAAQ;AAAA,EACV;AAAA,EAEA,QAAQ;AAAA,IACN,MAAM;AAAA,IACN,MAAM,CAAC,SAAiB,SAAS,IAAI;AAAA,IACrC,QAAQ,CAAC,SAAiD;AACxD,YAAM,OAAO,KAAK;AAClB,aAAO;AAAA,QACL,cAAe,MAAM,WAAsB;AAAA,QAC3C,SAAU,MAAM,WAAsB;AAAA,QACtC,YAAa,MAAM,cAA0B;AAAA,QAC7C,QAAQ;AAAA,QACR,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,QACpC,QAAQ,yBAAyB,MAAM,QAAQ,EAAE;AAAA,MACnD;AAAA,IACF;AAAA,EACF;AAAA,EAEA,OAAO;AAAA,IACL,MAAM;AAAA,IACN,MAAM,CAAC,SAAiB,kBAAkB,IAAI;AAAA,IAC9C,QAAQ,CAAC,SAAiD;AACxD,YAAM,QAAQ,KAAK;AACnB,aAAO;AAAA,QACL,cAAe,OAAO,sBAAkC,OAAO,eAA0B;AAAA,QACzF,SAAU,OAAO,WAAsB;AAAA,QACvC,YAAY;AAAA;AAAA,QACZ,QAAQ;AAAA,QACR,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,QACpC,QAAQ,mCAAmC,OAAO,QAAQ,EAAE;AAAA,MAC9D;AAAA,IACF;AAAA,EACF;AAAA,EAEA,QAAQ;AAAA,IACN,MAAM;AAAA,IACN,MAAM,CAAC,WAAmB,IAAI,MAAM;AAAA,IACpC,QAAQ,CAAC,MAA+B,WAAkC;AACxE,aAAO;AAAA,QACL,cAAe,KAAK,WAAsB;AAAA,QAC1C,SAAS;AAAA;AAAA,QACT,YAAY;AAAA,QACZ,QAAQ;AAAA,QACR,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,QACpC,QAAQ,4BAA4B,MAAM;AAAA,MAC5C;AAAA,IACF;AAAA,EACF;AAAA,EAEA,OAAO;AAAA,IACL,MAAM;AAAA,IACN,MAAM,CAAC,SAAiB;AACtB,YAAM,QAAQ,KAAK,YAAY;AAC/B,aAAO,6BAA6B,KAAK;AAAA,IAC3C;AAAA,IACA,QAAQ,CAAC,MAA+B,SAAgC;AAEtE,YAAM,QAAQ,KAAK;AACnB,UAAI;AAEJ,UAAI,SAAS,MAAM,SAAS,GAAG;AAE7B,mBAAW,QAAQ,OAAO;AACxB,gBAAM,YAAY,KAAK;AACvB,cAAI,aAAa,MAAM,QAAQ,SAAS,GAAG;AACzC,uBAAW,SAAS,WAAW;AAC7B,oBAAM,eAAe,MAAM;AAC3B,kBAAI,cAAc,SAAS;AACzB,sBAAM,MAAM,aAAa;AAEzB,oBAAI,CAAC,gBAAiB,CAAC,IAAI,SAAS,GAAG,KAAK,aAAa,SAAS,GAAG,GAAI;AACvE,iCAAe;AAAA,gBACjB,WAAW,CAAC,IAAI,SAAS,GAAG,KAAK,CAAC,aAAa,SAAS,GAAG,GAAG;AAE5D,sBAAI,MAAM,aAAc,gBAAe;AAAA,gBACzC;AAAA,cACF;AAAA,YACF;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAEA,aAAO;AAAA,QACL;AAAA,QACA,SAAS;AAAA;AAAA,QACT,YAAY;AAAA,QACZ,QAAQ;AAAA,QACR,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,QACpC,QAAQ,kDAAkD,KAAK,YAAY,CAAC;AAAA,MAC9E;AAAA,IACF;AAAA,EACF;AAAA,EAEA,UAAU;AAAA,IACR,MAAM;AAAA,IACN,MAAM,CAAC,SAAiB,OAAO,IAAI;AAAA,IACnC,QAAQ,CAAC,MAA+B,SAAgC;AACtE,YAAM,WAAW,KAAK;AACtB,YAAM,WAAW,WAAW,IAAI;AAChC,UAAI,CAAC,YAAY,SAAS,WAAW,GAAG;AACtC,eAAO,EAAE,cAAa,oBAAI,KAAK,GAAE,YAAY,GAAG,QAAQ,YAAY;AAAA,MACtE;AAGA,UAAI;AACJ,iBAAW,OAAO,UAAU;AAC1B,cAAM,UAAU,IAAI;AACpB,YAAI,WAAW,CAAC,QAAQ,SAAS,KAAK,KAAK,CAAC,QAAQ,SAAS,OAAO,KAAK,CAAC,QAAQ,SAAS,MAAM,KAAK,CAAC,QAAQ,SAAS,IAAI,KAAK,CAAC,QAAQ,SAAS,IAAI,GAAG;AACxJ,cAAI,CAAC,gBAAgB,UAAU,cAAc;AAC3C,2BAAe;AAAA,UACjB;AAAA,QACF;AAAA,MACF;AAGA,YAAM,SAAS,SAAS,CAAC;AAEzB,aAAO;AAAA,QACL;AAAA,QACA,SAAS,OAAO,WAAqB;AAAA,QACrC,YAAa,OAAO,cAA0B;AAAA,QAC9C,QAAS,OAAO,aAAyB;AAAA,QACzC,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,QACpC,QAAQ,iCAAiC,IAAI;AAAA,MAC/C;AAAA,IACF;AAAA,EACF;AAAA,EAEA,KAAK;AAAA,IACH,MAAM;AAAA,IACN,MAAM,CAAC,SAAiB,iBAAiB,IAAI;AAAA,IAC7C,QAAQ,CAAC,SAAiD;AACxD,YAAM,SAAS,KAAK;AACpB,aAAO;AAAA,QACL,cAAe,KAAK,iBAA6B,QAAQ,WAAuB,KAAK,WAAsB;AAAA,QAC3G,SAAU,QAAQ,WAAsB;AAAA,QACxC,YAAa,KAAK,kBAA8B;AAAA,QAChD,QAAS,KAAK,eAA2B;AAAA,QACzC,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,QACpC,QAAQ,gCAAiC,KAAK,QAAmB,EAAE;AAAA,MACrE;AAAA,IACF;AAAA,EACF;AACF;AAiBA,eAAsB,eACpB,WACA,MACA,UAAiC,CAAC,GACE;AACpC,QAAM,UAAU,mBAAmB,SAAS;AAC5C,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI,MAAM,8CAA8C,SAAS,EAAE;AAAA,EAC3E;AAEA,QAAM,OAAO,QAAQ,KAAK,IAAI;AAC9B,QAAM,WAAW,YAAY,QAAQ,MAAM,IAAI;AAG/C,MAAI,CAAC,QAAQ,OAAO;AAClB,UAAM,SAAS,UAAU,QAAQ;AACjC,QAAI,OAAQ,QAAO;AAAA,EACrB;AAGA,QAAM,gBAAgB,QAAQ,IAAI;AAClC,MAAI;AAEF,UAAM,gBAAgB,cAAc,IAAI,QAAQ;AAChD,UAAM,OAAO,eAAe;AAE5B,QAAI;AACJ,aAAS,UAAU,GAAG,UAAU,aAAa,WAAW;AACtD,UAAI;AACF,cAAM,WAAW,MAAM,WAAW,QAAQ,MAAM,MAAM,MAAM,QAAQ,MAAM;AAG1E,YAAI,SAAS,eAAe,OAAO,eAAe;AAChD,mBAAS,UAAU,cAAc,MAAM,cAAc,MAAM,cAAc;AACzE,iBAAO,cAAc;AAAA,QACvB;AAGA,YAAI,SAAS,eAAe,OAAO,SAAS,eAAe,OAAO,SAAS,eAAe,KAAK;AAC7F,iBAAO;AAAA,QACT;AAGA,YAAI,SAAS,eAAe,OAAO,SAAS,cAAc,KAAK;AAC7D,cAAI,UAAU,cAAc,GAAG;AAC7B,kBAAM,UAAU,eAAe,SAAS,SAAS,UAAU;AAC3D,kBAAM,MAAM,OAAO;AACnB;AAAA,UACF;AACA,gBAAM,IAAI,MAAM,YAAY,QAAQ,IAAI,aAAa,SAAS,UAAU,UAAU,WAAW,WAAW;AAAA,QAC1G;AAGA,YAAI;AACJ,YAAI;AACF,iBAAO,KAAK,MAAM,SAAS,IAAI;AAAA,QACjC,QAAQ;AACN,gBAAM,IAAI,MAAM,8BAA8B,QAAQ,IAAI,GAAG,IAAI,EAAE;AAAA,QACrE;AAEA,cAAM,SAAS,QAAQ,OAAO,MAAM,MAAM,SAAS;AACnD,YAAI,CAAC,QAAQ;AAEX,iBAAO;AAAA,QACT;AAGA,cAAM,eAAe,SAAS,QAAQ,MAAM;AAC5C,iBAAS,UAAU,QAAQ,cAAc,cAAc;AAEvD,eAAO;AAAA,MACT,SAAS,KAAK;AACZ,oBAAY,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC;AAC9D,YAAI,UAAU,cAAc,GAAG;AAC7B,gBAAM,cAAc,eAAe,SAAS,IAAI,QAAQ,SAAS,KAAK;AACtE,gBAAM,UAAU,eAAe,SAAS,cAAc,MAAM,GAAG;AAC/D,gBAAM,MAAM,OAAO;AAAA,QACrB;AAAA,MACF;AAAA,IACF;AAEA,UAAM,aAAa,IAAI,MAAM,qBAAqB,SAAS,IAAI,IAAI,EAAE;AAAA,EACvE,UAAE;AACA,oBAAgB,QAAQ,IAAI;AAAA,EAC9B;AACF;AAMA,eAAsB,oBACpB,WACA,OACA,UAAiC,CAAC,GACe;AACjD,QAAM,UAAU,oBAAI,IAAuC;AAG3D,QAAM,UAAU,MAAM,QAAQ;AAAA,IAC5B,MAAM,IAAI,OAAO,SAAS;AACxB,UAAI;AACF,cAAM,QAAQ,MAAM,eAAe,WAAW,MAAM,OAAO;AAC3D,eAAO,EAAE,MAAM,MAAM;AAAA,MACvB,QAAQ;AACN,eAAO,EAAE,MAAM,OAAO,OAAU;AAAA,MAClC;AAAA,IACF,CAAC;AAAA,EACH;AAEA,aAAW,EAAE,MAAM,MAAM,KAAK,SAAS;AACrC,YAAQ,IAAI,MAAM,KAAK;AAAA,EACzB;AAEA,SAAO;AACT;AAKO,SAAS,8BAAwC;AACtD,SAAO,OAAO,KAAK,kBAAkB;AACvC;AAMO,SAAS,qBAA2B;AACzC,gBAAc,MAAM;AACpB,kBAAgB,MAAM;AACxB;;;ACtgBA,SAAS,OAAOC,iBAAqC;AAwDrD,IAAM,eAAe;AACrB,IAAM,uBAAuB;AAC7B,IAAM,iBAAiB;AACvB,IAAMC,eAAc;AACpB,IAAMC,mBAAkB;AAIxB,SAAS,YACP,aACA,kBACiD;AAEjD,MAAI,eAAe,YAAY,SAAS,GAAG;AACzC,eAAW,KAAK,aAAa;AAC3B,UAAI,EAAE,SAAS,aAAa,EAAE,SAAS,WAAW;AAChD,cAAM,QAAQ,WAAW,EAAE,KAAK;AAChC,YAAI,SAAS,EAAK,QAAO;AACzB,YAAI,SAAS,EAAK,QAAO;AACzB,YAAI,SAAS,EAAK,QAAO;AACzB,YAAI,SAAS,IAAK,QAAO;AAAA,MAC3B;AAAA,IACF;AAAA,EACF;AAGA,MAAI,kBAAkB;AACpB,UAAM,KAAK,iBAAiB,YAAY;AACxC,QAAI,OAAO,WAAY,QAAO;AAC9B,QAAI,OAAO,OAAQ,QAAO;AAC1B,QAAI,OAAO,YAAY,OAAO,WAAY,QAAO;AACjD,QAAI,OAAO,MAAO,QAAO;AAAA,EAC3B;AAEA,SAAO;AACT;AAIA,SAAS,eAAe,MAAc,QAAqE;AACzG,SAAO,IAAI,QAAQ,CAACC,UAAS,WAAW;AACtC,UAAM,UAA0B;AAAA,MAC9B,UAAU;AAAA,MACV,MAAM;AAAA,MACN,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,gBAAgB;AAAA,QAChB,kBAAkB,OAAO,WAAW,IAAI,EAAE,SAAS;AAAA,QACnD,cAAc;AAAA,MAChB;AAAA,MACA;AAAA,MACA,SAAS;AAAA,IACX;AAEA,UAAM,MAAMH,UAAS,SAAS,CAAC,QAAyB;AACtD,YAAM,aAAa,IAAI,cAAc;AACrC,UAAI,eAAe;AACnB,UAAI,GAAG,QAAQ,CAAC,UAAkB;AAChC,wBAAgB;AAAA,MAClB,CAAC;AACD,UAAI,GAAG,OAAO,MAAM;AAClB,QAAAG,SAAQ,EAAE,YAAY,MAAM,aAAa,CAAC;AAAA,MAC5C,CAAC;AAAA,IACH,CAAC;AAED,QAAI,GAAG,SAAS,CAAC,QAAe;AAC9B,aAAO,GAAG;AAAA,IACZ,CAAC;AAED,QAAI,GAAG,WAAW,MAAM;AACtB,UAAI,QAAQ;AACZ,aAAO,IAAI,MAAM,yBAAyB,CAAC;AAAA,IAC7C,CAAC;AAED,QAAI,MAAM,IAAI;AACd,QAAI,IAAI;AAAA,EACV,CAAC;AACH;AAIA,SAASC,OAAM,IAA2B;AACxC,SAAO,IAAI,QAAQ,CAACD,aAAY,WAAWA,UAAS,EAAE,CAAC;AACzD;AAWA,eAAsB,cACpB,OACA,UAAgD,CAAC,GACxB;AACzB,QAAM,aAAa,oBAAI,IAAoC;AAG3D,aAAW,QAAQ,OAAO;AACxB,eAAW,IAAI,MAAM,CAAC,CAAC;AAAA,EACzB;AAGA,QAAM,UAAsB,CAAC;AAC7B,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK,gBAAgB;AACrD,YAAQ,KAAK,MAAM,MAAM,GAAG,IAAI,cAAc,CAAC;AAAA,EACjD;AAEA,MAAI;AAEJ,aAAW,SAAS,SAAS;AAC3B,UAAM,cAAoC;AAAA,MACxC,SAAS,MAAM,IAAI,CAAC,UAAU;AAAA,QAC5B,SAAS,EAAE,KAAK;AAAA,MAClB,EAAE;AAAA,IACJ;AAEA,UAAM,WAAW,KAAK,UAAU,WAAW;AAE3C,QAAI,UAAU;AACd,aAAS,UAAU,GAAG,UAAUF,gBAAe,CAAC,SAAS,WAAW;AAClE,UAAI;AACF,cAAM,WAAW,MAAM,eAAe,UAAU,QAAQ,MAAM;AAE9D,YAAI,SAAS,eAAe,KAAK;AAC/B,gBAAM,SAAS,KAAK,MAAM,SAAS,IAAI;AAEvC,cAAI,OAAO,WAAW,MAAM,QAAQ,OAAO,OAAO,GAAG;AACnD,qBAAS,IAAI,GAAG,IAAI,OAAO,QAAQ,QAAQ,KAAK;AAC9C,oBAAM,OAAO,MAAM,CAAC;AACpB,kBAAI,CAAC,KAAM;AAEX,oBAAM,QAAQ,OAAO,QAAQ,CAAC,GAAG;AACjC,kBAAI,CAAC,SAAS,MAAM,WAAW,EAAG;AAElC,oBAAM,SAAwB,CAAC;AAC/B,yBAAW,QAAQ,OAAO;AAExB,sBAAM,aAAa,KAAK;AACxB,sBAAM,qBAAqB,KAAK,WAAW,CAAC,GAAG;AAC/C,sBAAM,iBAAiB,YAAY,YAAY,oBAAoB;AAEnE,uBAAO,KAAK;AAAA,kBACV,IAAI,KAAK;AAAA,kBACT,SAAS,KAAK,WAAW,KAAK,WAAW;AAAA,kBACzC,UAAU,YAAY,KAAK,UAAU,cAAc;AAAA,kBACnD,SAAS,KAAK,WAAW,CAAC;AAAA,gBAC5B,CAAC;AAAA,cACH;AAEA,yBAAW,IAAI,MAAM,MAAM;AAAA,YAC7B;AAAA,UACF;AAEA,oBAAU;AAAA,QACZ,WAAW,SAAS,eAAe,OAAO,SAAS,cAAc,KAAK;AAEpE,cAAI,UAAUA,eAAc,GAAG;AAC7B,kBAAM,UAAUC,mBAAkB,KAAK,IAAI,GAAG,OAAO,IAAI,KAAK,OAAO,IAAI;AACzE,kBAAME,OAAM,OAAO;AAAA,UACrB,OAAO;AACL,kBAAM,IAAI,MAAM,oBAAoB,SAAS,UAAU,UAAUH,YAAW,cAAc,SAAS,IAAI,EAAE;AAAA,UAC3G;AAAA,QACF,OAAO;AAEL,gBAAM,IAAI,MAAM,oBAAoB,SAAS,UAAU,KAAK,SAAS,IAAI,EAAE;AAAA,QAC7E;AAAA,MACF,SAAS,KAAK;AACZ,oBAAY,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC;AAC9D,YAAI,UAAUA,eAAc,GAAG;AAC7B,gBAAM,UAAUC,mBAAkB,KAAK,IAAI,GAAG,OAAO,IAAI,KAAK,OAAO,IAAI;AACzE,gBAAME,OAAM,OAAO;AAAA,QACrB;AAAA,MACF;AAAA,IACF;AAEA,QAAI,CAAC,WAAW,WAAW;AAGzB,YAAM;AAAA,IACR;AAAA,EACF;AAEA,QAAM,WAAqB;AAAA,IACzB,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,IACpC,QAAQ,WAAW,MAAM,MAAM,gBAAgB,QAAQ,MAAM;AAAA,EAC/D;AAEA,SAAO,EAAE,YAAY,SAAS;AAChC;AAMA,eAAsB,eACpB,MACA,UAAgD,CAAC,GAChB;AACjC,QAAM,SAAS,MAAM,cAAc,CAAC,IAAI,GAAG,OAAO;AAClD,SAAO,OAAO,WAAW,IAAI,IAAI,KAAK,CAAC;AACzC;;;ACtQA,SAAS,iBAAwC;AACjD,SAAS,cAAAC,mBAAkB;AAC3B,SAAS,QAAAC,aAAY;AAuBrB,SAAS,YAAY,GAAuC;AAC1D,UAAQ,EAAE,YAAY,GAAG;AAAA,IACvB,KAAK;AAAY,aAAO;AAAA,IACxB,KAAK;AAAQ,aAAO;AAAA,IACpB,KAAK;AAAA,IACL,KAAK;AAAU,aAAO;AAAA,IACtB,KAAK;AAAO,aAAO;AAAA,IACnB;AAAS,aAAO;AAAA,EAClB;AACF;AAGA,SAAS,cAAc,GAAuC;AAC5D,UAAQ,EAAE,YAAY,GAAG;AAAA,IACvB,KAAK;AAAY,aAAO;AAAA,IACxB,KAAK;AAAQ,aAAO;AAAA,IACpB,KAAK;AAAU,aAAO;AAAA,IACtB,KAAK;AAAO,aAAO;AAAA,IACnB;AAAS,aAAO;AAAA,EAClB;AACF;AAOO,SAAS,YAAYC,gBAA0C;AACpE,QAAM,SAAS,gBAAgB,OAAO,CAAC,SAAS,QAAQ,GAAGA,cAAa;AACxE,QAAM,aAA+B,CAAC;AACtC,MAAI,cAAwB,CAAC;AAE7B,MAAI,OAAO,WAAW,KAAK,OAAO,WAAW,GAAG;AAE9C,QAAI;AACF,YAAM,OAAO,KAAK,MAAM,OAAO,UAAU,IAAI;AAC7C,YAAM,kBAAkB,KAAK;AAE7B,UAAI,iBAAiB;AACnB,mBAAW,CAAC,KAAK,IAAI,KAAK,OAAO,QAAQ,eAAe,GAAG;AACzD,gBAAM,MAAM,KAAK;AACjB,cAAI,CAAC,IAAK;AAEV,qBAAW,YAAY,KAAK;AAC1B,gBAAI,OAAO,aAAa,SAAU;AAClC,kBAAM,SAAS,SAAS;AACxB,kBAAM,OAAO,SAAS;AAEtB,gBAAI,OAAO,WAAW,SAAU;AAEhC,uBAAW,KAAK;AAAA,cACd,IAAK,SAAS,OAAmB,SAAS,QAAmB,OAAO,GAAG,IAAI,QAAQ,SAAS;AAAA,cAC5F,aAAa;AAAA,cACb,UAAU,YAAa,KAAK,YAAuB,MAAM;AAAA,cACzD,SAAU,SAAS,UAAqB,QAAQ;AAAA,cAChD,YAAa,KAAK,gBAA2B;AAAA,cAC7C,KAAM,SAAS,OAAkB;AAAA,cACjC,SAAU,SAAS,MAAiB,CAAE,SAAS,GAAc,IAAI,CAAC;AAAA,YACpE,CAAC;AAAA,UACH;AAAA,QACF;AAAA,MACF;AAEA,YAAM,WAAW,KAAK;AACtB,UAAI,UAAU;AACZ,sBAAc;AAAA,UACZ,0BAA0B,SAAS,mBAA6B,SAAS;AAAA,UACzE,uBAAuB,SAAS,qBAA+B,SAAS;AAAA,QAC1E;AAAA,MACF;AAAA,IACF,QAAQ;AACN,oBAAc,CAAC,uCAAuC;AAAA,IACxD;AAAA,EACF,OAAO;AACL,kBAAc,CAAC,8BAA8B,OAAO,MAAM,EAAE;AAAA,EAC9D;AAEA,QAAM,WAAqB;AAAA,IACzB,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,IACpC,QAAQ,YAAY,KAAK,IAAI,KAAK,SAAS,WAAW,MAAM;AAAA,EAC9D;AAEA,SAAO,EAAE,YAAY,SAAS;AAChC;AAQO,SAAS,YAAYA,gBAA0C;AAEpE,QAAM,WAAW,CAAC,oBAAoB,sBAAsB;AAC5D,MAAI,UAAU;AACd,aAAW,KAAK,UAAU;AACxB,QAAIF,YAAWC,MAAKC,gBAAe,CAAC,CAAC,GAAG;AACtC,gBAAU,iBAAiB,CAAC;AAC5B;AAAA,IACF;AAAA,EACF;AAEA,QAAM,OAAO,CAAC,SAAS,YAAY,MAAM;AACzC,MAAI,SAAS;AACX,SAAK,KAAK,GAAG,QAAQ,MAAM,GAAG,CAAC;AAAA,EACjC;AAEA,QAAM,SAAS,gBAAgB,eAAe,QAAQ,MAAM,cAAc,aAAa,MAAMA,cAAa;AAC1G,SAAO,oBAAoB,MAAM;AACnC;AAEA,SAAS,oBAAoB,QAA+C;AAC1E,QAAM,aAA+B,CAAC;AACtC,MAAI,cAAwB,CAAC;AAE7B,MAAI,OAAO,WAAW,GAAG;AACvB,QAAI;AACF,YAAM,OAAO,KAAK,MAAM,OAAO,UAAU,IAAI;AAC7C,iBAAW,SAAS,MAAM;AACxB,mBAAW,KAAK;AAAA,UACd,IAAK,MAAM,MAAkB,MAAM,oBAA+B;AAAA,UAClE,aAAc,MAAM,QAAmB;AAAA,UACvC,UAAU,YAAa,MAAM,YAAuB,MAAM;AAAA,UAC1D,SAAU,MAAM,eAA2B,MAAM,oBAA+B;AAAA,UAChF,YAAa,MAAM,eAA0B;AAAA,UAC7C,KAAM,MAAM,gBAA2B;AAAA,UACvC,SAAU,MAAM,WAAwB,CAAC;AAAA,QAC3C,CAAC;AAAA,MACH;AAAA,IACF,QAAQ;AACN,oBAAc,CAAC,uCAAuC;AAAA,IACxD;AAAA,EACF,OAAO;AACL,kBAAc,CAAC,8BAA8B,OAAO,MAAM,KAAK,OAAO,MAAM,EAAE;AAAA,EAChF;AAEA,QAAM,WAAqB;AAAA,IACzB,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,IACpC,QAAQ,YAAY,KAAK,IAAI,KAAK,SAAS,WAAW,MAAM;AAAA,EAC9D;AAEA,SAAO,EAAE,YAAY,SAAS;AAChC;AAOO,SAAS,cAAcA,gBAA0C;AACtE,QAAM,SAAS,gBAAgB,SAAS,CAAC,SAAS,QAAQ,GAAGA,cAAa;AAC1E,SAAO,sBAAsB,MAAM;AACrC;AAEA,SAAS,sBAAsB,QAA+C;AAC5E,QAAM,aAA+B,CAAC;AACtC,MAAI,cAAwB,CAAC;AAE7B,MAAI,OAAO,WAAW,GAAG;AACvB,QAAI;AACF,YAAM,OAAO,KAAK,MAAM,OAAO,UAAU,IAAI;AAC7C,YAAM,kBAAkB,KAAK;AAC7B,YAAM,iBAAiB,iBAAiB;AAExC,UAAI,gBAAgB;AAClB,mBAAW,OAAO,gBAAgB;AAChC,gBAAM,WAAW,IAAI;AACrB,gBAAM,MAAM,IAAI;AAChB,cAAI,CAAC,SAAU;AAEf,qBAAW,KAAK;AAAA,YACd,IAAK,SAAS,MAAiB;AAAA,YAC/B,aAAc,KAAK,QAAmB;AAAA,YACtC,UAAU,eAAe,SAAS,QAAkB,IAAI,MAAM,GAAG,IAAI,CAAC,KAAK,MAAM;AAAA,YACjF,SAAU,SAAS,SAAqB,SAAS,eAA0B;AAAA,YAC3E,YAAa,SAAS,oBAA+B;AAAA,YACrD,KAAM,SAAS,OAAkB;AAAA,YACjC,SAAU,SAAS,WAAwB,CAAC;AAAA,UAC9C,CAAC;AAAA,QACH;AAAA,MACF;AAAA,IACF,QAAQ;AACN,oBAAc,CAAC,yCAAyC;AAAA,IAC1D;AAAA,EACF,OAAO;AACL,kBAAc,CAAC,gCAAgC,OAAO,MAAM,KAAK,OAAO,MAAM,EAAE;AAAA,EAClF;AAEA,QAAM,WAAqB;AAAA,IACzB,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,IACpC,QAAQ,YAAY,KAAK,IAAI,KAAK,SAAS,WAAW,MAAM;AAAA,EAC9D;AAEA,SAAO,EAAE,YAAY,SAAS;AAChC;AASO,SAAS,eAAeA,gBAA0C;AACvE,QAAM,SAAS,gBAAgB,eAAe,CAAC,OAAO,GAAGA,cAAa;AACtE,QAAM,aAA+B,CAAC;AACtC,MAAI,cAAwB,CAAC;AAE7B,MAAI,OAAO,WAAW,KAAK,OAAO,WAAW,GAAG;AAE9C,QAAI;AACF,YAAM,OAAO,KAAK,MAAM,OAAO,UAAU,IAAI;AAC7C,YAAM,QAAQ,KAAK;AAEnB,UAAI,OAAO;AACT,mBAAW,KAAK,OAAO;AACrB,gBAAM,MAAM,EAAE;AAEd,qBAAW,KAAK;AAAA,YACd,IAAK,EAAE,MAAiB,OAAO;AAAA,YAC/B,aAAc,EAAE,WAAuB,EAAE,eAA0B;AAAA,YACnE,UAAU;AAAA;AAAA,YACV,SAAU,EAAE,WAAuB,EAAE,eAA0B,OAAO;AAAA,YACtE,YAAa,EAAE,iBAA4B;AAAA,YAC3C,KAAM,EAAE,OAAkB;AAAA,YAC1B,SAAS,MAAM,CAAC,GAAG,IAAI,CAAC;AAAA,UAC1B,CAAC;AAAA,QACH;AAAA,MACF;AAAA,IACF,QAAQ;AACN,oBAAc,CAAC,yCAAyC;AAAA,IAC1D;AAAA,EACF,WAAW,OAAO,WAAW,GAAG;AAC9B,kBAAc,CAAC,uCAAuC;AAAA,EACxD,OAAO;AACL,kBAAc,CAAC,gCAAgC,OAAO,MAAM,KAAK,OAAO,MAAM,EAAE;AAAA,EAClF;AAEA,QAAM,WAAqB;AAAA,IACzB,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,IACpC,QAAQ,YAAY,KAAK,IAAI,KAAK,SAAS,WAAW,MAAM;AAAA,EAC9D;AAEA,SAAO,EAAE,YAAY,SAAS;AAChC;AAOO,SAAS,iBAAiBA,gBAA0C;AACzE,QAAM,SAAS,gBAAgB,YAAY,CAAC,SAAS,eAAe,GAAGA,cAAa;AACpF,QAAM,aAA+B,CAAC;AACtC,MAAI,cAAwB,CAAC;AAE7B,MAAI,OAAO,WAAW,GAAG;AACvB,QAAI;AACF,YAAM,OAAO,KAAK,MAAM,OAAO,UAAU,IAAI;AAC7C,YAAM,iBAAiB,KAAK;AAE5B,UAAI,gBAAgB;AAClB,mBAAW,CAAC,KAAK,IAAI,KAAK,OAAO,QAAQ,cAAc,GAAG;AACxD,qBAAW,OAAO,MAAM;AACtB,uBAAW,KAAK;AAAA,cACd,IAAK,IAAI,OAAmB,IAAI,aAAwB,YAAY,GAAG;AAAA,cACvE,aAAa;AAAA,cACb,UAAU,YAAa,IAAI,YAAuB,QAAQ;AAAA,cAC1D,SAAU,IAAI,SAAqB,IAAI,eAA0B;AAAA,cACjE,YAAY,IAAI,OAAQ,IAAI,KAAgB,MAAM,GAAG,EAAE,IAAI,IAAI;AAAA,cAC/D,KAAM,IAAI,QAAmB;AAAA,cAC7B,SAAU,IAAI,MAAiB,CAAE,IAAI,GAAc,IAAI,CAAC;AAAA,YAC1D,CAAC;AAAA,UACH;AAAA,QACF;AAAA,MACF;AAAA,IACF,QAAQ;AACN,oBAAc,CAAC,4CAA4C;AAAA,IAC7D;AAAA,EACF,OAAO;AACL,kBAAc,CAAC,mCAAmC,OAAO,MAAM,KAAK,OAAO,MAAM,EAAE;AAAA,EACrF;AAEA,QAAM,WAAqB;AAAA,IACzB,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,IACpC,QAAQ,YAAY,KAAK,IAAI,KAAK,SAAS,WAAW,MAAM;AAAA,EAC9D;AAEA,SAAO,EAAE,YAAY,SAAS;AAChC;AAQO,SAAS,eAAeA,gBAA0C;AAEvE,QAAM,SAAS,gBAAgB,UAAU,CAAC,WAAW,SAAS,YAAY,MAAM,GAAGA,cAAa;AAChG,QAAM,aAA+B,CAAC;AACtC,MAAI,cAAwB,CAAC;AAE7B,MAAI,OAAO,WAAW,GAAG;AACvB,QAAI;AACF,YAAM,OAAO,KAAK,MAAM,OAAO,UAAU,IAAI;AAC7C,YAAM,kBAAkB,KAAK;AAC7B,YAAM,WAAW,KAAK;AAGtB,UAAI,iBAAiB;AAEnB,cAAM,WAAW,MAAM,QAAQ,eAAe,IAC1C,kBACA,CAAC;AACL,mBAAW,KAAK,UAAU;AACxB,qBAAW,KAAK;AAAA,YACd,IAAK,EAAE,cAA0B,EAAE,MAAiB;AAAA,YACpD,aAAc,EAAE,eAA0B;AAAA,YAC1C,UAAU,YAAa,EAAE,YAAuB,MAAM;AAAA,YACtD,SAAU,EAAE,eAA2B,EAAE,SAAoB;AAAA,YAC7D,YAAa,EAAE,gBAA4B,EAAE,kBAA6B;AAAA,YAC1E,KAAM,EAAE,eAA2B,EAAE,OAAkB;AAAA,YACvD,SAAU,EAAE,WAAwB,CAAC;AAAA,UACvC,CAAC;AAAA,QACH;AAAA,MACF,WAAW,UAAU;AAEnB,mBAAW,CAAC,KAAK,OAAO,KAAK,OAAO,QAAQ,QAAQ,GAAG;AACrD,qBAAW,SAAS,SAAS;AAC3B,uBAAW,KAAK;AAAA,cACd,IAAK,MAAM,cAA0B,MAAM,MAAiB;AAAA,cAC5D,aAAa;AAAA,cACb,UAAU,YAAa,MAAM,YAAuB,MAAM;AAAA,cAC1D,SAAU,MAAM,eAA2B,MAAM,SAAoB;AAAA,cACrE,YAAa,MAAM,gBAA4B,MAAM,kBAA6B;AAAA,cAClF,KAAM,MAAM,eAA2B,MAAM,OAAkB;AAAA,cAC/D,SAAU,MAAM,WAAwB,CAAC;AAAA,YAC3C,CAAC;AAAA,UACH;AAAA,QACF;AAAA,MACF;AAAA,IACF,QAAQ;AACN,oBAAc,CAAC,kDAAkD;AAAA,IACnE;AAAA,EACF,OAAO;AACL,kBAAc,CAAC,yCAAyC,OAAO,MAAM,KAAK,OAAO,MAAM,EAAE;AAAA,EAC3F;AAEA,QAAM,WAAqB;AAAA,IACzB,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,IACpC,QAAQ,YAAY,KAAK,IAAI,KAAK,SAAS,WAAW,MAAM;AAAA,EAC9D;AAEA,SAAO,EAAE,YAAY,SAAS;AAChC;AAUA,SAAS,gBACP,SACA,MACA,KACoB;AACpB,MAAI;AACF,UAAM,UAA4B;AAAA,MAChC;AAAA,MACA,UAAU;AAAA,MACV,SAAS;AAAA,MACT,WAAW,KAAK,OAAO;AAAA;AAAA,MACvB,aAAa;AAAA,IACf;AAEA,UAAM,SAAS,UAAU,SAAS,MAAkB,OAAO;AAE3D,WAAO;AAAA,MACL,QAAQ,OAAO;AAAA,MACf,QAAQ,OAAO,QAAQ,SAAS,KAAK;AAAA,MACrC,QAAQ,OAAO,QAAQ,SAAS,KAAK;AAAA,IACvC;AAAA,EACF,SAAS,KAAK;AACZ,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,QAAQ,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAAA,IACzD;AAAA,EACF;AACF;AAQO,SAAS,eACd,WACAA,gBACmB;AACnB,UAAQ,WAAW;AAAA,IACjB,KAAK;AACH,aAAO,YAAYA,cAAa;AAAA,IAClC,KAAK;AACH,aAAO,YAAYA,cAAa;AAAA,IAClC,KAAK;AACH,aAAO,cAAcA,cAAa;AAAA,IACpC,KAAK;AACH,aAAO,eAAeA,cAAa;AAAA,IACrC,KAAK;AACH,aAAO,iBAAiBA,cAAa;AAAA,IACvC,KAAK;AACH,aAAO,eAAeA,cAAa;AAAA;AAAA,IAErC;AACE,aAAO;AAAA,QACL,YAAY,CAAC;AAAA,QACb,UAAU;AAAA,UACR,MAAM;AAAA,UACN,QAAQ,gBAAgB,SAAS;AAAA,UACjC,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,UACpC,QAAQ,uCAAuC,SAAS;AAAA,QAC1D;AAAA,MACF;AAAA,EACJ;AACF;AAOO,SAAS,uBAAuB,WAAiC;AACtE,QAAM,SAAS;AAAA,IACb,cAAc,QAAQ,QACtB,cAAc,WAAW,cACzB,cAAc,SAAS,UACvB,cAAc,OAAO,gBACrB,cAAc,QAAQ,aACtB,cAAc,WAAW,WAAW;AAAA,IACpC,CAAC,WAAW;AAAA,IACZ,QAAQ,IAAI;AAAA,EACd;AAEA,SAAO,OAAO,WAAW;AAC3B;;;ACncA,SAAS,cAAc,SAA0B;AAC/C,SAAO,iBAAiB,KAAK,OAAO;AACtC;AAOO,SAAS,gBAAgB,GAAW,GAAmB;AAG5D,QAAM,SAAS,EAAE,MAAM,qBAAqB;AAC5C,QAAM,SAAS,EAAE,MAAM,qBAAqB;AAC5C,QAAM,QAAQ,SAAS,CAAC,KAAK;AAC7B,QAAM,OAAO,SAAS,CAAC;AACvB,QAAM,QAAQ,SAAS,CAAC,KAAK;AAC7B,QAAM,OAAO,SAAS,CAAC;AAGvB,QAAM,aAAa,MAAM,MAAM,GAAG,EAAE,IAAI,MAAM;AAC9C,QAAM,aAAa,MAAM,MAAM,GAAG,EAAE,IAAI,MAAM;AAE9C,WAAS,IAAI,GAAG,IAAI,KAAK,IAAI,WAAW,QAAQ,WAAW,MAAM,GAAG,KAAK;AACvE,UAAM,OAAO,WAAW,CAAC,KAAK;AAC9B,UAAM,OAAO,WAAW,CAAC,KAAK;AAC9B,QAAI,OAAO,KAAM,QAAO;AACxB,QAAI,OAAO,KAAM,QAAO;AAAA,EAC1B;AAGA,MAAI,SAAS,KAAM,QAAO;AAC1B,MAAI,SAAS,OAAW,QAAO;AAC/B,MAAI,SAAS,OAAW,QAAO;AAG/B,QAAM,YAAY,KAAK,MAAM,GAAG;AAChC,QAAM,YAAY,KAAK,MAAM,GAAG;AAChC,WAAS,IAAI,GAAG,IAAI,KAAK,IAAI,UAAU,QAAQ,UAAU,MAAM,GAAG,KAAK;AACrE,UAAM,MAAM,UAAU,CAAC,KAAK;AAC5B,UAAM,MAAM,UAAU,CAAC,KAAK;AAC5B,QAAI,QAAQ,IAAK;AACjB,UAAM,OAAO,OAAO,GAAG;AACvB,UAAM,OAAO,OAAO,GAAG;AACvB,QAAI,CAAC,OAAO,MAAM,IAAI,KAAK,CAAC,OAAO,MAAM,IAAI,KAAK,OAAO,SAAS,IAAI,KAAK,OAAO,SAAS,IAAI,GAAG;AAChG,UAAI,OAAO,KAAM,QAAO;AACxB,UAAI,OAAO,KAAM,QAAO;AAAA,IAC1B,OAAO;AACL,UAAI,MAAM,IAAK,QAAO;AACtB,UAAI,MAAM,IAAK,QAAO;AAAA,IACxB;AACA,WAAO,MAAM,MAAM,IAAI;AAAA,EACzB;AACA,SAAO;AACT;AAMA,SAAS,mBAAmB,YAA6B;AACvD,SACE,WAAW,WAAW,GAAG,KACzB,WAAW,WAAW,GAAG,KACzB,WAAW,WAAW,IAAI,KAC1B,WAAW,WAAW,GAAG,KACzB,cAAc,UAAU;AAE5B;AAaA,SAAS,kBAAkB,QAAgB,cAAsB,YAA8B;AAC7F,QAAM,iBAAiB,YAAY,KAAK,KAAK;AAG7C,QAAM,cAAc,OAAO,MAAM,GAAG,EAAE,CAAC;AACvC,QAAM,cAAc,aAAa,MAAM,GAAG,EAAE,CAAC;AAE7C,MAAI,CAAC,eAAe,CAAC,YAAa,QAAO;AAGzC,MAAI,eAAe,WAAW,GAAG,GAAG;AAClC,WAAO,gBAAgB;AAAA,EACzB;AAGA,MAAI,eAAe,WAAW,GAAG,GAAG;AAClC,UAAM,cAAc,OAAO,MAAM,GAAG,EAAE,CAAC;AACvC,UAAM,cAAc,aAAa,MAAM,GAAG,EAAE,CAAC;AAC7C,QAAI,eAAe,eAAe,gBAAgB,YAAa,QAAO;AACtE,QAAI,eAAe,eAAe,gBAAgB,YAAa,QAAO;AACtE,WAAO;AAAA,EACT;AAGA,MAAI,eAAe,WAAW,IAAI,KAAK,eAAe,WAAW,GAAG,GAAG;AACrE,WAAO,gBAAgB;AAAA,EACzB;AAMA,MAAI,cAAc,cAAc,GAAG;AACjC,WAAO,gBAAgB;AAAA,EACzB;AAGA,SAAO,gBAAgB;AACzB;AAaO,SAAS,eACd,KACA,cACA,cACkB;AAIlB,MAAI,IAAI,eAAe,QAAQ;AAC7B,WAAO;AAAA,EACT;AAGA,MAAI,IAAI,eAAe,OAAO;AAC5B,WAAO;AAAA,EACT;AAIA,MAAI,cAAc,qBAAqB;AACrC,WAAO;AAAA,EACT;AAIA,MAAI,cAAc,cAAc;AAC9B,WAAO;AAAA,EACT;AAIA,MAAI,cAAc,YAAY;AAC5B,WAAO;AAAA,EACT;AAEA,MAAI,cAAc,QAAQ;AACxB,WAAO;AAAA,EACT;AAIA,MAAI,cAAc,aAAa;AAC7B,WAAO;AAAA,EACT;AAIA,QAAM,SAAS,IAAI;AACnB,QAAM,eAAe,cAAc;AAEnC,MAAI,UAAU,cAAc;AAC1B,QAAI,WAAW,cAAc;AAC3B,aAAO;AAAA,IACT;AAEA,QAAI;AACF,YAAM,MAAM,gBAAgB,QAAQ,YAAY;AAChD,UAAI,MAAM,GAAG;AAEX,cAAM,aAAa,IAAI;AACvB,YAAI,cAAc,mBAAmB,UAAU,GAAG;AAChD,gBAAM,WAAW,kBAAkB,QAAQ,cAAc,UAAU;AACnE,iBAAO,WAAW,8BAA8B;AAAA,QAClD;AAEA,eAAO;AAAA,MACT;AAGA,aAAO;AAAA,IACT,QAAQ;AAAA,IAER;AAAA,EACF;AAMA,MAAI,IAAI,WAAW,gBAAgB,IAAI,WAAW,kBAAkB;AAClE,WAAO,IAAI;AAAA,EACb;AAIA,SAAO,IAAI,UAAU;AACvB;AAKO,SAAS,0BACd,QACA,QACoB;AACpB,SAAO;AAAA,IACL,qBAAqB;AAAA,IACrB,UAAU;AAAA,MACR;AAAA,QACE,MAAM;AAAA,QACN;AAAA,QACA,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,QACpC,QAAQ,UAAU;AAAA,MACpB;AAAA,IACF;AAAA,EACF;AACF;AAKO,SAAS,mBACd,QACA,OACoB;AACpB,SAAO;AAAA,IACL,cAAc;AAAA,IACd,UAAU;AAAA,MACR;AAAA,QACE,MAAM;AAAA,QACN;AAAA,QACA,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,QACpC,QAAQ,SAAS;AAAA,MACnB;AAAA,IACF;AAAA,EACF;AACF;;;ACvSA,SAAS,kBAAkB;;;ACSpB,IAAM,uBAAuB;AAWpC,IAAM,oBAAwE;AAAA,EAC5E,YAAY;AAAA,EACZ,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,wBAAwB;AAAA,EACxB,2BAA2B;AAC7B;AAOA,IAAM,qBAAgE;AAAA,EACpE,YAAY;AAAA,EACZ,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,2BAA2B;AAAA,EAC3B,wBAAwB;AAC1B;AAEA,IAAM,eAAe;AACrB,IAAM,gBAAgB;AAEtB,SAAS,YAAY,KAAoC;AACvD,QAAM,OAAO,mBAAmB,IAAI,MAAM,KAAK;AAC/C,QAAM,SAAS,IAAI,SAAS,eAAe;AAC3C,QAAM,UAAU,IAAI,UAAU,YAAY,gBAAgB;AAC1D,SAAO,OAAO,SAAS;AACzB;AAUA,SAAS,SAAS,KAAoC;AACpD,SAAO,GAAG,IAAI,SAAS,KAAI,IAAI,IAAI,KAAI,IAAI,UAAU,IAAI,aAAa,EAAE;AAC1E;AAeO,SAAS,iBACd,cACA,UAAyB,CAAC,GACE;AAC5B,QAAM,QAAQ,KAAK,IAAI,GAAG,QAAQ,SAAS,oBAAoB;AAC/D,MAAI,UAAU,EAAG,QAAO,CAAC;AAEzB,QAAM,OAAO,oBAAI,IAA6B;AAE9C,aAAW,cAAc,cAAc;AACrC,UAAM,UAAU,kBAAkB,WAAW,MAAM;AACnD,QAAI,CAAC,QAAS;AAGd,QAAI,WAAW,eAAe,UAAU,WAAW,eAAe,MAAO;AAEzE,UAAM,YAA6B;AAAA,MACjC;AAAA,MACA;AAAA,MACA,UAAU,YAAY,UAAU;AAAA,IAClC;AAEA,UAAM,MAAM,SAAS,UAAU;AAC/B,UAAM,WAAW,KAAK,IAAI,GAAG;AAG7B,QAAI,CAAC,YAAY,UAAU,WAAW,SAAS,UAAU;AACvD,WAAK,IAAI,KAAK,SAAS;AAAA,IACzB;AAAA,EACF;AAEA,SAAO,CAAC,GAAG,KAAK,OAAO,CAAC,EACrB;AAAA,IACC,CAAC,GAAG,MACF,EAAE,WAAW,EAAE,YACf,EAAE,WAAW,KAAK,cAAc,EAAE,WAAW,IAAI,MAChD,EAAE,WAAW,UAAU,IAAI,cAAc,EAAE,WAAW,UAAU,EAAE;AAAA,EACvE,EACC,MAAM,GAAG,KAAK;AACnB;AAGO,SAAS,kBACd,YAC0D;AAC1D,QAAM,MAAM,oBAAI,IAAwC;AACxD,aAAW,aAAa,YAAY;AAClC,UAAM,OAAO,IAAI,IAAI,UAAU,OAAO;AACtC,QAAI,KAAM,MAAK,KAAK,SAAS;AAAA,QACxB,KAAI,IAAI,UAAU,SAAS,CAAC,SAAS,CAAC;AAAA,EAC7C;AACA,SAAO;AACT;;;ADnDA,SAAS,WAAW,WAAsD;AACxE,UAAQ,WAAW;AAAA,IACjB,KAAK;AAAO,aAAO;AAAA,IACnB,KAAK;AAAU,aAAO;AAAA,IACtB,KAAK;AAAQ,aAAO;AAAA,IACpB,KAAK;AAAM,aAAO;AAAA,IAClB,KAAK;AAAU,aAAO;AAAA,IACtB,KAAK;AAAO,aAAO;AAAA,IACnB,KAAK;AAAQ,aAAO;AAAA;AAAA,IAEpB,KAAK;AAAS,aAAO;AAAA,IACrB,KAAK;AAAU,aAAO;AAAA;AAAA,IACtB,KAAK;AAAQ,aAAO;AAAA,IACpB,KAAK;AAAS,aAAO;AAAA;AAAA,IACrB,KAAK;AAAU,aAAO;AAAA;AAAA,IAEtB,KAAK;AAAO,aAAO;AAAA,IACnB;AAAS,aAAO;AAAA,EAClB;AACF;AAIA,IAAM,kBAAkB;AAIjB,IAAM,kBAAN,MAAsB;AAAA,EACnB;AAAA,EAER,YAAY,OAAuB;AACjC,SAAK,QAAQ;AAAA,EACf;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,MAAM,UACJ,WACA,YACA,QACA,YACmB;AACnB,UAAM,aAAa,WAAW;AAG9B,iBAAa,eAAe,GAAG,CAAC;AAChC,UAAM,gBAAgB,MAAM,mBAAmB,UAAU;AAGzD,UAAM,aAA0B,cAAc,IAAI,CAAC,OAAO;AAAA,MACxD,IAAI,EAAE;AAAA,MACN,cAAc,EAAE;AAAA,MAChB,WAAW,EAAE;AAAA,MACb,gBAAgB,EAAE;AAAA,MAClB,WAAW,CAAC,GAAG,EAAE,SAAS;AAAA,MAC1B,WAAW,CAAC,GAAG,EAAE,SAAS;AAAA,MAC1B,YAAY,EAAE;AAAA,MACd,UAAU,EAAE;AAAA,IACd,EAAE;AAGF,iBAAa,gBAAgB,GAAG,WAAW,MAAM;AACjD,UAAM,kBAA2C,CAAC;AAClD,QAAI,gBAA0B;AAE9B,aAAS,IAAI,GAAG,IAAI,WAAW,QAAQ,KAAK;AAC1C,YAAM,KAAK,WAAW,CAAC;AACvB,YAAM,UAAU,WAAW,GAAG,SAAS;AACvC,UAAI,OAAyC,CAAC;AAE9C,UAAI,SAAS;AACX,YAAI;AAIF,iBAAO,MAAM,QAAQ,UAAU,IAAI,EAAE,aAAa,WAAW,CAAC;AAAA,QAChE,QAAQ;AACN,iBAAO,CAAC;AAAA,QACV;AAAA,MACF;AAGA,UAAI,GAAG,aAAa,eAAe;AACjC,wBAAgB;AAAA,MAClB;AAEA,sBAAgB,KAAK,GAAG,IAAI;AAC5B,mBAAa,gBAAgB,IAAI,GAAG,WAAW,MAAM;AAAA,IACvD;AAGA,UAAM,cAAc,mBAAmB,eAAe;AAEtD,UAAM,WAAqB;AAAA,MACzB,IAAI;AAAA,MACJ;AAAA,MACA;AAAA,MACA;AAAA,MACA,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,MAClC;AAAA,MACA,cAAc;AAAA,MACd,UAAU,CAAC;AAAA,MACX,UAAU;AAAA,MACV,gBAAgB;AAAA,IAClB;AAGA,SAAK,MAAM,aAAa,QAAQ;AAEhC,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAcA,MAAM,OACJ,UACA,UAAyB,CAAC,GACP;AACnB,UAAM,WAAW,QAAQ,WAAW;AACpC,QAAI,CAAC,YAAY,QAAQ,QAAQ,SAAS;AACxC,aAAO;AAAA,IACT;AAGA,UAAM,cAAc,oBAAI,IAA0C;AAClE,eAAW,OAAO,SAAS,cAAc;AAEvC,UAAI,IAAI,eAAe,UAAU,IAAI,eAAe,MAAO;AAC3D,UAAI,CAAC,IAAI,KAAM;AAEf,YAAM,OAAO,YAAY,IAAI,IAAI,SAAS;AAC1C,UAAI,MAAM;AACR,aAAK,KAAK,GAAG;AAAA,MACf,OAAO;AACL,oBAAY,IAAI,IAAI,WAAW,CAAC,GAAG,CAAC;AAAA,MACtC;AAAA,IACF;AAGA,UAAM,eAAe,oBAAI,IAAmC;AAC5D,UAAM,cAAyB,CAAC,GAAG,SAAS,QAAQ;AAEpD,eAAW,CAAC,WAAW,IAAI,KAAK,aAAa;AAE3C,YAAM,QAAQ,CAAC,GAAG,IAAI,IAAI,KAAK,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;AAElD,iBAAW,QAAQ,OAAO;AACxB,cAAM,aAAwD,CAAC;AAC/D,YAAI,QAAQ,OAAQ,YAAW,SAAS,QAAQ;AAChD,YAAI,QAAQ,qBAAsB,YAAW,QAAQ;AAErD,YAAI;AACJ,YAAI;AAEJ,YAAI;AACF,0BAAgB,MAAM,eAAe,WAAW,MAAM,UAAU;AAEhE,cAAI,eAAe;AAEjB,6BAAiB;AAAA,cACf,cAAc,cAAc;AAAA,cAC5B,YAAY,cAAc;AAAA,cAC1B,QAAQ,cAAc;AAAA,cACtB,UAAU;AAAA,gBACR;AAAA,kBACE,MAAM;AAAA,kBACN,QAAQ,cAAc;AAAA,kBACtB,aAAa,cAAc;AAAA,kBAC3B,QAAQ,iBAAiB,cAAc,gBAAgB,KAAK,cAAc,cAAc,WAAW,KAAK;AAAA,gBAC1G;AAAA,cACF;AAAA,YACF;AAAA,UACF,OAAO;AAEL,6BAAiB;AAAA,cACf,qBAAqB;AAAA,cACrB,UAAU;AAAA,gBACR;AAAA,kBACE,MAAM;AAAA,kBACN,QAAQ,GAAG,SAAS,iBAAiB,IAAI;AAAA,kBACzC,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,kBACpC,QAAQ;AAAA,gBACV;AAAA,cACF;AAAA,YACF;AAAA,UACF;AAAA,QACF,SAAS,KAAK;AAEZ,2BAAiB;AAAA,YACf,cAAc;AAAA,YACd,UAAU;AAAA,cACR;AAAA,gBACE,MAAM;AAAA,gBACN,QAAQ,GAAG,SAAS,iBAAiB,IAAI;AAAA,gBACzC,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,gBACpC,QAAQ,eAAe,QAAQ,IAAI,UAAU;AAAA,cAC/C;AAAA,YACF;AAAA,UACF;AAAA,QACF;AAGA,YAAI;AAEJ,YAAI;AAEF,gBAAM,UAAU,KAAK,OAAO,CAAC,MAAM,EAAE,SAAS,IAAI;AAClD,gBAAM,QAAQ,QACX,IAAI,CAAC,MAAM,EAAE,IAAI,EACjB,OAAO,CAAC,MAAmB,CAAC,CAAC,CAAC;AAEjC,cAAI,MAAM,SAAS,GAAG;AACpB,kBAAM,YAAY,MAAM,cAAc,OAAO,EAAE,QAAQ,QAAQ,OAAO,CAAC;AAEvE,kBAAM,cAAc,CAAC,GAAG,UAAU,WAAW,OAAO,CAAC,EAAE;AAAA,cACrD,CAAC,eAAe,WAAW,SAAS;AAAA,YACtC;AAEA,gBAAI,aAAa;AACf,+BAAiB;AAAA,gBACf,aAAa;AAAA,cACf;AAAA,YACF;AAAA,UACF;AAAA,QACF,QAAQ;AAAA,QAER;AAGA,mBAAW,OAAO,MAAM;AACtB,cAAI,IAAI,SAAS,KAAM;AAEvB,gBAAM,YAAY,eAAe,KAAK,gBAAgB,cAAc;AACpE,gBAAM,cAA0B;AAAA,YAC9B,GAAG,IAAI;AAAA,YACP,GAAI,gBAAgB,YAAY,CAAC;AAAA,YACjC,GAAI,gBAAgB,YAAY,CAAC;AAAA,UACnC;AAEA,uBAAa,IAAI,IAAI,IAAI;AAAA,YACvB,GAAG;AAAA,YACH,cAAc,eAAe,gBAAgB,IAAI;AAAA,YACjD,SAAS,eAAe,WAAW,IAAI;AAAA,YACvC,YAAY,eAAe,cAAc,IAAI;AAAA,YAC7C,QAAQ,eAAe,UAAU,IAAI;AAAA,YACrC,QAAQ;AAAA,YACR,UAAU;AAAA,UACZ,CAAC;AAGD,cAAI,cAAc,aAAa,cAAc,gBAAgB,cAAc,kBAAkB;AAC3F,wBAAY;AAAA,cACV,uBAAuB,IAAI,IAAI,WAAW,eAAe,OAAO;AAAA,YAClE;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAGA,UAAM,oBAAoB,SAAS,aAAa;AAAA,MAC9C,CAAC,QAAQ,aAAa,IAAI,IAAI,EAAE,KAAK;AAAA,IACvC;AAEA,WAAO;AAAA,MACL,GAAG;AAAA,MACH,cAAc;AAAA,MACd,UAAU;AAAA,IACZ;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAqBA,MAAM,SACJ,UACA,UASI,CAAC,GACc;AACnB,QAAI,CAAC,QAAQ,cAAc,QAAQ,QAAQ,QAAS,QAAO;AAE3D,UAAM,aAAa,iBAAiB,SAAS,cAAc;AAAA,MACzD,OAAO,QAAQ;AAAA,IACjB,CAAC;AACD,QAAI,WAAW,WAAW,EAAG,QAAO;AAEpC,YAAQ,aAAa,eAAe,GAAG,WAAW,MAAM;AAExD,QAAI;AACJ,QAAI;AACF,iBAAW,MAAM,QAAQ,WAAW,SAAS,YAAY;AAAA,QACvD,QAAQ,QAAQ;AAAA,QAChB,YAAY,CAAC,WAAW,UAAU;AAEhC,kBAAQ,aAAa,eAAe,WAAW,KAAK;AAAA,QACtD;AAAA,MACF,CAAC;AAAA,IACH,QAAQ;AAEN,aAAO;AAAA,IACT;AAEA,YAAQ,aAAa,gBAAgB,GAAG,CAAC;AACzC,QAAI,SAAS,WAAW,EAAG,QAAO;AAIlC,WAAO,EAAE,GAAG,UAAU,UAAU,CAAC,GAAG,SAAS,UAAU,GAAG,QAAQ,EAAE;AAAA,EACtE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,QACJ,WACA,SACoD;AACpD,UAAM,QAAQ,QAAQ,SAAS,WAAW;AAC1C,UAAM,cAAc,QAAQ,eAAe;AAG3C,UAAM,MAAoB;AAAA,MACxB,IAAI;AAAA,MACJ;AAAA,MACA,YAAY,QAAQ;AAAA,MACpB,MAAM;AAAA,MACN,QAAQ;AAAA,MACR,aAAa;AAAA,MACb;AAAA,MACA,WAAW,QAAQ;AAAA,MACnB,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,MAClC,UAAU,EAAE,OAAO,UAAU,WAAW,GAAG,OAAO,EAAE;AAAA,IACtD;AACA,SAAK,MAAM,QAAQ,GAAG;AAEtB,UAAM,YAAY,CAAC,QAA4B,aAAoC;AACjF,WAAK,MAAM,gBAAgB,OAAO,QAAQ,QAAQ;AAClD,UAAI,SAAU,SAAQ,aAAa,SAAS,OAAO,SAAS,WAAW,SAAS,KAAK;AAAA,IACvF;AACA,UAAM,iBAAiB,MAAY;AACjC,UAAI,QAAQ,QAAQ,QAAS,OAAM,IAAI,aAAa,2BAA2B,YAAY;AAAA,IAC7F;AAEA,QAAI;AACF,qBAAe;AAEf,gBAAU,eAAe,EAAE,OAAO,eAAe,WAAW,GAAG,OAAO,EAAE,CAAC;AACzE,YAAM,WAAW,MAAM,KAAK;AAAA,QAC1B;AAAA,QACA,QAAQ;AAAA,QACR;AAAA,QACA,CAAC,OAAO,WAAW,UAAU;AAC3B,yBAAe;AACf,oBAAU,OAA6B,EAAE,OAAO,WAAW,MAAM,CAAC;AAAA,QACpE;AAAA,MACF;AACA,qBAAe;AAGf,YAAM,WAAW,QAAQ,WAAW;AACpC,UAAI,UAAU;AACZ,kBAAU,aAAa,EAAE,OAAO,aAAa,WAAW,GAAG,OAAO,EAAE,CAAC;AACrE,cAAM,WAAW,MAAM,KAAK,OAAO,UAAU;AAAA,UAC3C,QAAQ;AAAA,UACR,QAAQ,QAAQ;AAAA,QAClB,CAAC;AACD,uBAAe;AAGf,cAAM,aAAa,MAAM,KAAK,SAAS,UAAU;AAAA,UAC/C,YAAY,QAAQ;AAAA,UACpB,eAAe,QAAQ;AAAA,UACvB,QAAQ,QAAQ;AAAA,UAChB,YAAY,CAAC,OAAO,WAAW,UAAU;AACvC,sBAAU,OAAO,EAAE,OAAO,WAAW,MAAM,CAAC;AAAA,UAC9C;AAAA,QACF,CAAC;AACD,uBAAe;AAGf,aAAK,MAAM,aAAa,UAAU;AAElC,kBAAU,aAAa,EAAE,OAAO,aAAa,WAAW,GAAG,OAAO,EAAE,CAAC;AAErE,eAAO;AAAA,UACL,UAAU;AAAA,UACV,KAAK,EAAE,GAAG,KAAK,QAAQ,aAAa,cAAa,oBAAI,KAAK,GAAE,YAAY,EAAE;AAAA,QAC5E;AAAA,MACF;AAGA,WAAK,MAAM,aAAa,QAAQ;AAChC,gBAAU,aAAa,EAAE,OAAO,aAAa,WAAW,GAAG,OAAO,EAAE,CAAC;AAErE,aAAO;AAAA,QACL;AAAA,QACA,KAAK,EAAE,GAAG,KAAK,QAAQ,aAAa,cAAa,oBAAI,KAAK,GAAE,YAAY,EAAE;AAAA,MAC5E;AAAA,IACF,SAAS,KAAK;AACZ,UAAI,QAAQ,QAAQ,WAAY,eAAe,gBAAgB,IAAI,SAAS,cAAe;AACzF,kBAAU,WAAW;AAAA,MACvB,OAAO;AACL,kBAAU,QAAQ;AAAA,MACpB;AACA,YAAM;AAAA,IACR;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,eAAe,UAAoB,SAAwB,MAAc;AACvE,QAAI,WAAW,OAAQ,QAAO,KAAK,UAAU,UAAU,MAAM,CAAC;AAE9D,UAAM,QAAkB;AAAA,MACtB;AAAA,MACA;AAAA,MACA,kBAAkB,SAAS,SAAS;AAAA,MACpC,eAAe,SAAS,UAAU;AAAA,MAClC,oBAAoB,SAAS,WAAW;AAAA,MACxC,mBAAmB,SAAS,WAAW,MAAM;AAAA,MAC7C,qBAAqB,SAAS,aAAa,MAAM;AAAA,MACjD,iBAAiB,SAAS,SAAS,MAAM;AAAA,MACzC,iBAAiB,SAAS,QAAQ;AAAA,MAClC;AAAA,IACF;AAGA,QAAI,SAAS,WAAW,SAAS,GAAG;AAClC,YAAM,KAAK,iBAAiB,EAAE;AAC9B,YAAM,KAAK,6CAA6C;AACxD,YAAM,KAAK,mBAAmB;AAC9B,iBAAW,MAAM,SAAS,YAAY;AACpC,cAAM,WAAW,SAAS,aAAa,OAAO,CAAC,MAAM,EAAE,gBAAgB,GAAG,EAAE,EAAE;AAC9E,cAAM,KAAK,KAAK,GAAG,YAAY,MAAM,GAAG,SAAS,MAAM,GAAG,QAAQ,MAAM,QAAQ,IAAI;AAAA,MACtF;AACA,YAAM,KAAK,EAAE;AAAA,IACf;AAGA,UAAM,WAAW,SAAS;AAQ1B,QAAI,SAAS,SAAS,GAAG;AACvB,YAAM,KAAK,eAAe,EAAE;AAC5B,YAAM,aAAa,oBAAI,IAA8C;AACrE,iBAAW,KAAK,UAAU;AACxB,cAAM,OAAO,WAAW,IAAI,EAAE,QAAQ,KAAK,CAAC;AAC5C,aAAK,KAAK,CAAC;AACX,mBAAW,IAAI,EAAE,UAAU,IAAI;AAAA,MACjC;AACA,iBAAW,OAAO,CAAC,YAAY,QAAQ,UAAU,OAAO,MAAM,GAAG;AAC/D,cAAM,QAAQ,WAAW,IAAI,GAAG;AAChC,YAAI,CAAC,SAAS,MAAM,WAAW,EAAG;AAClC,cAAM,KAAK,OAAO,IAAI,OAAO,CAAC,EAAE,YAAY,IAAI,IAAI,MAAM,CAAC,CAAC,KAAK,MAAM,MAAM,KAAK,EAAE;AACpF,mBAAW,KAAK,OAAO;AACrB,gBAAM,MAAM,SAAS,aAAa,KAAK,CAAC,MAAM,EAAE,OAAO,EAAE,YAAY;AACrE,gBAAM,KAAK,OAAO,KAAK,QAAQ,EAAE,YAAY,aAAQ,EAAE,IAAI,WAAM,EAAE,SAAS,EAAE;AAAA,QAChF;AACA,cAAM,KAAK,EAAE;AAAA,MACf;AAAA,IACF;AAGA,QAAI,SAAS,aAAa,SAAS,GAAG;AACpC,YAAM,KAAK,mBAAmB,EAAE;AAChC,YAAM,KAAK,iDAAiD;AAC5D,YAAM,KAAK,uBAAuB;AAClC,iBAAW,OAAO,SAAS,cAAc;AACvC,cAAM;AAAA,UACJ,KAAK,IAAI,IAAI,MAAM,IAAI,SAAS,MAAM,IAAI,MAAM,MAAM,IAAI,UAAU,QAAG,MAAM,IAAI,gBAAgB,QAAG;AAAA,QACtG;AAAA,MACF;AAAA,IACF;AAEA,WAAO,MAAM,KAAK,IAAI;AAAA,EACxB;AACF;AAIA,SAAS,mBAAmB,cAAwD;AAClF,QAAM,QAAQ,aACX,IAAI,CAAC,MAAM,GAAG,EAAE,IAAI,IAAI,EAAE,UAAU,EAAE,aAAa,SAAS,EAAE,EAC9D,KAAK,EACL,KAAK,GAAG;AACX,MAAI,OAAO;AACX,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,UAAM,OAAO,MAAM,WAAW,CAAC;AAC/B,YAAS,QAAQ,KAAK,OAAQ;AAC9B,YAAQ;AAAA,EACV;AACA,SAAO,MAAM,KAAK,IAAI,IAAI,EAAE,SAAS,EAAE,CAAC;AAC1C;AAEA,SAAS,uBACP,cACA,QACA,UACS;AACT,UAAQ,QAAQ;AAAA,IACd,KAAK;AACH,aAAO;AAAA,QACL,IAAI,WAAW,YAAY;AAAA,QAC3B;AAAA,QACA,MAAM;AAAA,QACN,UAAU;AAAA,QACV,QAAQ;AAAA,QACR,YAAY;AAAA,QACZ,WAAW;AAAA,QACX,UAAU,CAAC;AAAA,MACb;AAAA,IACF,KAAK;AACH,aAAO;AAAA,QACL,IAAI,WAAW,YAAY;AAAA,QAC3B;AAAA,QACA,MAAM;AAAA,QACN,UAAU;AAAA,QACV,QAAQ;AAAA,QACR,YAAY;AAAA,QACZ,WAAW;AAAA,QACX,UAAU,CAAC;AAAA,MACb;AAAA,IACF,KAAK;AACH,aAAO;AAAA,QACL,IAAI,WAAW,YAAY;AAAA,QAC3B;AAAA,QACA,MAAM;AAAA,QACN,UAAU;AAAA,QACV,QAAQ;AAAA,QACR,YAAY;AAAA,QACZ,WAAW;AAAA,QACX,UAAU,CAAC;AAAA,MACb;AAAA,IACF,KAAK;AACH,aAAO;AAAA,QACL,IAAI,WAAW,YAAY;AAAA,QAC3B;AAAA,QACA,MAAM;AAAA,QACN,UAAU;AAAA,QACV,QAAQ;AAAA,QACR,YAAY;AAAA,QACZ,WAAW;AAAA,QACX,UAAU,CAAC;AAAA,MACb;AAAA,IACF,KAAK;AACH,aAAO;AAAA,QACL,IAAI,WAAW,YAAY;AAAA,QAC3B;AAAA,QACA,MAAM;AAAA,QACN,UAAU;AAAA,QACV,QAAQ;AAAA,QACR,YAAY;AAAA,QACZ,WAAW;AAAA,QACX,UAAU,CAAC;AAAA,MACb;AAAA,IACF;AACE,aAAO;AAAA,QACL,IAAI,WAAW,YAAY;AAAA,QAC3B;AAAA,QACA,MAAM;AAAA,QACN,UAAU;AAAA,QACV,QAAQ;AAAA,QACR,YAAY;AAAA,QACZ,WAAW,sBAAsB,MAAM;AAAA,QACvC,UAAU,CAAC;AAAA,MACb;AAAA,EACJ;AACF;;;AE/qBA,IAAM,qBAAqB;AAOpB,SAAS,kBACd,UACA,UAAuD,CAAC,GAC/B;AACzB,MAAI,CAAC,SAAS,EAAG,QAAO;AACxB,QAAM,YAAY,QAAQ,aAAa;AAEvC,SAAO,OAAO,QAA6C;AAEzD,UAAM,MAAM,SAAS;AACrB,QAAI,CAAC,IAAK,OAAM,IAAI,MAAM,2CAA2C;AAErE,UAAM,UAAmB;AAAA,MACvB,OAAO,IAAI;AAAA,MACX,QAAQ,CAAC,EAAE,MAAM,QAAQ,MAAM,IAAI,OAAO,CAAC;AAAA,MAC3C,UAAU,CAAC,EAAE,MAAM,QAAQ,SAAS,IAAI,OAAO,CAAC;AAAA,MAChD,WAAW,IAAI;AAAA,IACjB;AAEA,QAAI,IAAI,SAAS,aAAa,kBAAkB;AAC9C,cAAQ,iBAAiB;AAAA,QACvB,MAAM;AAAA,QACN,YAAY,EAAE,MAAM,IAAI,YAAY,QAAQ,OAAO,QAAQ,IAAI,OAAO;AAAA,MACxE;AAAA,IACF,WAAW,IAAI,SAAS,aAAa,UAAU;AAC7C,cAAQ,iBAAiB,EAAE,MAAM,cAAc;AAAA,IACjD;AAEA,UAAM,QAAQ,IAAI,gBAAgB;AAClC,UAAM,UAAU,MAAM;AACpB,YAAM,MAAM,IAAI,MAAM,+BAA+B,CAAC;AAAA,IACxD;AACA,QAAI,QAAQ,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;AAC7D,UAAM,KAAK,WAAW,MAAM;AAC1B,YAAM,MAAM,IAAI,MAAM,iCAAiC,CAAC;AAAA,IAC1D,GAAG,SAAS;AACZ,OAAG,QAAQ;AAEX,QAAI;AACF,YAAM,MAAM,MAAM,IAAI,SAAS,SAAS,SAAS,EAAE,QAAQ,MAAM,OAAO,CAAC;AACzE,aAAO,IAAI,QACR,OAAO,CAAC,UAAU,MAAM,SAAS,MAAM,EACvC,IAAI,CAAC,UAAU,MAAM,IAAI,EACzB,KAAK,IAAI,EACT,KAAK;AAAA,IACV,UAAE;AACA,UAAI,QAAQ,oBAAoB,SAAS,OAAO;AAChD,mBAAa,EAAE;AACf,YAAM,MAAM;AAAA,IACd;AAAA,EACF;AACF;AAKA,SAAS,gBAAgB,MAAsB;AAC7C,QAAM,UAAU,KAAK,KAAK;AAC1B,QAAM,QAAQ,QAAQ,MAAM,kDAAkD;AAC9E,UAAQ,QAAQ,CAAC,KAAK,SAAS,KAAK;AACtC;AASA,SAAS,kBAAkB,MAAsB;AAC/C,QAAM,UAAU,gBAAgB,IAAI;AACpC,MAAI,QAAQ,WAAW,GAAG,EAAG,QAAO;AACpC,QAAM,QAAQ,QAAQ,QAAQ,GAAG;AACjC,QAAM,MAAM,QAAQ,YAAY,GAAG;AACnC,MAAI,UAAU,MAAM,MAAM,MAAO,QAAO,QAAQ,MAAM,OAAO,MAAM,CAAC;AACpE,SAAO;AACT;AASO,SAAS,kBAAkB,MAA8C;AAC9E,MAAI,CAAC,KAAK,KAAK,EAAG,QAAO;AACzB,MAAI;AACF,UAAM,SAAkB,KAAK,MAAM,kBAAkB,IAAI,CAAC;AAC1D,WAAO,WAAW,QAAQ,OAAO,WAAW,YAAY,CAAC,MAAM,QAAQ,MAAM,IACxE,SACD;AAAA,EACN,QAAQ;AACN,WAAO;AAAA,EACT;AACF;;;AC5FA,IAAM,qBAAqB;AAC3B,IAAM,oBAAoB;AAC1B,IAAM,yBAAyB;AAO/B,IAAM,qBAAqB;AAC3B,IAAM,qBAAqB;AAC3B,IAAM,yBAAyB;AAE/B,IAAM,0BAAoE;AAAA,EACxE,iBAAiB;AAAA,EACjB,aAAa;AAAA,EACb,eAAe;AACjB;AAEA,IAAM,mBAAwC,oBAAI,IAAI;AAAA,EACpD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAED,IAAM,gBAAqC,oBAAI,IAAI;AAAA,EACjD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAID,IAAM,eAAe;AAAA,EACnB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,EAAE,KAAK,IAAI;AAEX,IAAM,oBAAqD;AAAA,EACzD,iBAAiB;AAAA,IACf;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,EAAE,KAAK,IAAI;AAAA,EACX,aAAa;AAAA,IACX;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,EAAE,KAAK,IAAI;AAAA,EACX,eAAe;AAAA,IACb;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,EAAE,KAAK,IAAI;AACb;AAEA,IAAM,sBAAuD;AAAA,EAC3D,iBAAiB;AAAA,EACjB,aAAa;AAAA,EACb,eAAe;AACjB;AAEA,IAAM,uBAAgD;AAAA,EACpD,MAAM;AAAA,EACN,sBAAsB;AAAA,EACtB,YAAY;AAAA,IACV,UAAU;AAAA,MACR,MAAM;AAAA,MACN,OAAO;AAAA,QACL,MAAM;AAAA,QACN,sBAAsB;AAAA,QACtB,YAAY;AAAA,UACV,SAAS,EAAE,MAAM,UAAU,aAAa,0CAA0C;AAAA,UAClF,UAAU,EAAE,MAAM,UAAU,MAAM,CAAC,QAAQ,OAAO,UAAU,QAAQ,UAAU,EAAE;AAAA,UAChF,QAAQ;AAAA,YACN,MAAM;AAAA,YACN,MAAM;AAAA,cACJ;AAAA,cACA;AAAA,cACA;AAAA,cACA;AAAA,cACA;AAAA,cACA;AAAA,cACA;AAAA,YACF;AAAA,UACF;AAAA,UACA,YAAY,EAAE,MAAM,UAAU,SAAS,GAAG,SAAS,EAAE;AAAA,UACrD,WAAW,EAAE,MAAM,SAAS;AAAA,UAC5B,cAAc,EAAE,MAAM,SAAS;AAAA,UAC/B,SAAS,EAAE,MAAM,SAAS,OAAO,EAAE,MAAM,SAAS,EAAE;AAAA,QACtD;AAAA,QACA,UAAU,CAAC,WAAW,YAAY,UAAU,cAAc,WAAW;AAAA,MACvE;AAAA,IACF;AAAA,EACF;AAAA,EACA,UAAU,CAAC,UAAU;AACvB;AAIA,SAAS,YAAY,WAAoC;AACvD,QAAM,MAAM,UAAU;AACtB,QAAM,OAAO,IAAI,UAAU,IAAI,aAAa;AAC5C,QAAM,KAAK,IAAI,gBAAgB;AAC/B,UAAQ,UAAU,SAAS;AAAA,IACzB,KAAK;AACH,aAAO,GAAG,IAAI,IAAI,IAAI,IAAI,OAAO,EAAE;AAAA,IACrC,KAAK;AACH,aAAO,GAAG,IAAI,IAAI,IAAI,IAAI,SAAS;AAAA,IACrC,KAAK;AACH,aAAO,GAAG,IAAI,IAAI,IAAI,IAAI;AAAA,EAC9B;AACF;AAGA,eAAe,SACb,OACA,OACA,MACc;AACd,QAAM,MAAM,IAAI,MAAS,MAAM,MAAM;AACrC,MAAI,SAAS;AACb,QAAM,UAAU,MAAM,KAAK,EAAE,QAAQ,KAAK,IAAI,OAAO,MAAM,MAAM,EAAE,GAAG,YAAY;AAChF,eAAS;AACP,YAAM,QAAQ;AACd,UAAI,SAAS,MAAM,OAAQ;AAC3B,UAAI,KAAK,IAAI,MAAM,KAAK,MAAM,KAAK,GAAI,KAAK;AAAA,IAC9C;AAAA,EACF,CAAC;AACD,QAAM,QAAQ,IAAI,OAAO;AACzB,SAAO;AACT;AAIA,SAAS,mBAAmB,KAAoC;AAC9D,QAAM,OAAO;AAAA,IACX,cAAc,IAAI,SAAS;AAAA,IAC3B,cAAc,IAAI,UAAU,IAAI,aAAa,IAAI,aAAa,SAAS;AAAA,EACzE;AACA,MAAI,IAAI,aAAc,MAAK,KAAK,kBAAkB,IAAI,YAAY,EAAE;AACpE,MAAI,IAAI,UAAW,MAAK,KAAK,eAAe,IAAI,SAAS,EAAE;AAC3D,OAAK,KAAK,IAAI,SAAS,sBAAsB,uBAAuB;AACpE,OAAK,KAAK,UAAU,IAAI,KAAK,EAAE;AAC/B,MAAI,IAAI,QAAS,MAAK,KAAK,YAAY,IAAI,OAAO,EAAE;AACpD,MAAI,IAAI,WAAY,MAAK,KAAK,2BAA2B;AACzD,MAAI,IAAI,OAAQ,MAAK,KAAK,uBAAuB;AACjD,OAAK,KAAK,WAAW,IAAI,MAAM,EAAE;AACjC,SAAO,KAAK,KAAK,IAAI;AACvB;AAEA,SAAS,cAAc,SAAkD;AACvE,MAAI,QAAQ,WAAW,EAAG,QAAO;AACjC,SAAO,QACJ,IAAI,CAAC,MAAM,OAAO,EAAE,KAAK;AAAA,MAAS,EAAE,GAAG;AAAA,MAAS,SAAS,EAAE,SAAS,iBAAiB,CAAC,EAAE,EACxF,KAAK,IAAI;AACd;AAEA,SAAS,SAAS,OAAe,KAAqB;AACpD,QAAM,QAAQ,MAAM,QAAQ,QAAQ,GAAG,EAAE,KAAK;AAC9C,SAAO,MAAM,UAAU,MAAM,QAAQ,GAAG,MAAM,MAAM,GAAG,GAAG,CAAC;AAC7D;AAEA,SAAS,YACP,SACA,SACQ;AACR,QAAM,SAAS,QAAQ;AAAA,IAAI,CAAC,EAAE,WAAW,QAAQ,GAAG,MAClD;AAAA,MACE,GAAG,IAAI,CAAC,KAAK,UAAU,WAAW,IAAI;AAAA,MACtC,MAAM,mBAAmB,UAAU,UAAU,CAAC;AAAA,MAC9C;AAAA,MACA,cAAc,OAAO;AAAA,IACvB,EAAE,KAAK,IAAI;AAAA,EACb;AAEA,SAAO;AAAA,IACL,qCAAqC,oBAAoB,OAAO,CAAC;AAAA,IACjE;AAAA,IACA;AAAA,IACA,GAAG;AAAA,IACH;AAAA,IACA;AAAA,IACA;AAAA,IAGA;AAAA,IACA;AAAA,EACF,EAAE,KAAK,IAAI;AACb;AAIA,SAAS,eAAe,OAAoC;AAC1D,SAAO,OAAO,UAAU,YAAY,MAAM,KAAK,IAAI,MAAM,KAAK,IAAI;AACpE;AAEA,SAAS,gBAAgB,OAAwB;AAC/C,MAAI,OAAO,UAAU,YAAY,CAAC,OAAO,SAAS,KAAK,EAAG,QAAO;AACjE,SAAO,KAAK,IAAI,oBAAoB,KAAK,IAAI,oBAAoB,KAAK,CAAC;AACzE;AAEA,SAAS,eACP,KACA,SACA,aACY;AACZ,QAAM,QAAQ,IAAI,IAAI,QAAQ,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC;AAC/C,QAAM,QAAQ,MAAM,QAAQ,GAAG,IAC3B,IAAI,OAAO,CAAC,MAAmB,OAAO,MAAM,YAAY,MAAM,IAAI,CAAC,CAAC,IACpE,CAAC;AAGL,QAAM,OAAO,MAAM,SAAS,IAAI,QAAQ,QAAQ,IAAI,CAAC,MAAM,EAAE,GAAG;AAChE,SAAO,KAAK,IAAI,CAAC,SAAS;AAAA,IACxB,MAAM;AAAA,IACN,QAAQ;AAAA,IACR;AAAA,IACA,QAAQ,QAAQ,KAAK,CAAC,MAAM,EAAE,QAAQ,GAAG,GAAG;AAAA,EAC9C,EAAE;AACJ;AAEA,SAAS,UACP,KACA,SACA,QACA,aACgB;AAChB,QAAM,OAAO,eAAe,IAAI,OAAO;AACvC,MAAI,CAAC,KAAM,QAAO;AAIlB,QAAM,QAAQ,OAAO,IAAI,IAAI;AAC7B,MAAI,CAAC,MAAO,QAAO;AAEnB,QAAM,YAAY,eAAe,IAAI,SAAS;AAC9C,MAAI,CAAC,UAAW,QAAO;AAEvB,QAAM,WAAW,iBAAiB,IAAI,IAAI,QAAkB,IACvD,IAAI,WACL;AACJ,QAAM,SAAS,cAAc,IAAI,IAAI,MAAgB,IAChD,IAAI,SACL;AAEJ,QAAM,eAAe,eAAe,IAAI,YAAY;AAEpD,SAAO;AAAA,IACL,IAAI,YAAY,MAAM,UAAU,WAAW,EAAE,IAAI,OAAO;AAAA,IACxD,cAAc,MAAM,UAAU,WAAW;AAAA,IACzC,MAAM,wBAAwB,OAAO;AAAA,IACrC;AAAA,IACA;AAAA,IACA,YAAY,gBAAgB,IAAI,UAAU;AAAA,IAC1C;AAAA,IACA,GAAI,eAAe,EAAE,aAAa,IAAI,CAAC;AAAA,IACvC,UAAU,eAAe,IAAI,SAAS,MAAM,SAAS,WAAW;AAAA,EAClE;AACF;AAEA,SAAS,cACP,QACA,SACA,QACA,aACW;AACX,MAAI,CAAC,UAAU,CAAC,MAAM,QAAQ,OAAO,QAAQ,EAAG,QAAO,CAAC;AACxD,QAAM,OAAO,oBAAI,IAAY;AAC7B,QAAM,MAAiB,CAAC;AACxB,aAAW,OAAO,OAAO,UAAU;AACjC,QAAI,CAAC,OAAO,OAAO,QAAQ,YAAY,MAAM,QAAQ,GAAG,EAAG;AAC3D,UAAM,UAAU,UAAU,KAAgC,SAAS,QAAQ,WAAW;AACtF,QAAI,CAAC,WAAW,KAAK,IAAI,QAAQ,EAAE,EAAG;AACtC,SAAK,IAAI,QAAQ,EAAE;AACnB,QAAI,KAAK,OAAO;AAAA,EAClB;AACA,SAAO;AACT;AAmBO,SAAS,iBAAiB,SAAuD;AACtF,QAAM,MAAM,QAAQ,QAAQ,MAAM,oBAAI,KAAK;AAE3C,SAAO;AAAA,IACL,MAAM,SAAS,YAAY,OAAwB,CAAC,GAAgC;AAClF,UAAI,WAAW,WAAW,EAAG,QAAO,CAAC;AAErC,YAAM,WAAW,CAAC,GAAG,kBAAkB,UAAU,EAAE,QAAQ,CAAC;AAC5D,YAAM,WAAsB,CAAC;AAC7B,UAAI,YAAY;AAChB,WAAK,aAAa,GAAG,SAAS,MAAM;AAEpC,iBAAW,CAAC,SAAS,OAAO,KAAK,UAAU;AACzC,YAAI,KAAK,QAAQ,QAAS;AAE1B,YAAI;AACF,gBAAM,UAAU,MAAM;AAAA,YAAS;AAAA,YAAS;AAAA,YAAoB,CAAC,cAC3D,QAAQ,OAAO,YAAY,SAAS,GAAG,EAAE,QAAQ,KAAK,OAAO,CAAC;AAAA,UAChE;AACA,cAAI,KAAK,QAAQ,QAAS;AAE1B,gBAAM,UAAU,QAAQ,IAAI,CAAC,WAAW,OAAO;AAAA,YAC7C;AAAA,YACA,SAAS,QAAQ,CAAC,KAAK,CAAC;AAAA,UAC1B,EAAE;AACF,gBAAM,SAAS,IAAI,IAAI,QAAQ,IAAI,CAAC,UAAU,CAAC,MAAM,UAAU,WAAW,MAAM,KAAK,CAAC,CAAC;AAEvF,gBAAM,OAAO,MAAM,QAAQ,IAAI;AAAA,YAC7B,QAAQ,kBAAkB,OAAO;AAAA,YACjC,QAAQ,YAAY,SAAS,OAAO;AAAA,YACpC,QAAQ;AAAA,YACR,YAAY,aAAa,OAAO;AAAA,YAChC,WAAW;AAAA,YACX,QAAQ,KAAK;AAAA,UACf,CAAC;AAED,mBAAS;AAAA,YACP,GAAG,cAAc,kBAAkB,IAAI,GAAG,SAAS,QAAQ,IAAI,EAAE,YAAY,CAAC;AAAA,UAChF;AAAA,QACF,QAAQ;AAAA,QAGR;AAEA;AACA,aAAK,aAAa,WAAW,SAAS,MAAM;AAAA,MAC9C;AAEA,aAAO;AAAA,IACT;AAAA,EACF;AACF;;;ACzXA,SAAS,kBAAkB;AAG3B,IAAM,sBAAsB;AAS5B,IAAM,aAAa;AAQZ,SAAS,iBAAiB,UAA6B,CAAC,GAAmB;AAChF,QAAM,aAAa,QAAQ,cAAc;AAEzC,SAAO,OAAO,OAAO,SAAmD;AACtE,QAAI,KAAK,QAAQ,QAAS,QAAO,CAAC;AAClC,QAAI;AACF,YAAM,MAAM,MAAM,WAAW;AAAA,QAC3B;AAAA,UACE;AAAA,UACA,aAAa;AAAA,UACb,GAAI,QAAQ,SAAS,EAAE,QAAQ,QAAQ,OAAO,IAAI,CAAC;AAAA,QACrD;AAAA,QACA;AAAA,QACA,EAAE,QAAQ,KAAK,UAAU,IAAI,gBAAgB,EAAE,OAAO;AAAA,MACxD;AACA,aAAO,IAAI,QAAQ,IAAI,CAAC,YAAY;AAAA,QAClC,OAAO,OAAO;AAAA,QACd,KAAK,OAAO;AAAA,QACZ,SAAS,OAAO;AAAA,MAClB,EAAE;AAAA,IACJ,QAAQ;AAIN,aAAO,CAAC;AAAA,IACV;AAAA,EACF;AACF;;;ACjDA,SAAS,oBAAoB;AAC7B,SAAS,WAAW,cAAAC,mBAAkB;AACtC,SAAS,QAAAC,aAAY;AACrB,SAAS,eAAe;;;ACAjB,IAAM,iBAAiB;AAEvB,IAAM,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAsDZ,SAAS,YAAY,IAA8C;AAExE,aAAW,aAAa,IAAI,MAAM,GAAG,GAAG;AACtC,UAAM,UAAU,UAAU,KAAK;AAC/B,QAAI,SAAS;AACX,SAAG,KAAK,OAAO;AAAA,IACjB;AAAA,EACF;AAGA,QAAM,MAAM,GAAG,QAAQ,8CAA8C,EAAE,IAAI;AAI3E,MAAI,CAAC,KAAK;AACR,OAAG,QAAQ,2DAA2D,EAAE,IAAI,cAAc;AAAA,EAC5F,WAAW,IAAI,UAAU,gBAAgB;AAEvC,OAAG,QAAQ,iDAAiD,EAAE,IAAI,cAAc;AAAA,EAClF;AACF;;;ADtDO,IAAM,iBAAN,MAAqB;AAAA,EAClB;AAAA,EACA;AAAA,EAER,YAAY,SAAuB;AACjC,SAAK,SACH,QAAQ,UACRC;AAAA,MACE,QAAQ;AAAA,MACR;AAAA,MACA;AAAA,MACA,QAAQ;AAAA,MACR;AAAA,MACA;AAAA,IACF;AAGF,UAAM,MAAM,KAAK,OAAO,MAAM,GAAG,KAAK,OAAO,YAAY,IAAI,CAAC;AAC9D,QAAI,CAACC,YAAW,GAAG,GAAG;AACpB,gBAAU,KAAK,EAAE,WAAW,KAAK,CAAC;AAAA,IACpC;AAEA,SAAK,KAAK,IAAI,aAAa,KAAK,MAAM;AACtC,SAAK,GAAG,KAAK,4BAA4B;AACzC,SAAK,GAAG,KAAK,2BAA2B;AACxC,gBAAY,KAAK,EAAE;AAAA,EACrB;AAAA;AAAA;AAAA,EAKA,QAAc;AACZ,QAAI;AACF,WAAK,GAAG,MAAM;AAAA,IAChB,QAAQ;AAAA,IAER;AAAA,EACF;AAAA;AAAA,EAGA,IAAI,OAAe;AACjB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA,EAKA,aAAa,UAA0B;AACrC,UAAM,OAAO,KAAK,GAAG,QAAQ;AAAA;AAAA;AAAA,KAG5B;AACD,SAAK;AAAA,MACH,SAAS;AAAA,MACT,SAAS;AAAA,MACT,SAAS;AAAA,MACT,SAAS;AAAA,MACT,SAAS;AAAA,MACT,KAAK,UAAU,QAAQ;AAAA,MACvB,SAAS;AAAA,IACX;AAAA,EACF;AAAA;AAAA,EAGA,YAAY,WAAyC;AACnD,UAAM,OAAO,KAAK,GAAG,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA,KAK5B;AACD,UAAM,MAAM,KAAK,IAAI,SAAS;AAC9B,QAAI,CAAC,IAAK,QAAO;AACjB,QAAI;AACF,aAAO,KAAK,MAAM,IAAI,QAAQ;AAAA,IAChC,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA;AAAA,EAGA,gBAAgB,IAAkC;AAChD,UAAM,OAAO,KAAK,GAAG,QAAQ;AAAA;AAAA,KAE5B;AACD,UAAM,MAAM,KAAK,IAAI,EAAE;AACvB,QAAI,CAAC,IAAK,QAAO;AACjB,QAAI;AACF,aAAO,KAAK,MAAM,IAAI,QAAQ;AAAA,IAChC,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA;AAAA,EAGA,cAAc,WAAmB,QAAQ,IAAgB;AACvD,UAAM,OAAO,KAAK,GAAG,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA,KAK5B;AACD,UAAM,OAAO,KAAK,IAAI,WAAW,KAAK;AACtC,WAAO,KACJ,IAAI,CAAC,MAAM;AACV,UAAI;AACF,eAAO,KAAK,MAAM,EAAE,QAAQ;AAAA,MAC9B,QAAQ;AACN,eAAO;AAAA,MACT;AAAA,IACF,CAAC,EACA,OAAO,CAAC,MAAqB,MAAM,MAAS;AAAA,EACjD;AAAA;AAAA,EAGA,sBAAsB,WAAmB,QAAwB;AAC/D,UAAM,OAAO,KAAK,GAAG,QAAQ;AAAA;AAAA,KAE5B;AACD,UAAM,SAAS,KAAK,IAAI,WAAW,MAAM;AACzC,WAAO,OAAO,OAAO,OAAO;AAAA,EAC9B;AAAA;AAAA;AAAA,EAKA,QAAQ,KAAyB;AAC/B,UAAM,OAAO,KAAK,GAAG,QAAQ;AAAA;AAAA;AAAA;AAAA,KAI5B;AACD,SAAK;AAAA,MACH,IAAI;AAAA,MACJ,IAAI;AAAA,MACJ,IAAI;AAAA,MACJ,IAAI;AAAA,MACJ,IAAI;AAAA,MACJ,IAAI;AAAA,MACJ,IAAI;AAAA,MACJ,IAAI,aAAa;AAAA,MACjB,IAAI;AAAA,MACJ,IAAI,eAAe;AAAA,MACnB,IAAI,SAAS;AAAA,MACb,IAAI,WAAW,KAAK,UAAU,IAAI,QAAQ,IAAI;AAAA,IAChD;AAAA,EACF;AAAA;AAAA,EAGA,OAAO,IAAsC;AAC3C,UAAM,OAAO,KAAK,GAAG,QAAQ;AAAA;AAAA,KAE5B;AACD,UAAM,MAAM,KAAK,IAAI,EAAE;AACvB,QAAI,CAAC,IAAK,QAAO;AACjB,WAAO,KAAK,SAAS,GAAG;AAAA,EAC1B;AAAA;AAAA,EAGA,gBAAgB,IAAY,QAA4B,UAAuC;AAC7F,UAAM,eAAe,WAAW,KAAK,UAAU,QAAQ,IAAI;AAC3D,UAAM,cAAc,WAAW,eAAe,WAAW,YAAY,WAAW,eAC5E,oBAAI,KAAK,GAAE,YAAY,IACvB;AAEJ,UAAM,OAAO,KAAK,GAAG,QAAQ;AAAA;AAAA;AAAA;AAAA,KAI5B;AACD,SAAK,IAAI,QAAQ,cAAc,aAAa,EAAE;AAAA,EAChD;AAAA;AAAA,EAGA,SAAS,WAAmB,QAAQ,IAAoB;AACtD,UAAM,OAAO,KAAK,GAAG,QAAQ;AAAA;AAAA,KAE5B;AACD,UAAM,OAAO,KAAK,IAAI,WAAW,KAAK;AACtC,WAAO,KAAK,IAAI,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC;AAAA,EACzC;AAAA;AAAA;AAAA,EAKA,aAAa,YAAoB,UAAkB,WAAyB;AAC1E,UAAM,OAAO,KAAK,GAAG,QAAQ;AAAA;AAAA;AAAA,KAG5B;AACD,SAAK,IAAI,YAAY,UAAU,SAAS;AAAA,EAC1C;AAAA;AAAA,EAGA,YAAY,YAAoB,WAA4B;AAC1D,UAAM,OAAO,KAAK,GAAG,QAAQ;AAAA;AAAA;AAAA;AAAA,KAI5B;AACD,UAAM,SAAS,KAAK,IAAI,YAAY,SAAS;AAC7C,WAAO,OAAO,UAAU;AAAA,EAC1B;AAAA;AAAA,EAGA,cAAc,YAA0B;AACtC,UAAM,OAAO,KAAK,GAAG,QAAQ;AAAA;AAAA;AAAA,KAG5B;AACD,SAAK,IAAI,UAAU;AAAA,EACrB;AAAA;AAAA,EAGA,WAAW,YAA0B;AACnC,UAAM,OAAO,KAAK,GAAG,QAAQ;AAAA;AAAA,KAE5B;AACD,SAAK,IAAI,UAAU;AAAA,EACrB;AAAA;AAAA,EAGA,mBAAmB,QAA0C;AAC3D,UAAM,OAAO,KAAK,GAAG,QAAQ;AAAA;AAAA,KAE5B;AACD,UAAM,OAAO,KAAK,IAAI,MAAM;AAC5B,WAAO,KAAK,IAAI,CAAC,MAAM,KAAK,YAAY,CAAC,CAAC;AAAA,EAC5C;AAAA;AAAA,EAIQ,SAAS,KAA4C;AAC3D,QAAI;AACJ,QAAI,IAAI,iBAAiB,OAAO,IAAI,kBAAkB,UAAU;AAC9D,UAAI;AACF,mBAAW,KAAK,MAAM,IAAI,aAAa;AAAA,MACzC,QAAQ;AAAA,MAER;AAAA,IACF;AAEA,WAAO;AAAA,MACL,IAAI,OAAO,IAAI,EAAE;AAAA,MACjB,WAAW,OAAO,IAAI,UAAU;AAAA,MAChC,YAAY,OAAO,IAAI,WAAW;AAAA,MAClC,MAAM,IAAI;AAAA,MACV,QAAQ,IAAI;AAAA,MACZ,aAAa,OAAO,IAAI,eAAe,EAAE;AAAA,MACzC,aAAa,OAAO,IAAI,gBAAgB,EAAE;AAAA,MAC1C,WAAW,IAAI,aAAa,OAAO,IAAI,UAAU,IAAI;AAAA,MACrD,WAAW,OAAO,IAAI,UAAU;AAAA,MAChC,aAAa,IAAI,eAAe,OAAO,IAAI,YAAY,IAAI;AAAA,MAC3D,OAAO,IAAI,QAAQ,OAAO,IAAI,KAAK,IAAI;AAAA,MACvC,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;AAAA,IACjC;AAAA,EACF;AAAA,EAEQ,YAAY,KAA8C;AAChE,WAAO;AAAA,MACL,YAAY,OAAO,IAAI,WAAW;AAAA,MAClC,UAAU,OAAO,IAAI,SAAS;AAAA,MAC9B,WAAW,OAAO,IAAI,UAAU;AAAA,MAChC,QAAQ,IAAI;AAAA,MACZ,UAAU,OAAO,IAAI,QAAQ;AAAA,MAC7B,WAAW,IAAI,aAAa,OAAO,IAAI,UAAU,IAAI;AAAA,MACrD,aAAa,IAAI,eAAe,OAAO,IAAI,YAAY,IAAI;AAAA,IAC7D;AAAA,EACF;AACF;;;AErQA,eAAsB,gBACpB,YACA,MACyB;AACzB,QAAM,EAAE,OAAO,iBAAiB,kBAAkB,YAAY,IAAI;AAGlE,MAAI,gBAAgB,GAAG;AACrB,WAAO,EAAE,YAAY,WAAW,IAAI,WAAW,MAAM;AAAA,EACvD;AAGA,QAAM,UAAU,MAAM,mBAAmB,SAAS;AAClD,QAAM,QAAQ,QAAQ,KAAK,CAAC,MAA8B,EAAE,eAAe,UAAU;AACrF,MAAI,CAAC,OAAO;AACV,WAAO,EAAE,YAAY,WAAW,IAAI,WAAW,MAAM;AAAA,EACvD;AAGA,QAAM,UAAU,MAAM,YAAY,YAAY,MAAM,SAAS;AAC7D,MAAI,CAAC,SAAS;AACZ,WAAO,EAAE,YAAY,WAAW,MAAM,WAAW,WAAW,MAAM;AAAA,EACpE;AAGA,QAAM,WAAW,MAAM,gBAAgB,MAAM,QAAQ;AACrD,QAAM,UAAU,WAAW,aAAa,QAAQ,IAAI,oBAAoB,MAAM,QAAQ;AAGtF,QAAM,UAAU,MAAM,iBAAiB,MAAM,WAAW,MAAM,UAAU,OAAO;AAE/E,MAAI,SAAS;AACX,UAAM,cAAc,UAAU;AAC9B,kBAAc,YAAY,MAAM,SAAS;AACzC,WAAO,EAAE,YAAY,WAAW,MAAM,WAAW,WAAW,KAAK;AAAA,EACnE;AAEA,QAAM,WAAW,UAAU;AAC3B,SAAO,EAAE,YAAY,WAAW,MAAM,WAAW,WAAW,MAAM;AACpE;AAMA,eAAsB,uBACpB,WACA,MACiB;AACjB,QAAM,EAAE,OAAO,gBAAgB,IAAI;AACnC,MAAI,gBAAgB,EAAG,QAAO;AAE9B,QAAM,UAAU,MAAM,mBAAmB,SAAS;AAClD,MAAI,YAAY;AAChB,aAAW,SAAS,SAAS;AAC3B,QAAI,MAAM,cAAc,UAAW;AACnC,UAAM,SAAS,MAAM,gBAAgB,MAAM,YAAY,IAAI;AAC3D,QAAI,OAAO,UAAW;AAAA,EACxB;AACA,SAAO;AACT;AAOA,SAAS,aAAa,UAA4B;AAChD,QAAM,QAAkB;AAAA,IACtB;AAAA,IACA;AAAA,IACA,KAAK,SAAS,WAAW,MAAM,wBAAqB,SAAS,aAAa,MAAM,0BAAuB,SAAS,SAAS,MAAM;AAAA,EACjI;AAEA,QAAM,WAAW,SAAS;AAM1B,QAAM,WAAW,SAAS,OAAO,CAAC,MAAM,EAAE,aAAa,cAAc,EAAE,aAAa,MAAM;AAC1F,MAAI,SAAS,SAAS,GAAG;AACvB,UAAM,KAAK,IAAI,SAAS,KAAK,IAAI,GAAG,SAAS,MAAM,CAAC,qBAAqB;AACzE,eAAW,KAAK,SAAS,MAAM,GAAG,CAAC,GAAG;AACpC,YAAM,MAAM,SAAS,aAAa,KAAK,CAAC,MAAsB,EAAE,OAAO,EAAE,YAAY;AACrF,YAAM,KAAK,cAAS,KAAK,QAAQ,EAAE,YAAY,aAAQ,EAAE,IAAI,KAAK,EAAE,SAAS,EAAE;AAAA,IACjF;AAAA,EACF;AAEA,QAAM,KAAK,IAAI,gDAAgD;AAC/D,SAAO,MAAM,KAAK,IAAI;AACxB;",
|
|
6
6
|
"names": ["resolve", "relative", "resolve", "dedupKey", "readFileSync", "join", "manifestEvidence", "lockfileEvidence", "join", "readFileSync", "manifestEvidence", "lockfileEvidence", "workspaceRoot", "readFileSync", "manifestEvidence", "lockfileEvidence", "parseTomlSections", "readFileSync", "readFileSync", "manifestEvidence", "lockfileEvidence", "readFileSync", "readFileSync", "join", "manifestEvidence", "lockfileEvidence", "join", "readFileSync", "readFileSync", "join", "manifestEvidence", "lockfileEvidence", "join", "readFileSync", "readFileSync", "join", "manifestEvidence", "lockfileEvidence", "join", "readFileSync", "readFileSync", "manifestEvidence", "readFileSync", "readFileSync", "manifestEvidence", "lockfileEvidence", "readFileSync", "readFileSync", "manifestEvidence", "lockfileEvidence", "readFileSync", "readFileSync", "manifestEvidence", "readFileSync", "resolve", "httpsGet", "MAX_RETRIES", "BASE_BACKOFF_MS", "resolve", "sleep", "existsSync", "join", "workspaceRoot", "existsSync", "join", "join", "existsSync"]
|
|
7
7
|
}
|