// @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 } : {}),
};
}