{"version":3,"file":"journal.cjs","names":["DEFAULT_TEST_ID","generateId"],"sources":["../src/journal.ts"],"sourcesContent":["import { generateId } from \"./helpers.js\";\nimport type { Fixture, FixtureMatch, JournalEntry } from \"./types.js\";\nimport { DEFAULT_TEST_ID } from \"./constants.js\";\nexport { DEFAULT_TEST_ID } from \"./constants.js\";\n\n/**\n * Compare two field values, handling RegExp by source+flags rather than reference.\n */\nfunction fieldEqual(a: unknown, b: unknown): boolean {\n  if (a instanceof RegExp && b instanceof RegExp)\n    return a.source === b.source && a.flags === b.flags;\n  return a === b;\n}\n\n/**\n * Compare two systemMessage values. Handles string, string[], and RegExp.\n * Both-undefined is treated as equal.\n */\nfunction systemMessageEqual(\n  a: string | string[] | RegExp | undefined,\n  b: string | string[] | RegExp | undefined,\n): boolean {\n  if (a === undefined && b === undefined) return true;\n  if (a === undefined || b === undefined) return false;\n  if (typeof a === \"string\" && typeof b === \"string\") return a === b;\n  if (Array.isArray(a) && Array.isArray(b))\n    return a.length === b.length && a.every((v, i) => v === b[i]);\n  if (a instanceof RegExp && b instanceof RegExp)\n    return a.source === b.source && a.flags === b.flags;\n  return false;\n}\n\n/**\n * Check whether two fixture match objects have the same criteria\n * (ignoring sequenceIndex). Used to group sequenced fixtures.\n */\nfunction matchCriteriaEqual(a: FixtureMatch, b: FixtureMatch): boolean {\n  return (\n    fieldEqual(a.userMessage, b.userMessage) &&\n    systemMessageEqual(a.systemMessage, b.systemMessage) &&\n    fieldEqual(a.inputText, b.inputText) &&\n    fieldEqual(a.toolCallId, b.toolCallId) &&\n    fieldEqual(a.toolName, b.toolName) &&\n    fieldEqual(a.model, b.model) &&\n    fieldEqual(a.responseFormat, b.responseFormat) &&\n    fieldEqual(a.predicate, b.predicate) &&\n    fieldEqual(a.endpoint, b.endpoint) &&\n    fieldEqual(a.turnIndex, b.turnIndex) &&\n    fieldEqual(a.hasToolResult, b.hasToolResult)\n  );\n}\n\nexport interface JournalOptions {\n  /**\n   * Maximum number of entries to retain. When exceeded, oldest entries are\n   * dropped FIFO. Set to 0 (or omit) for unbounded retention (the historical\n   * default — suitable for short-lived test runs only). Negative values are\n   * rejected at the CLI parse layer; programmatically they are treated as 0\n   * (unbounded) for back-compat.\n   *\n   * Long-running servers (e.g. mock proxies in CI/demo environments) should\n   * always set a finite cap: every request appends an entry holding the\n   * request body + headers + fixture reference, and without a cap the\n   * journal grows until the process OOMs.\n   */\n  maxEntries?: number;\n  /**\n   * Maximum number of unique testIds retained in the fixture match-count\n   * map (`fixtureMatchCountsByTestId`). When exceeded, the oldest testId\n   * (by first-insertion order) is evicted FIFO. Set to 0 (or omit) for\n   * unbounded retention. Negative values are rejected at the CLI parse\n   * layer; programmatically they are treated as 0 (unbounded) for\n   * back-compat. Without a cap this map can grow over time in long-running\n   * servers that see many unique testIds.\n   */\n  fixtureCountsMaxTestIds?: number;\n}\n\nexport class Journal {\n  private entries: JournalEntry[] = [];\n  private readonly fixtureMatchCountsByTestId: Map<string, Map<Fixture, number>> = new Map();\n  private readonly maxEntries: number;\n  private readonly fixtureCountsMaxTestIds: number;\n\n  constructor(options: JournalOptions = {}) {\n    // Treat 0 or negative as \"unbounded\" to preserve prior behavior when\n    // the option is omitted or explicitly disabled.\n    const cap = options.maxEntries;\n    this.maxEntries = cap !== undefined && cap > 0 ? cap : 0;\n    const testIdCap = options.fixtureCountsMaxTestIds;\n    this.fixtureCountsMaxTestIds = testIdCap !== undefined && testIdCap > 0 ? testIdCap : 0;\n  }\n\n  /** Backwards-compatible accessor — returns the default (no testId) count map. */\n  get fixtureMatchCounts(): Map<Fixture, number> {\n    return this.getFixtureMatchCountsForTest(DEFAULT_TEST_ID);\n  }\n\n  add(entry: Omit<JournalEntry, \"id\" | \"timestamp\">): JournalEntry {\n    const full: JournalEntry = {\n      id: generateId(\"req\"),\n      timestamp: Date.now(),\n      ...entry,\n    };\n    this.entries.push(full);\n    // FIFO eviction when over capacity. Array.prototype.shift() is O(n)\n    // regardless of how many we drop per add; we accept it at small caps\n    // (default 1000) because the constant factor is tiny and this runs once\n    // per request. For much larger caps, switch to a ring buffer for true\n    // O(1) eviction.\n    if (this.maxEntries > 0 && this.entries.length > this.maxEntries) {\n      this.entries.shift();\n    }\n    return full;\n  }\n\n  getAll(opts?: { limit?: number }): JournalEntry[] {\n    if (opts?.limit !== undefined) {\n      return this.entries.slice(-opts.limit);\n    }\n    return this.entries.slice();\n  }\n\n  getLast(): JournalEntry | null {\n    return this.entries.length > 0 ? this.entries[this.entries.length - 1] : null;\n  }\n\n  findByFixture(fixture: Fixture): JournalEntry[] {\n    return this.entries.filter((e) => e.response.fixture === fixture);\n  }\n\n  /**\n   * READ-ONLY accessor. Returns the existing count map for `testId`, or an\n   * empty transient Map if none exists. Does NOT insert into the cache and\n   * does NOT trigger FIFO eviction — callers may read freely without\n   * perturbing cache state. For the write path, see\n   * `getOrCreateFixtureMatchCountsForTest`.\n   */\n  getFixtureMatchCountsForTest(testId: string): Map<Fixture, number> {\n    return this.fixtureMatchCountsByTestId.get(testId) ?? new Map();\n  }\n\n  /**\n   * WRITE path: get the count map for `testId`, inserting a fresh empty Map\n   * if missing and running FIFO eviction when the testId cap is exceeded.\n   * Only callers that intend to mutate the map (e.g. incrementing a count)\n   * should use this.\n   */\n  private getOrCreateFixtureMatchCountsForTest(testId: string): Map<Fixture, number> {\n    let counts = this.fixtureMatchCountsByTestId.get(testId);\n    if (!counts) {\n      counts = new Map();\n      this.fixtureMatchCountsByTestId.set(testId, counts);\n      // FIFO eviction when over capacity. JS Map preserves insertion order,\n      // so the first key returned by keys() is the oldest. Same O(n) shift\n      // caveat as `entries`: acceptable at small caps (default 500).\n      if (\n        this.fixtureCountsMaxTestIds > 0 &&\n        this.fixtureMatchCountsByTestId.size > this.fixtureCountsMaxTestIds\n      ) {\n        const oldest = this.fixtureMatchCountsByTestId.keys().next().value;\n        if (oldest !== undefined) {\n          this.fixtureMatchCountsByTestId.delete(oldest);\n        }\n      }\n    }\n    return counts;\n  }\n\n  getFixtureMatchCount(fixture: Fixture, testId = DEFAULT_TEST_ID): number {\n    return this.getFixtureMatchCountsForTest(testId).get(fixture) ?? 0;\n  }\n\n  incrementFixtureMatchCount(\n    fixture: Fixture,\n    allFixtures?: readonly Fixture[],\n    testId = DEFAULT_TEST_ID,\n  ): void {\n    const counts = this.getOrCreateFixtureMatchCountsForTest(testId);\n    counts.set(fixture, (counts.get(fixture) ?? 0) + 1);\n    // When a sequenced fixture matches, also increment all siblings with matching criteria\n    if (fixture.match.sequenceIndex !== undefined && allFixtures) {\n      for (const sibling of allFixtures) {\n        if (sibling === fixture) continue;\n        if (sibling.match.sequenceIndex === undefined) continue;\n        if (matchCriteriaEqual(fixture.match, sibling.match)) {\n          counts.set(sibling, (counts.get(sibling) ?? 0) + 1);\n        }\n      }\n    }\n  }\n\n  clearMatchCounts(testId?: string): void {\n    if (testId !== undefined) {\n      this.fixtureMatchCountsByTestId.delete(testId);\n    } else {\n      this.fixtureMatchCountsByTestId.clear();\n    }\n  }\n\n  /**\n   * Clear ONLY the request journal entries, preserving fixture match-counts.\n   * Match-counts are fixture-matching/sequencing state, not journal data, so\n   * clearing the journal must not silently rewind sequenced fixtures. Used by\n   * `POST /__aimock/reset/journal`. For a full reset (entries + match-counts),\n   * use `clear()` instead.\n   */\n  clearEntries(): void {\n    this.entries = [];\n  }\n\n  clear(): void {\n    this.entries = [];\n    this.fixtureMatchCountsByTestId.clear();\n  }\n\n  get size(): number {\n    return this.entries.length;\n  }\n}\n"],"mappings":"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"}