{"version":3,"file":"mock-skills-store.mjs","names":[],"sources":["../../../../../../../../ai/src/skills/store/mock-skills-store.ts"],"sourcesContent":["import type {\n  SkillCatalogEntry,\n  SkillRecord,\n} from \"../contracts/skill-record.type\";\nimport type { SkillsStoreContract } from \"../contracts/skills-store.contract\";\n\n/**\n * In-memory {@link SkillsStoreContract} that ships with the package.\n *\n * Backs tests and small/ephemeral skill libraries with zero external\n * dependencies. Holds the **latest** record per skill name; `saveCandidate`\n * writes an INERT `type: \"candidate\"` (never injectable until promoted),\n * and `promote` flips it to `type: \"promoted\"` with a monotonic\n * `version + 1`.\n *\n * Construct via `new MockSkillsStore([...records])` — it is a concrete\n * test/utility store, not a factory-fronted runtime primitive, so `new`\n * is the public surface here.\n *\n * @example\n * const store = new MockSkillsStore([\n *   { name: \"scaffold\", description: \"Scaffold a form\", version: 1, body: \"...\", type: \"authored\" },\n * ]);\n * const lib = skills({ name: \"build\", sources: [{ type: \"store\", store }] });\n */\nexport class MockSkillsStore implements SkillsStoreContract {\n  /** Latest record per skill name. */\n  private readonly records = new Map<string, SkillRecord>();\n\n  public constructor(seed: SkillRecord[] = []) {\n    for (const record of seed) {\n      this.records.set(record.name, { ...record });\n    }\n  }\n\n  /**\n   * List the cheap catalog metadata for every NON-candidate skill,\n   * optionally filtered to those whose `tags` intersect `scope.tags`.\n   * Candidates are filtered out — they can never be catalogued or injected.\n   */\n  public async list(scope?: { tags?: string[] }): Promise<SkillCatalogEntry[]> {\n    const wanted = scope?.tags;\n\n    return [...this.records.values()]\n      .filter((record) => record.type !== \"candidate\")\n      .filter((record) => intersects(record.tags, wanted))\n      .map(toCatalogEntry);\n  }\n\n  /**\n   * Load the full record for `name`. When `version` is given, returns the\n   * record only if its version matches (pin); otherwise the latest. A\n   * `candidate` is never returned here — it is inert until promoted.\n   */\n  public async load(name: string, version?: number): Promise<SkillRecord | undefined> {\n    const record = this.records.get(name);\n\n    if (!record || record.type === \"candidate\") {\n      return undefined;\n    }\n\n    if (version !== undefined && record.version !== version) {\n      return undefined;\n    }\n\n    return { ...record };\n  }\n\n  /**\n   * Write an INERT candidate (`type: \"candidate\"`, `version: 0`). A\n   * candidate is filtered out of `list()` / `load()` — it can never be\n   * injected until a `review` gate promotes it.\n   */\n  public async saveCandidate(\n    record: Omit<SkillRecord, \"version\" | \"type\">,\n  ): Promise<SkillRecord> {\n    const candidate: SkillRecord = {\n      ...record,\n      version: 0,\n      type: \"candidate\",\n    };\n\n    this.records.set(candidate.name, candidate);\n\n    return { ...candidate };\n  }\n\n  /**\n   * Promote the stored candidate for `name` to a new monotonic version\n   * (`type: \"promoted\"`, `version + 1`). Throws when there is no candidate\n   * to promote — promotion of a non-existent skill is a programming error.\n   */\n  public async promote(name: string): Promise<SkillRecord> {\n    const existing = this.records.get(name);\n\n    if (!existing) {\n      throw new Error(`MockSkillsStore.promote: no skill named \"${name}\" to promote`);\n    }\n\n    const promoted: SkillRecord = {\n      ...existing,\n      version: existing.version + 1,\n      type: \"promoted\",\n    };\n\n    this.records.set(name, promoted);\n\n    return { ...promoted };\n  }\n}\n\n/** Project a full record down to its catalog entry (body omitted). */\nfunction toCatalogEntry(record: SkillRecord): SkillCatalogEntry {\n  return {\n    name: record.name,\n    description: record.description,\n    version: record.version,\n    tags: record.tags,\n    type: record.type,\n  };\n}\n\n/**\n * True when no filter tags are requested, or when the record carries at\n * least one of the requested tags. A tagless record matches only the\n * unfiltered case.\n */\nfunction intersects(recordTags: string[] | undefined, wanted: string[] | undefined): boolean {\n  if (!wanted || wanted.length === 0) {\n    return true;\n  }\n\n  if (!recordTags || recordTags.length === 0) {\n    return false;\n  }\n\n  return recordTags.some((tag) => wanted.includes(tag));\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAyBA,IAAa,kBAAb,MAA4D;CAI1D,AAAO,YAAY,OAAsB,CAAC,GAAG;iCAFlB,IAAI,IAAyB;EAGtD,KAAK,MAAM,UAAU,MACnB,KAAK,QAAQ,IAAI,OAAO,MAAM,EAAE,GAAG,OAAO,CAAC;CAE/C;;;;;;CAOA,MAAa,KAAK,OAA2D;EAC3E,MAAM,SAAS,OAAO;EAEtB,OAAO,CAAC,GAAG,KAAK,QAAQ,OAAO,CAAC,CAAC,CAC9B,QAAQ,WAAW,OAAO,SAAS,WAAW,CAAC,CAC/C,QAAQ,WAAW,WAAW,OAAO,MAAM,MAAM,CAAC,CAAC,CACnD,IAAI,cAAc;CACvB;;;;;;CAOA,MAAa,KAAK,MAAc,SAAoD;EAClF,MAAM,SAAS,KAAK,QAAQ,IAAI,IAAI;EAEpC,IAAI,CAAC,UAAU,OAAO,SAAS,aAC7B;EAGF,IAAI,YAAY,UAAa,OAAO,YAAY,SAC9C;EAGF,OAAO,EAAE,GAAG,OAAO;CACrB;;;;;;CAOA,MAAa,cACX,QACsB;EACtB,MAAM,YAAyB;GAC7B,GAAG;GACH,SAAS;GACT,MAAM;EACR;EAEA,KAAK,QAAQ,IAAI,UAAU,MAAM,SAAS;EAE1C,OAAO,EAAE,GAAG,UAAU;CACxB;;;;;;CAOA,MAAa,QAAQ,MAAoC;EACvD,MAAM,WAAW,KAAK,QAAQ,IAAI,IAAI;EAEtC,IAAI,CAAC,UACH,MAAM,IAAI,MAAM,4CAA4C,KAAK,aAAa;EAGhF,MAAM,WAAwB;GAC5B,GAAG;GACH,SAAS,SAAS,UAAU;GAC5B,MAAM;EACR;EAEA,KAAK,QAAQ,IAAI,MAAM,QAAQ;EAE/B,OAAO,EAAE,GAAG,SAAS;CACvB;AACF;;AAGA,SAAS,eAAe,QAAwC;CAC9D,OAAO;EACL,MAAM,OAAO;EACb,aAAa,OAAO;EACpB,SAAS,OAAO;EAChB,MAAM,OAAO;EACb,MAAM,OAAO;CACf;AACF;;;;;;AAOA,SAAS,WAAW,YAAkC,QAAuC;CAC3F,IAAI,CAAC,UAAU,OAAO,WAAW,GAC/B,OAAO;CAGT,IAAI,CAAC,cAAc,WAAW,WAAW,GACvC,OAAO;CAGT,OAAO,WAAW,MAAM,QAAQ,OAAO,SAAS,GAAG,CAAC;AACtD"}