Source: utils/bornRepoKey.js

// @ts-check
/**
 * Born-Repo-Keys — geteilter Vertrag zwischen members und ide-server
 * (080-Workspaces/018-Cross-Service-Contracts.mdx; AP 20/C6).
 *
 * Ein **geborenes** Repo (Thread-Repo, geborene Einheit) hat **keine** `gitUrl`;
 * es entsteht im Plattform-Origin des ide-servers. Damit es auch in der
 * members-Topologie (Roots/Ableger, Teilen, UI) erscheint, legt members beim
 * ersten Repository-Share eines Projekts die **Ressourcen-Zeile** an — und muss
 * dabei **denselben** `repositoryKey` bilden wie der ide-server. Genau das ist
 * der Vertrag: diese Datei ist die members-Fassung von
 * `ide-server/src/workspaces/repoKeyName.js` + `threads/threadRepo.js`.
 *
 * **Warum die Ableitung doppelt existiert (und doppelt bleiben muss):**
 * `bareRepoPath(key)` ist **global** (`<GIT_BARE_BASE>/<key>.git`); der Key ist
 * die Identität des Bares. Läuft die Ableitung auseinander, zeigt die
 * members-Zeile auf ein anderes Bare als der ide-server ⇒ zwei leere
 * Thread-Repos, stiller Datenverlust. Deshalb: identische Regeln, identische
 * Grenzen — und ein Test auf **beiden** Seiten, der genau das festhält.
 *
 * Der members-Dienst kennt die physische Bare-Ablage **nicht** und schreibt sie
 * auch nicht an: er bildet nur den Namen. Das Bare legt der ide-server an
 * (`ensurePlatformRepo`).
 *
 * @module utils/bornRepoKey
 */

import crypto from 'node:crypto';

/**
 * Repo-Keys, die als Bare-Verzeichnisname zulässig sind — identisch zu
 * `ide-server/src/workspaces/repoKeyName.js` (`VALID_REPO_KEY`).
 */
export const VALID_REPO_KEY = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/;

/** Kennung geborener Thread-Repos in `metadata.kind` (ide-server: `platform`). */
export const BORN_THREAD_KIND = 'thread-repo';

/** Markierung, die eine Ressourcenzeile als **geboren** ausweist. */
export const BORN_FLAG = 'born';

/**
 * Repo-Keys auf `VALID_REPO_KEY` bringen. Der `..`-Fall ist der wichtige:
 * `'../../etc/passwd'` → `'etc-passwd'`. Identisch zur ide-server-Fassung.
 *
 * @param {unknown} value
 * @param {number} [maxLength]
 * @returns {string} leer, wenn nichts Gültiges übrig bleibt
 */
export function sanitizeRepoKey(value, maxLength = 96) {
    return String(value ?? '')
        .replace(/[^a-zA-Z0-9._-]+/g, '-')
        .replace(/^[^a-zA-Z0-9]+/, '')
        .replace(/-+$/g, '')
        .slice(0, maxLength);
}

/**
 * Kurzer, stabiler Suffix aus einer UID (8 Hex-Zeichen) — identisch zu
 * `shortUidSuffix`/`projectKeySuffix` des ide-servers (md5, erste 8 Hex).
 * @param {unknown} value
 * @returns {string}
 */
export function shortUidSuffix(value) {
    return crypto.createHash('md5').update(String(value ?? '')).digest('hex').slice(0, 8);
}

/**
 * Deterministischer Repo-Key des Thread-Repos eines Projekts.
 *
 * Regel (identisch zu `ide-server/src/threads/threadRepo.js`):
 * `threads-<primaryRepoKey:sanitized(96)>-<md5(projectUid)[0:8]>`.
 *
 * Der **primäre** Repo-Key ist der `repositoryKey` des Repository-Shares mit
 * der **kleinsten UID** (der ide-server bildet `projectKeyForSnapshot` genauso,
 * `snapshot.shares` sortiert nach `uid`) — **geborene** Shares zählen dabei
 * nicht, sonst hinge der Key an sich selbst.
 *
 * @param {{ projectUid?: unknown, primaryRepoKey?: unknown }} input
 * @returns {string|null} `null`, wenn kein gültiger Key gebildet werden kann
 */
export function threadRepoKey(input = {}) {
    const base = sanitizeRepoKey(input.primaryRepoKey, 96);
    const suffix = shortUidSuffix(input.projectUid);
    if (!base || !suffix) return null;
    const key = `threads-${base}-${suffix}`;
    return VALID_REPO_KEY.test(key) ? key : null;
}

/**
 * Metadaten einer **geborenen** Thread-Repo-Share-Zeile.
 *
 * Sie ist eine reguläre, editierbare `repositoryShare`-Zeile (`required: true`)
 * — nur ohne `gitUrl` und ohne members-Snapshot-Ursprung. `born: true` ist die
 * generische Kennung, die der ide-server liest (`isBornResource`); `indexing`
 * hält das Repo aus dem RAG-Index (Thread-Wissen wird über den Overlay-Pfad
 * indexiert, nicht als Projekt-Share-Base).
 *
 * @param {{ repoKey: string, defaultBranch?: string, createdBy?: string }} input
 * @returns {Record<string, unknown>}
 */
export function bornThreadRepoMetadata(input) {
    return {
        [BORN_FLAG]: true,
        kind: BORN_THREAD_KIND,
        required: true,
        repositoryKey: input.repoKey,
        defaultBranch: input.defaultBranch || 'dev',
        indexing: { enabled: false },
        ...(input.createdBy ? { createdBy: input.createdBy } : {}),
    };
}