// src/compliance-freshness/regulator-watcher.ts
var RegulatorWatcher = class {
  constructor(regulators, fetcher, options = {}) {
    this.regulators = regulators;
    this.fetcher = fetcher;
    this.cadenceOverrides = options.cadenceOverrides ?? {};
  }
  regulators;
  fetcher;
  cadenceOverrides;
  /** Returns all updates detected across all configured regulators. */
  async watchAll(since) {
    const results = await Promise.allSettled(
      this.regulators.map((r) => this.watchRegulator(r.id, since))
    );
    const updates = [];
    for (const result of results) {
      if (result.status === "fulfilled") {
        updates.push(...result.value);
      }
    }
    return updates;
  }
  /** Returns updates from a single regulator since the given timestamp. */
  async watchRegulator(regulatorId, since) {
    return this.fetcher.fetchUpdates(regulatorId, since);
  }
  /** Returns the effective cadence for a regulator (override or config default). */
  effectiveCadence(regulatorId) {
    if (this.cadenceOverrides[regulatorId]) {
      return this.cadenceOverrides[regulatorId];
    }
    const config = this.regulators.find((r) => r.id === regulatorId);
    return config?.cadence ?? "monthly";
  }
  /** Returns a Date representing `cadence` ago from `now`. */
  static sinceForCadence(cadence, now = /* @__PURE__ */ new Date()) {
    const ms = now.getTime();
    switch (cadence) {
      case "daily":
        return new Date(ms - 24 * 60 * 60 * 1e3);
      case "weekly":
        return new Date(ms - 7 * 24 * 60 * 60 * 1e3);
      case "monthly":
        return new Date(ms - 30 * 24 * 60 * 60 * 1e3);
    }
  }
};
var KNOWN_AUSTRALIAN_REGULATORS = [
  {
    id: "AUSTRAC",
    name: "Australian Transaction Reports and Analysis Centre",
    jurisdiction: "AU",
    publicationUrls: [
      "https://www.austrac.gov.au/news-and-media/news",
      "https://www.austrac.gov.au/business/compliance-and-guidance"
    ],
    cadence: "weekly",
    tags: ["AML", "CTF", "financial-services", "reporting", "tranche-2"]
  },
  {
    id: "ASIC",
    name: "Australian Securities and Investments Commission",
    jurisdiction: "AU",
    publicationUrls: [
      "https://asic.gov.au/about-asic/news-centre/news/",
      "https://asic.gov.au/regulatory-resources/regulatory-guides/"
    ],
    cadence: "monthly",
    tags: ["financial-services", "licensing", "market-integrity", "advice"]
  },
  {
    id: "ATO",
    name: "Australian Taxation Office",
    jurisdiction: "AU",
    publicationUrls: [
      "https://www.ato.gov.au/about-ato/news/",
      "https://www.ato.gov.au/law/"
    ],
    cadence: "monthly",
    tags: ["tax", "GST", "superannuation", "BAS", "reporting"]
  },
  {
    id: "AHPRA",
    name: "Australian Health Practitioner Regulation Agency",
    jurisdiction: "AU",
    publicationUrls: [
      "https://www.ahpra.gov.au/News/Notifications.aspx",
      "https://www.ahpra.gov.au/Publications/Codes-guidelines-and-policies.aspx"
    ],
    cadence: "monthly",
    tags: ["health", "practitioners", "registration", "standards"]
  },
  {
    id: "NHMRC",
    name: "National Health and Medical Research Council",
    jurisdiction: "AU",
    publicationUrls: [
      "https://www.nhmrc.gov.au/about-us/news-centre/nhmrc-news",
      "https://www.nhmrc.gov.au/about-us/publications"
    ],
    cadence: "monthly",
    tags: ["health", "research", "ethics", "clinical-trials"]
  },
  {
    id: "TPB",
    name: "Tax Practitioners Board",
    jurisdiction: "AU",
    publicationUrls: [
      "https://www.tpb.gov.au/news-media",
      "https://www.tpb.gov.au/tpb-guidance"
    ],
    cadence: "monthly",
    tags: ["tax-agents", "BAS-agents", "registration", "standards"]
  },
  {
    id: "AFCA",
    name: "Australian Financial Complaints Authority",
    jurisdiction: "AU",
    publicationUrls: [
      "https://www.afca.org.au/news",
      "https://www.afca.org.au/make-a-complaint/rules-and-guidelines"
    ],
    cadence: "monthly",
    tags: ["financial-services", "disputes", "complaints", "EDR"]
  },
  {
    id: "NDIS_COMMISSION",
    name: "NDIS Quality and Safeguards Commission",
    jurisdiction: "AU",
    publicationUrls: [
      "https://www.ndiscommission.gov.au/news-and-media",
      "https://www.ndiscommission.gov.au/providers/ndis-practice-standards"
    ],
    cadence: "monthly",
    tags: ["disability", "NDIS", "practice-standards", "quality-safeguards"]
  },
  {
    id: "ACCC",
    name: "Australian Competition and Consumer Commission",
    jurisdiction: "AU",
    publicationUrls: [
      "https://www.accc.gov.au/media-release",
      "https://www.accc.gov.au/consumers/consumer-rights-guarantees"
    ],
    cadence: "monthly",
    tags: ["consumer-protection", "competition", "fair-trading"]
  },
  {
    id: "OAIC",
    name: "Office of the Australian Information Commissioner",
    jurisdiction: "AU",
    publicationUrls: [
      "https://www.oaic.gov.au/news-and-information/media-releases",
      "https://www.oaic.gov.au/privacy/guidance-and-advice"
    ],
    cadence: "monthly",
    tags: ["privacy", "data-protection", "notifiable-breaches", "APP"]
  },
  {
    id: "FAIR_TRADING_NSW",
    name: "NSW Fair Trading",
    jurisdiction: "AU-NSW",
    publicationUrls: [
      "https://www.fairtrading.nsw.gov.au/news"
    ],
    cadence: "monthly",
    tags: ["consumer-protection", "fair-trading", "state"]
  },
  {
    id: "FAIR_TRADING_VIC",
    name: "Consumer Affairs Victoria",
    jurisdiction: "AU-VIC",
    publicationUrls: [
      "https://www.consumer.vic.gov.au/news-and-updates"
    ],
    cadence: "monthly",
    tags: ["consumer-protection", "fair-trading", "state"]
  },
  {
    id: "FAIR_TRADING_QLD",
    name: "Queensland Office of Fair Trading",
    jurisdiction: "AU-QLD",
    publicationUrls: [
      "https://www.qld.gov.au/law/fair-trading"
    ],
    cadence: "monthly",
    tags: ["consumer-protection", "fair-trading", "state"]
  }
];

// src/compliance-freshness/cross-reference-map.ts
var CrossReferenceMap = class {
  byPrimary;
  byRelated;
  constructor(rules) {
    this.byPrimary = /* @__PURE__ */ new Map();
    this.byRelated = /* @__PURE__ */ new Map();
    for (const rule of rules) {
      if (!this.byPrimary.has(rule.primaryRegulatorId)) {
        this.byPrimary.set(rule.primaryRegulatorId, []);
      }
      this.byPrimary.get(rule.primaryRegulatorId).push(rule);
      for (const relatedId of rule.relatedRegulatorIds) {
        if (!this.byRelated.has(relatedId)) {
          this.byRelated.set(relatedId, []);
        }
        this.byRelated.get(relatedId).push(rule);
      }
    }
  }
  /**
   * Returns the IDs of all regulators that should be reviewed when
   * `regulatorId` publishes an update.
   */
  getRelatedRegulatorIds(regulatorId) {
    const rules = this.byPrimary.get(regulatorId) ?? [];
    const ids = /* @__PURE__ */ new Set();
    for (const rule of rules) {
      for (const id of rule.relatedRegulatorIds) {
        ids.add(id);
      }
    }
    return Array.from(ids);
  }
  /** Returns all rules where `regulatorId` is the primary trigger. */
  getRulesForPrimary(regulatorId) {
    return this.byPrimary.get(regulatorId) ?? [];
  }
  /** Returns all rules where `regulatorId` appears as a related regulator. */
  getRulesForRelated(regulatorId) {
    return this.byRelated.get(regulatorId) ?? [];
  }
  /**
   * Returns the highest impact level among all rules triggered by an update
   * to `regulatorId`. Returns `null` if there are no cross-reference rules.
   */
  maxImpactLevel(regulatorId) {
    const rules = this.byPrimary.get(regulatorId) ?? [];
    if (rules.length === 0) return null;
    const order = { informational: 0, advisory: 1, mandatory: 2 };
    let max = "informational";
    for (const rule of rules) {
      if (order[rule.impactLevel] > order[max]) {
        max = rule.impactLevel;
      }
    }
    return max;
  }
};
var AUSTRALIAN_CROSS_REFERENCE_RULES = [
  {
    id: "xref-austrac-asic-ato",
    primaryRegulatorId: "AUSTRAC",
    relatedRegulatorIds: ["ASIC", "ATO"],
    description: "AML/CTF rule changes affect financial services licensing (ASIC) and AUSTRAC/ATO reporting obligations for designated services.",
    impactLevel: "mandatory"
  },
  {
    id: "xref-asic-afca",
    primaryRegulatorId: "ASIC",
    relatedRegulatorIds: ["AFCA"],
    description: "ASIC regulatory guide updates flow through to AFCA's jurisdiction and how complaints are assessed for financial services firms.",
    impactLevel: "advisory"
  },
  {
    id: "xref-afca-asic",
    primaryRegulatorId: "AFCA",
    relatedRegulatorIds: ["ASIC"],
    description: "AFCA rule changes on complaints handling are often aligned with, or anticipate, corresponding ASIC regulatory guide revisions.",
    impactLevel: "advisory"
  },
  {
    id: "xref-ato-tpb",
    primaryRegulatorId: "ATO",
    relatedRegulatorIds: ["TPB"],
    description: "ATO administrative changes (e.g. lodgement obligations, safe harbours) directly affect TPB-registered tax practitioners' obligations.",
    impactLevel: "mandatory"
  },
  {
    id: "xref-tpb-ato",
    primaryRegulatorId: "TPB",
    relatedRegulatorIds: ["ATO"],
    description: "TPB code of conduct changes may reference or tighten ATO administrative requirements for registered tax and BAS agents.",
    impactLevel: "advisory"
  },
  {
    id: "xref-ahpra-oaic",
    primaryRegulatorId: "AHPRA",
    relatedRegulatorIds: ["OAIC"],
    description: "AHPRA registration and disciplinary records are health information under the Privacy Act. Changes to AHPRA handling obligations may trigger APP reviews.",
    impactLevel: "advisory"
  },
  {
    id: "xref-oaic-ahpra",
    primaryRegulatorId: "OAIC",
    relatedRegulatorIds: ["AHPRA"],
    description: "Privacy Act amendments (especially APP 11 and notifiable data breaches) affect how AHPRA-registered practitioners manage patient records.",
    impactLevel: "mandatory"
  },
  {
    id: "xref-ndis-commission-ahpra",
    primaryRegulatorId: "NDIS_COMMISSION",
    relatedRegulatorIds: ["AHPRA"],
    description: "NDIS practice standards for registered health supports require dual compliance with NDIS Commission rules and AHPRA registration standards.",
    impactLevel: "mandatory"
  },
  {
    id: "xref-ahpra-ndis-commission",
    primaryRegulatorId: "AHPRA",
    relatedRegulatorIds: ["NDIS_COMMISSION"],
    description: "Updates to AHPRA profession-specific standards (e.g. physiotherapy, psychology) cascade to NDIS provider obligations in those disciplines.",
    impactLevel: "advisory"
  },
  {
    id: "xref-ndis-commission-oaic",
    primaryRegulatorId: "NDIS_COMMISSION",
    relatedRegulatorIds: ["OAIC"],
    description: "NDIS participant data is sensitive information under the Privacy Act; NDIS Commission practice standards interact with APP obligations.",
    impactLevel: "advisory"
  },
  {
    id: "xref-accc-fair-trading-states",
    primaryRegulatorId: "ACCC",
    relatedRegulatorIds: [
      "FAIR_TRADING_NSW",
      "FAIR_TRADING_VIC",
      "FAIR_TRADING_QLD"
    ],
    description: "ACCC Australian Consumer Law (ACL) changes propagate to state Fair Trading bodies who administer the ACL under concurrent jurisdiction.",
    impactLevel: "mandatory"
  },
  {
    id: "xref-fair-trading-nsw-accc",
    primaryRegulatorId: "FAIR_TRADING_NSW",
    relatedRegulatorIds: ["ACCC"],
    description: "NSW Fair Trading state-level reforms (e.g. rental, building) sometimes anticipate or complement ACCC consumer protection positions.",
    impactLevel: "informational"
  },
  {
    id: "xref-nhmrc-ahpra",
    primaryRegulatorId: "NHMRC",
    relatedRegulatorIds: ["AHPRA"],
    description: "NHMRC clinical and research ethics guidelines inform AHPRA profession-specific standards for research-active practitioners.",
    impactLevel: "informational"
  }
];

// src/compliance-freshness/drift-detector.ts
var DriftDetector = class {
  constructor(citationStore, briefingStore, crossRef) {
    this.citationStore = citationStore;
    this.briefingStore = briefingStore;
    this.crossRef = crossRef;
  }
  citationStore;
  briefingStore;
  crossRef;
  /**
   * Processes a batch of regulator updates and returns one DriftDetectionResult
   * per affected briefing.
   *
   * A briefing is affected if it cites at least one CitationSource whose
   * `version` or `status` has changed. Cross-reference expansion is applied
   * to identify cascading impacts even when no direct citation match exists.
   */
  async detectDrift(updates) {
    if (updates.length === 0) return [];
    const updatedCitationIds = /* @__PURE__ */ new Set();
    const updateByCitationId = /* @__PURE__ */ new Map();
    for (const update of updates) {
      for (const citationId of update.affectedCitationIds ?? []) {
        updatedCitationIds.add(citationId);
        updateByCitationId.set(citationId, update);
      }
    }
    const affectedCitations = updatedCitationIds.size > 0 ? await this.citationStore.listByIds(Array.from(updatedCitationIds)) : [];
    const affectedCitationIds = affectedCitations.map((c) => c.id);
    const affectedBriefings = affectedCitationIds.length > 0 ? await this.briefingStore.listByCitationIds(affectedCitationIds) : [];
    const updatedRegulatorIds = new Set(updates.map((u) => u.regulatorId));
    const cascadingIds = /* @__PURE__ */ new Set();
    for (const regulatorId of updatedRegulatorIds) {
      for (const id of this.crossRef.getRelatedRegulatorIds(regulatorId)) {
        cascadingIds.add(id);
      }
    }
    const results = [];
    for (const briefing of affectedBriefings) {
      const outdatedCitations = await this.buildOutdatedCitations(
        briefing,
        affectedCitations,
        updateByCitationId
      );
      if (outdatedCitations.length === 0) continue;
      const severity = this.computeSeverity(
        outdatedCitations,
        cascadingIds,
        updatedRegulatorIds
      );
      results.push({
        briefingId: briefing.id,
        operatorId: briefing.operatorId,
        outdatedCitations,
        cascadingRegulatorIds: Array.from(cascadingIds),
        severity,
        detectedAt: /* @__PURE__ */ new Date()
      });
    }
    return results;
  }
  async buildOutdatedCitations(briefing, affectedCitations, updateByCitationId) {
    const drifts = [];
    const affectedSet = new Set(affectedCitations.map((c) => c.id));
    for (const citationId of briefing.citationIds) {
      if (!affectedSet.has(citationId)) continue;
      const citation = affectedCitations.find((c) => c.id === citationId);
      if (!citation) continue;
      const update = updateByCitationId.get(citationId);
      if (!update || !update.newVersion) continue;
      if (citation.version === update.newVersion) continue;
      drifts.push({
        citationId,
        regulatorId: citation.regulatorId,
        previousVersion: citation.version,
        currentVersion: update.newVersion,
        changeDescription: update.description,
        changeUrl: update.url
      });
    }
    return drifts;
  }
  computeSeverity(outdatedCitations, cascadingIds, updatedRegulatorIds) {
    for (const regulatorId of updatedRegulatorIds) {
      const maxImpact = this.crossRef.maxImpactLevel(regulatorId);
      if (maxImpact === "mandatory" && cascadingIds.size > 0) {
        return "critical";
      }
    }
    if (outdatedCitations.length >= 3) return "critical";
    if (outdatedCitations.length >= 2) return "high";
    for (const regulatorId of updatedRegulatorIds) {
      const maxImpact = this.crossRef.maxImpactLevel(regulatorId);
      if (maxImpact === "advisory") return "medium";
    }
    if (outdatedCitations.length === 1) return "low";
    return "low";
  }
};

// src/compliance-freshness/intervention-drafter.ts
var InterventionDrafter = class {
  constructor(anthropic, briefingStore, citationStore, interventionStore, options = {}) {
    this.anthropic = anthropic;
    this.briefingStore = briefingStore;
    this.citationStore = citationStore;
    this.interventionStore = interventionStore;
    this.model = options.model ?? "claude-haiku-4-5-20251001";
    this.maxTokens = options.maxTokens ?? 2048;
  }
  anthropic;
  briefingStore;
  citationStore;
  interventionStore;
  model;
  maxTokens;
  /**
   * Drafts a briefing update and client notification for the given drift event.
   * Returns a stored InterventionDraft in DRAFT status ready for operator approval.
   */
  async draftIntervention(driftResult) {
    const briefing = await this.briefingStore.get(driftResult.briefingId);
    if (!briefing) {
      throw new Error(`Briefing not found: ${driftResult.briefingId}`);
    }
    const citations = await this.citationStore.listByIds(briefing.citationIds);
    const { briefingUpdate, clientNotification } = await this.callClaude(
      briefing,
      citations,
      driftResult
    );
    return this.interventionStore.create({
      briefingId: briefing.id,
      operatorId: briefing.operatorId,
      driftResult,
      briefingUpdateDraft: briefingUpdate,
      clientNotificationDraft: clientNotification,
      status: "DRAFT"
    });
  }
  async callClaude(briefing, citations, drift) {
    const citationSummary = drift.outdatedCitations.map(
      (d) => `- "${citations.find((c) => c.id === d.citationId)?.title ?? d.citationId}" (${d.regulatorId}): ${d.previousVersion} \u2192 ${d.currentVersion}` + (d.changeDescription ? `
  Change: ${d.changeDescription}` : "") + (d.changeUrl ? `
  URL: ${d.changeUrl}` : "")
    ).join("\n");
    const cascadeSummary = drift.cascadingRegulatorIds.length > 0 ? `
Cascading impact identified for: ${drift.cascadingRegulatorIds.join(", ")}.` : "";
    const prompt = `You are a compliance writer updating a regulatory briefing after detecting that cited sources have changed.

## Current Briefing
Title: ${briefing.title}

${briefing.content}

## Detected Regulatory Changes (severity: ${drift.severity})
${citationSummary}${cascadeSummary}

## Task
Produce two outputs separated by the delimiter "---NOTIFICATION---":

**Part 1 \u2014 Updated Briefing (Markdown)**
Revise the briefing to reflect the regulatory changes above. Retain the original structure. Update only the sections affected by the cited changes. Note the new effective versions inline. Keep the tone professional and precise.

**Part 2 \u2014 Client Notification (Markdown, \u2264 150 words)**
Write a concise update for the operator to send to affected clients. Explain what changed, why it matters, and what action (if any) clients should take. Do not speculate beyond the listed changes.

Respond with Part 1, then the literal line "---NOTIFICATION---", then Part 2. No preamble.`;
    const response = await this.anthropic.messages.create({
      model: this.model,
      max_tokens: this.maxTokens,
      messages: [{ role: "user", content: prompt }]
    });
    const firstBlock = response.content[0];
    const raw = firstBlock !== void 0 && firstBlock.type === "text" ? firstBlock.text : "";
    const delimiter = "---NOTIFICATION---";
    const delimiterIndex = raw.indexOf(delimiter);
    if (delimiterIndex === -1) {
      return {
        briefingUpdate: raw.trim(),
        clientNotification: `Regulatory changes detected for briefing "${briefing.title}". Please review the updated briefing for details.`
      };
    }
    return {
      briefingUpdate: raw.slice(0, delimiterIndex).trim(),
      clientNotification: raw.slice(delimiterIndex + delimiter.length).trim()
    };
  }
};

// src/compliance-freshness/version-history.ts
var VersionHistory = class {
  constructor(store) {
    this.store = store;
  }
  store;
  /**
   * Records a status transition for a briefing.
   *
   * `version` must be the new version number (typically prior version + 1).
   * `content` is the snapshot of the briefing at the time of transition.
   */
  async recordTransition(briefingId, version, newStatus, content, options = {}) {
    const record = {
      briefingId,
      version,
      status: newStatus,
      content,
      reason: options.reason,
      supersededById: options.supersededById,
      createdAt: /* @__PURE__ */ new Date()
    };
    await this.store.append(record);
    return record;
  }
  /** Returns the complete audit trail for a briefing, oldest first. */
  async getHistory(briefingId) {
    return this.store.getHistory(briefingId);
  }
  /** Returns the most recent state of a briefing, or null if none exists. */
  async getCurrent(briefingId) {
    return this.store.getLatest(briefingId);
  }
  /** Reconstructs the supersession chain for a briefing. */
  async getSupersessionChain(briefingId) {
    const history = await this.store.getHistory(briefingId);
    return history.filter(
      (r) => r.status === "SUPERSEDED" || r.supersededById != null
    );
  }
};

// src/compliance-freshness/memory-stores.ts
import { randomUUID } from "crypto";
var InMemoryCitationStore = class {
  citations = /* @__PURE__ */ new Map();
  async get(id) {
    return this.citations.get(id) ?? null;
  }
  async listByIds(ids) {
    return ids.flatMap((id) => {
      const c = this.citations.get(id);
      return c ? [c] : [];
    });
  }
  async getByRegulator(regulatorId) {
    return Array.from(this.citations.values()).filter(
      (c) => c.regulatorId === regulatorId
    );
  }
  async upsert(citation) {
    this.citations.set(citation.id, citation);
    return citation;
  }
};
var InMemoryBriefingStore = class {
  briefings = /* @__PURE__ */ new Map();
  async get(id) {
    return this.briefings.get(id) ?? null;
  }
  async update(id, patch) {
    const existing = this.briefings.get(id);
    if (!existing) throw new Error(`Briefing not found: ${id}`);
    const updated = { ...existing, ...patch, updatedAt: /* @__PURE__ */ new Date() };
    this.briefings.set(id, updated);
    return updated;
  }
  async listPublished(operatorId) {
    return Array.from(this.briefings.values()).filter(
      (b) => b.operatorId === operatorId && b.status === "PUBLISHED"
    );
  }
  async listByCitationIds(citationIds) {
    const ids = new Set(citationIds);
    return Array.from(this.briefings.values()).filter(
      (b) => b.status === "PUBLISHED" && b.citationIds.some((id) => ids.has(id))
    );
  }
  /** Test helper: seed a briefing without going through a create flow. */
  seed(briefing) {
    this.briefings.set(briefing.id, briefing);
  }
};
var InMemoryInterventionStore = class {
  drafts = /* @__PURE__ */ new Map();
  async create(draft) {
    const record = {
      ...draft,
      id: randomUUID(),
      createdAt: /* @__PURE__ */ new Date()
    };
    this.drafts.set(record.id, record);
    return record;
  }
  async get(id) {
    return this.drafts.get(id) ?? null;
  }
  async update(id, patch) {
    const existing = this.drafts.get(id);
    if (!existing) throw new Error(`InterventionDraft not found: ${id}`);
    const updated = { ...existing, ...patch };
    this.drafts.set(id, updated);
    return updated;
  }
  async listPending() {
    return Array.from(this.drafts.values()).filter(
      (d) => d.status === "DRAFT"
    );
  }
};
var InMemoryVersionHistoryStore = class {
  records = [];
  async append(record) {
    this.records.push(record);
  }
  async getHistory(briefingId) {
    return this.records.filter((r) => r.briefingId === briefingId).sort((a, b) => a.version - b.version);
  }
  async getLatest(briefingId) {
    const history = this.records.filter((r) => r.briefingId === briefingId).sort((a, b) => b.version - a.version);
    return history[0] ?? null;
  }
};
var InMemoryRegulatorPublicationFetcher = class {
  updates = [];
  addUpdates(...updates) {
    this.updates.push(...updates);
  }
  async fetchUpdates(regulatorId, since) {
    return this.updates.filter(
      (u) => u.regulatorId === regulatorId && u.publishedAt.getTime() > since.getTime()
    );
  }
};

// src/compliance-freshness/scanners/types.ts
var consoleScannerLogger = {
  info: (m, c) => console.log(`[scanner] ${m}`, c ?? ""),
  warn: (m, c) => console.warn(`[scanner] ${m}`, c ?? ""),
  error: (m, c) => console.error(`[scanner] ${m}`, c ?? "")
};
var CADENCE_MINUTES = {
  daily: 24 * 60,
  weekly: 7 * 24 * 60,
  monthly: 30 * 24 * 60
};

// src/compliance-freshness/scanners/scanner-orchestrator.ts
var DEFAULT_DEDUP_CAPACITY = 1e4;
var ScannerOrchestrator = class {
  scanners = /* @__PURE__ */ new Map();
  seen = /* @__PURE__ */ new Map();
  timers = /* @__PURE__ */ new Map();
  logger;
  dedupCapacity;
  constructor(options = {}) {
    this.logger = options.logger ?? consoleScannerLogger;
    this.dedupCapacity = options.dedupCapacity ?? DEFAULT_DEDUP_CAPACITY;
  }
  registerScanner(scanner) {
    this.scanners.set(scanner.regulatorId, scanner);
  }
  listScanners() {
    return [...this.scanners.values()];
  }
  /**
   * Run every registered scanner once, dedupe by hash, return only
   * publications not previously seen. Fail-soft per scanner.
   */
  async runOnce(since) {
    const settled = await Promise.allSettled(
      [...this.scanners.values()].map(async (s) => {
        try {
          return await s.scan(since);
        } catch (err) {
          this.logger.error("scanner failed", {
            regulatorId: s.regulatorId,
            error: err instanceof Error ? err.message : String(err)
          });
          throw err;
        }
      })
    );
    const out = [];
    for (const result of settled) {
      if (result.status !== "fulfilled") continue;
      for (const pub of result.value) {
        if (this.seen.has(pub.hash)) continue;
        this.markSeen(pub.hash);
        out.push(pub);
      }
    }
    return out;
  }
  /**
   * Run a single registered scanner, dedupe, fail-soft.
   * Public so the fetcher adapter can call into one scanner at a time.
   */
  async runScanner(regulatorId, since) {
    const scanner = this.scanners.get(regulatorId);
    if (!scanner) return [];
    let pubs;
    try {
      pubs = await scanner.scan(since);
    } catch (err) {
      this.logger.error("scanner failed", {
        regulatorId,
        error: err instanceof Error ? err.message : String(err)
      });
      return [];
    }
    const out = [];
    for (const pub of pubs) {
      if (this.seen.has(pub.hash)) continue;
      this.markSeen(pub.hash);
      out.push(pub);
    }
    return out;
  }
  /** Start per-scanner polling timers. Override interval applies to all. */
  start(intervalOverrideMinutes) {
    if (this.timers.size > 0) {
      this.logger.warn("ScannerOrchestrator.start called while already running");
      return;
    }
    for (const scanner of this.scanners.values()) {
      const minutes = intervalOverrideMinutes ?? scanner.intervalMinutes;
      const ms = Math.max(6e4, minutes * 6e4);
      const timer = setInterval(() => {
        void this.runScanner(scanner.regulatorId).catch((err) => {
          this.logger.error("scheduled scan failed", {
            regulatorId: scanner.regulatorId,
            error: err instanceof Error ? err.message : String(err)
          });
        });
      }, ms);
      this.timers.set(scanner.regulatorId, timer);
    }
  }
  stop() {
    for (const timer of this.timers.values()) clearInterval(timer);
    this.timers.clear();
  }
  /** Drop dedup state (test hook + manual reset). */
  resetDedup() {
    this.seen.clear();
  }
  /**
   * Project this orchestrator down to Phase A's RegulatorPublicationFetcher.
   * The watcher only sees the RegulatorUpdate fields; scanner-specific
   * metadata (citationId, hash, classification, industries) is stripped.
   */
  asFetcher() {
    return {
      fetchUpdates: async (regulatorId, since) => {
        const pubs = await this.runScanner(regulatorId, since);
        return pubs.map(toRegulatorUpdate);
      }
    };
  }
  markSeen(hash) {
    this.seen.set(hash, Date.now());
    if (this.seen.size > this.dedupCapacity) {
      const overflow = this.seen.size - this.dedupCapacity;
      const iter = this.seen.keys();
      for (let i = 0; i < overflow; i++) {
        const next = iter.next();
        if (next.done) break;
        this.seen.delete(next.value);
      }
    }
  }
};
function toRegulatorUpdate(pub) {
  return {
    id: pub.id,
    regulatorId: pub.regulatorId,
    title: pub.title,
    description: pub.description,
    url: pub.url,
    publishedAt: pub.publishedAt,
    detectedAt: pub.detectedAt,
    affectedCitationIds: pub.affectedCitationIds,
    newVersion: pub.newVersion
  };
}

// src/compliance-freshness/scanners/hash.ts
import { createHash } from "crypto";
function computePublicationHash(title, publishedAt, sourceUrl) {
  const normalized = `${title.trim()}
${publishedAt.toISOString()}
${sourceUrl.trim()}`;
  return createHash("sha256").update(normalized, "utf8").digest("hex");
}

// src/compliance-freshness/scanners/http.ts
var DEFAULT_TIMEOUT_MS = 15e3;
var USER_AGENT = "Sentinel-AI-Agent-Kit/0.29.0 ComplianceFreshnessScanner";
var defaultHttpClient = {
  async get(url, init = {}) {
    const controller = new AbortController();
    const timeoutMs = init.timeoutMs ?? DEFAULT_TIMEOUT_MS;
    const timer = setTimeout(() => controller.abort(), timeoutMs);
    try {
      const res = await fetch(url, {
        method: "GET",
        signal: controller.signal,
        headers: {
          "User-Agent": USER_AGENT,
          Accept: "text/html,application/xhtml+xml,application/xml,application/rss+xml;q=0.9,*/*;q=0.8",
          ...init.headers ?? {}
        }
      });
      const body = await res.text();
      if (res.status < 200 || res.status >= 300) {
        throw new Error(
          `HTTP ${res.status} for ${url} \u2014 body length ${body.length}`
        );
      }
      return { status: res.status, body };
    } finally {
      clearTimeout(timer);
    }
  }
};

// src/compliance-freshness/scanners/base.ts
import * as cheerio from "cheerio";
function summarize(text, limit = 500) {
  if (!text) return void 0;
  const cleaned = text.replace(/\s+/g, " ").trim();
  if (!cleaned) return void 0;
  return cleaned.length > limit ? `${cleaned.slice(0, limit - 1)}\u2026` : cleaned;
}
function buildPublication(input) {
  const detectedAt = input.now ?? /* @__PURE__ */ new Date();
  const hash = computePublicationHash(input.title, input.publishedAt, input.url);
  return {
    id: hash,
    regulatorId: input.regulatorId,
    title: input.title,
    description: input.description,
    url: input.url,
    publishedAt: input.publishedAt,
    detectedAt,
    hash,
    citationId: input.citationId,
    classification: input.classification,
    industryVerticals: input.industryVerticals,
    sourceUrl: input.url,
    summary: input.summary ?? summarize(input.description),
    rawBody: input.rawBody
  };
}
function isAfter(publishedAt, since) {
  if (!since) return true;
  return publishedAt.getTime() > since.getTime();
}
function safeParse(fn, logger, ctx) {
  try {
    return fn();
  } catch (err) {
    logger.warn("scanner entry parse failed", {
      ...ctx,
      error: err instanceof Error ? err.message : String(err)
    });
    return null;
  }
}
function loadHtml(body) {
  return cheerio.load(body);
}
function parseDate(raw) {
  if (!raw) return null;
  const trimmed = raw.trim();
  if (!trimmed) return null;
  const d = new Date(trimmed);
  if (Number.isNaN(d.getTime())) return null;
  return d;
}
function resolveUrl(href, base) {
  try {
    return new URL(href, base).toString();
  } catch {
    return href;
  }
}

// src/compliance-freshness/scanners/austrac.ts
var SOURCE_URL = "https://www.austrac.gov.au/news-and-media/news";
var INDUSTRIES = [
  "financial-advisory",
  "accounting",
  "legal",
  "real-estate"
];
var AustracScanner = class {
  constructor(http = defaultHttpClient, logger = consoleScannerLogger) {
    this.http = http;
    this.logger = logger;
  }
  http;
  logger;
  regulatorId = "AUSTRAC";
  intervalMinutes = CADENCE_MINUTES.weekly;
  async scan(since) {
    const { body } = await this.http.get(SOURCE_URL);
    const $ = loadHtml(body);
    const results = [];
    const candidates = $(
      "article, .views-row, li.news-item, .card, .news-listing__item"
    );
    candidates.each((_, el) => {
      const parsed = safeParse(
        () => {
          const $el = $(el);
          const $link = $el.find("a").first();
          const href = $link.attr("href");
          const title = $link.text().trim() || $el.find("h2, h3").first().text().trim();
          const dateRaw = $el.find("time").attr("datetime") ?? $el.find("time").first().text().trim() ?? $el.find(".date, .field--name-created").first().text().trim();
          const description = $el.find("p, .summary, .field--name-body").first().text().trim();
          if (!title || !href || !dateRaw) return null;
          const publishedAt = parseDate(dateRaw);
          if (!publishedAt) return null;
          const url = resolveUrl(href, SOURCE_URL);
          if (!isAfter(publishedAt, since)) return null;
          return buildPublication({
            regulatorId: this.regulatorId,
            title,
            description,
            url,
            publishedAt,
            classification: classifyAustrac(title, url),
            industryVerticals: INDUSTRIES,
            summary: summarize(description)
          });
        },
        this.logger,
        { scanner: this.regulatorId }
      );
      if (parsed) results.push(parsed);
    });
    this.logger.info("AUSTRAC scan complete", {
      found: results.length,
      since: since?.toISOString()
    });
    return results;
  }
};
function classifyAustrac(title, url) {
  const t = `${title} ${url}`.toLowerCase();
  if (t.includes("guidance")) return "guidance";
  if (t.includes("rule") || t.includes("determination")) return "determination";
  if (t.includes("policy")) return "policy-update";
  return "media-release";
}

// src/compliance-freshness/scanners/asic.ts
var MEDIA_URL = "https://asic.gov.au/about-asic/news-centre/find-a-media-release/";
var RG_URL = "https://asic.gov.au/regulatory-resources/regulatory-guides/";
var INDUSTRIES2 = ["financial-advisory", "accounting"];
var RG_CITATION_RE = /\bRG\s?(\d{1,4})\b/i;
var AsicScanner = class {
  constructor(http = defaultHttpClient, logger = consoleScannerLogger) {
    this.http = http;
    this.logger = logger;
  }
  http;
  logger;
  regulatorId = "ASIC";
  intervalMinutes = CADENCE_MINUTES.monthly;
  async scan(since) {
    const [media, rgs] = await Promise.allSettled([
      this.scanMediaReleases(since),
      this.scanRegulatoryGuides(since)
    ]);
    const results = [];
    if (media.status === "fulfilled") results.push(...media.value);
    else this.logger.warn("ASIC media-release scan failed", { error: String(media.reason) });
    if (rgs.status === "fulfilled") results.push(...rgs.value);
    else this.logger.warn("ASIC RG scan failed", { error: String(rgs.reason) });
    this.logger.info("ASIC scan complete", {
      found: results.length,
      since: since?.toISOString()
    });
    return results;
  }
  async scanMediaReleases(since) {
    const { body } = await this.http.get(MEDIA_URL);
    const $ = loadHtml(body);
    const results = [];
    $("article, .media-release, li.search-result, .views-row").each((_, el) => {
      const parsed = safeParse(
        () => {
          const $el = $(el);
          const $link = $el.find("a").first();
          const href = $link.attr("href");
          const title = $link.text().trim() || $el.find("h2, h3").first().text().trim();
          const dateRaw = $el.find("time").attr("datetime") ?? $el.find("time").first().text().trim() ?? $el.find(".date, .release-date").first().text().trim();
          const description = $el.find("p, .summary").first().text().trim();
          if (!title || !href || !dateRaw) return null;
          const publishedAt = parseDate(dateRaw);
          if (!publishedAt || !isAfter(publishedAt, since)) return null;
          return buildPublication({
            regulatorId: this.regulatorId,
            title,
            description,
            url: resolveUrl(href, MEDIA_URL),
            publishedAt,
            classification: "media-release",
            industryVerticals: INDUSTRIES2,
            summary: summarize(description)
          });
        },
        this.logger,
        { scanner: this.regulatorId, kind: "media-release" }
      );
      if (parsed) results.push(parsed);
    });
    return results;
  }
  async scanRegulatoryGuides(since) {
    const { body } = await this.http.get(RG_URL);
    const $ = loadHtml(body);
    const results = [];
    $("article, .regulatory-guide, li.search-result, .views-row, tr.rg-row").each(
      (_, el) => {
        const parsed = safeParse(
          () => {
            const $el = $(el);
            const $link = $el.find("a").first();
            const href = $link.attr("href");
            const title = $link.text().trim() || $el.find("h2, h3, td.title").first().text().trim();
            const dateRaw = $el.find("time").attr("datetime") ?? $el.find("time").first().text().trim() ?? $el.find(".date, .updated, td.updated").first().text().trim();
            const description = $el.find("p, .summary, td.description").first().text().trim();
            if (!title || !href || !dateRaw) return null;
            const publishedAt = parseDate(dateRaw);
            if (!publishedAt || !isAfter(publishedAt, since)) return null;
            const match = title.match(RG_CITATION_RE) ?? href.match(RG_CITATION_RE);
            const citationId = match ? `RG ${match[1]}` : void 0;
            return buildPublication({
              regulatorId: this.regulatorId,
              title,
              description,
              url: resolveUrl(href, RG_URL),
              publishedAt,
              classification: "regulatory-guide",
              industryVerticals: INDUSTRIES2,
              citationId,
              summary: summarize(description)
            });
          },
          this.logger,
          { scanner: this.regulatorId, kind: "regulatory-guide" }
        );
        if (parsed) results.push(parsed);
      }
    );
    return results;
  }
};

// src/compliance-freshness/scanners/ato.ts
var SOURCE_URL2 = "https://www.ato.gov.au/law/";
var INDUSTRIES3 = [
  "accounting",
  "financial-advisory",
  "legal"
];
var CITATION_RE = /\b(TR|TD|PCG|SMSFR|GSTR|GSTD|MT|LCR|FTR|TA)\s?(\d{2,4}\/\d{1,4}[A-Z]?)\b/i;
var AtoScanner = class {
  constructor(http = defaultHttpClient, logger = consoleScannerLogger) {
    this.http = http;
    this.logger = logger;
  }
  http;
  logger;
  regulatorId = "ATO";
  intervalMinutes = CADENCE_MINUTES.monthly;
  async scan(since) {
    const { body } = await this.http.get(SOURCE_URL2);
    const $ = loadHtml(body);
    const results = [];
    $("article, .law-listing__item, .ruling, .views-row, li.search-result").each(
      (_, el) => {
        const parsed = safeParse(
          () => {
            const $el = $(el);
            const $link = $el.find("a").first();
            const href = $link.attr("href");
            const title = $link.text().trim() || $el.find("h2, h3").first().text().trim();
            const dateRaw = $el.find("time").attr("datetime") ?? $el.find("time").first().text().trim() ?? $el.find(".date, .issued, .effective").first().text().trim();
            const description = $el.find("p, .summary, .lead").first().text().trim();
            if (!title || !href || !dateRaw) return null;
            const publishedAt = parseDate(dateRaw);
            if (!publishedAt || !isAfter(publishedAt, since)) return null;
            const url = resolveUrl(href, SOURCE_URL2);
            const match = title.match(CITATION_RE) ?? url.match(CITATION_RE);
            const citationId = match && match[1] && match[2] ? `${match[1].toUpperCase()} ${match[2]}` : void 0;
            return buildPublication({
              regulatorId: this.regulatorId,
              title,
              description,
              url,
              publishedAt,
              classification: classifyAto(citationId, title),
              industryVerticals: INDUSTRIES3,
              citationId,
              summary: summarize(description)
            });
          },
          this.logger,
          { scanner: this.regulatorId }
        );
        if (parsed) results.push(parsed);
      }
    );
    this.logger.info("ATO scan complete", {
      found: results.length,
      since: since?.toISOString()
    });
    return results;
  }
};
function classifyAto(citationId, title) {
  if (citationId) {
    const prefix = citationId.split(" ")[0]?.toUpperCase();
    if (prefix === "TR" || prefix === "GSTR" || prefix === "SMSFR" || prefix === "MT" || prefix === "LCR") {
      return "ruling";
    }
    if (prefix === "TD" || prefix === "GSTD" || prefix === "FTR") return "determination";
    if (prefix === "PCG") return "guidance";
    if (prefix === "TA") return "guidance";
  }
  const t = title.toLowerCase();
  if (t.includes("ruling")) return "ruling";
  if (t.includes("determination")) return "determination";
  if (t.includes("guideline") || t.includes("guidance")) return "guidance";
  return "other";
}

// src/compliance-freshness/scanners/ahpra.ts
var SOURCE_URL3 = "https://www.ahpra.gov.au/News/Notifications.aspx";
var INDUSTRIES4 = [
  "healthcare",
  "gp",
  "allied-health",
  "dental"
];
var AhpraScanner = class {
  constructor(http = defaultHttpClient, logger = consoleScannerLogger) {
    this.http = http;
    this.logger = logger;
  }
  http;
  logger;
  regulatorId = "AHPRA";
  intervalMinutes = CADENCE_MINUTES.monthly;
  async scan(since) {
    const { body } = await this.http.get(SOURCE_URL3);
    const $ = loadHtml(body);
    const results = [];
    $("article, .news-item, li.notification, .views-row, .news-listing__item").each(
      (_, el) => {
        const parsed = safeParse(
          () => {
            const $el = $(el);
            const $link = $el.find("a").first();
            const href = $link.attr("href");
            const title = $link.text().trim() || $el.find("h2, h3").first().text().trim();
            const dateRaw = $el.find("time").attr("datetime") ?? $el.find("time").first().text().trim() ?? $el.find(".date, .published, .news-date").first().text().trim();
            const description = $el.find("p, .summary").first().text().trim();
            if (!title || !href || !dateRaw) return null;
            const publishedAt = parseDate(dateRaw);
            if (!publishedAt || !isAfter(publishedAt, since)) return null;
            return buildPublication({
              regulatorId: this.regulatorId,
              title,
              description,
              url: resolveUrl(href, SOURCE_URL3),
              publishedAt,
              classification: classifyAhpra(title),
              industryVerticals: INDUSTRIES4,
              summary: summarize(description)
            });
          },
          this.logger,
          { scanner: this.regulatorId }
        );
        if (parsed) results.push(parsed);
      }
    );
    this.logger.info("AHPRA scan complete", {
      found: results.length,
      since: since?.toISOString()
    });
    return results;
  }
};
function classifyAhpra(title) {
  const t = title.toLowerCase();
  if (t.includes("standard") || t.includes("code")) return "standard-update";
  if (t.includes("policy")) return "policy-update";
  if (t.includes("guideline") || t.includes("guidance")) return "guidance";
  return "notice";
}

// src/compliance-freshness/scanners/ndis-commission.ts
var SOURCE_URL4 = "https://www.ndiscommission.gov.au/news-and-media";
var INDUSTRIES5 = ["ndis-provider", "allied-health"];
var NdisCommissionScanner = class {
  constructor(http = defaultHttpClient, logger = consoleScannerLogger) {
    this.http = http;
    this.logger = logger;
  }
  http;
  logger;
  regulatorId = "NDIS_COMMISSION";
  intervalMinutes = CADENCE_MINUTES.monthly;
  async scan(since) {
    const { body } = await this.http.get(SOURCE_URL4);
    const $ = loadHtml(body);
    const results = [];
    $("article, .news-item, li.news, .views-row, .media-item").each((_, el) => {
      const parsed = safeParse(
        () => {
          const $el = $(el);
          const $link = $el.find("a").first();
          const href = $link.attr("href");
          const title = $link.text().trim() || $el.find("h2, h3").first().text().trim();
          const dateRaw = $el.find("time").attr("datetime") ?? $el.find("time").first().text().trim() ?? $el.find(".date, .published, .news-date").first().text().trim();
          const description = $el.find("p, .summary").first().text().trim();
          if (!title || !href || !dateRaw) return null;
          const publishedAt = parseDate(dateRaw);
          if (!publishedAt || !isAfter(publishedAt, since)) return null;
          return buildPublication({
            regulatorId: this.regulatorId,
            title,
            description,
            url: resolveUrl(href, SOURCE_URL4),
            publishedAt,
            classification: classifyNdis(title),
            industryVerticals: INDUSTRIES5,
            summary: summarize(description)
          });
        },
        this.logger,
        { scanner: this.regulatorId }
      );
      if (parsed) results.push(parsed);
    });
    this.logger.info("NDIS Commission scan complete", {
      found: results.length,
      since: since?.toISOString()
    });
    return results;
  }
};
function classifyNdis(title) {
  const t = title.toLowerCase();
  if (t.includes("practice standard") || t.includes("standard")) return "standard-update";
  if (t.includes("policy")) return "policy-update";
  if (t.includes("guidance") || t.includes("guideline")) return "guidance";
  return "media-release";
}

// src/compliance-freshness/scanners/oaic.ts
var SOURCE_URL5 = "https://www.oaic.gov.au/news-and-information/media-releases";
var INDUSTRIES6 = ["all"];
var OaicScanner = class {
  constructor(http = defaultHttpClient, logger = consoleScannerLogger) {
    this.http = http;
    this.logger = logger;
  }
  http;
  logger;
  regulatorId = "OAIC";
  intervalMinutes = CADENCE_MINUTES.monthly;
  async scan(since) {
    const { body } = await this.http.get(SOURCE_URL5);
    const $ = loadHtml(body);
    const results = [];
    $("article, .media-release, .views-row, li.media-release, .card").each(
      (_, el) => {
        const parsed = safeParse(
          () => {
            const $el = $(el);
            const $link = $el.find("a").first();
            const href = $link.attr("href");
            const title = $link.text().trim() || $el.find("h2, h3").first().text().trim();
            const dateRaw = $el.find("time").attr("datetime") ?? $el.find("time").first().text().trim() ?? $el.find(".date, .release-date").first().text().trim();
            const description = $el.find("p, .summary").first().text().trim();
            if (!title || !href || !dateRaw) return null;
            const publishedAt = parseDate(dateRaw);
            if (!publishedAt || !isAfter(publishedAt, since)) return null;
            return buildPublication({
              regulatorId: this.regulatorId,
              title,
              description,
              url: resolveUrl(href, SOURCE_URL5),
              publishedAt,
              classification: classifyOaic(title),
              industryVerticals: INDUSTRIES6,
              summary: summarize(description)
            });
          },
          this.logger,
          { scanner: this.regulatorId }
        );
        if (parsed) results.push(parsed);
      }
    );
    this.logger.info("OAIC scan complete", {
      found: results.length,
      since: since?.toISOString()
    });
    return results;
  }
};
function classifyOaic(title) {
  const t = title.toLowerCase();
  if (t.includes("determination")) return "determination";
  if (t.includes("guidance") || t.includes("guideline")) return "guidance";
  if (t.includes("policy")) return "policy-update";
  if (t.includes("notice") || t.includes("notifiable")) return "notice";
  return "media-release";
}

// src/compliance-freshness/citations/types.ts
var DEFAULT_FRESHNESS_DAYS = {
  AUSTRAC: 30,
  ASIC: 30,
  ATO: 30,
  OAIC: 30,
  NDIS_COMMISSION: 60,
  AHPRA: 90,
  NHMRC: 180,
  default: 60
};

// src/compliance-freshness/citations/citation-watermark.ts
var MARKER_PATTERN = /\[citation:([a-z0-9_-]+)\/(\d{4}-\d{2}-\d{2})\/([^\]]+)\]/gi;
function toIsoDate(date) {
  return date.toISOString().slice(0, 10);
}
function slugify(input) {
  return input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
}
var CitationWatermark = class _CitationWatermark {
  /** Build the canonical marker text. */
  static format(marker) {
    const reg = marker.regulatorId.toLowerCase();
    const rule = marker.ruleId.trim();
    return `[citation:${reg}/${marker.publishedDate}/${rule}]`;
  }
  /**
   * Build a marker from a scanned publication.
   *
   * `citationId` is slugified so markers remain URL/markup-safe and compare
   * reliably ("RG 99" → "rg-99"). For matching purposes, downstream code
   * compares the slugified form, not the raw citationId.
   */
  static fromPublication(publication) {
    const rawCitation = publication.citationId?.trim();
    const ruleId = rawCitation ? slugify(rawCitation) || rawCitation : slugify(publication.title) || publication.hash.slice(0, 12);
    return {
      regulatorId: publication.regulatorId,
      publishedDate: toIsoDate(publication.publishedAt),
      ruleId
    };
  }
  /** Build a marker from a CitationStore record. */
  static fromCitationSource(source) {
    const ruleId = source.id;
    const publishedDate = source.effectiveDate ? toIsoDate(source.effectiveDate) : toIsoDate(source.observedAt);
    return {
      regulatorId: source.regulatorId,
      publishedDate,
      ruleId,
      citationSourceId: source.id
    };
  }
  /** Parse every marker out of a text. Order-preserving; duplicates retained. */
  static parseMarkers(text) {
    const out = [];
    MARKER_PATTERN.lastIndex = 0;
    let match;
    while ((match = MARKER_PATTERN.exec(text)) !== null) {
      const raw = match[0];
      const regulator = match[1];
      const date = match[2];
      const ruleId = match[3];
      if (regulator === void 0 || date === void 0 || ruleId === void 0) continue;
      out.push({
        raw,
        index: match.index,
        marker: {
          regulatorId: regulator.toUpperCase(),
          publishedDate: date,
          ruleId: ruleId.trim()
        }
      });
    }
    return out;
  }
  /**
   * Inserts a marker immediately after the first occurrence of `claim` in
   * `text`. No-op (returns the original text) if the claim isn't found —
   * callers should validate with `containsClaim()` first when that matters.
   */
  static insertMarker(text, claim, marker) {
    const idx = text.indexOf(claim);
    if (idx === -1) return text;
    const cut = idx + claim.length;
    const formatted = _CitationWatermark.format(marker);
    return `${text.slice(0, cut)} ${formatted}${text.slice(cut)}`;
  }
  /** Count of markers in a text (cheap pre-check before resolving). */
  static count(text) {
    MARKER_PATTERN.lastIndex = 0;
    let n = 0;
    while (MARKER_PATTERN.exec(text) !== null) n++;
    return n;
  }
  /** Strip all markers — used when generating clean copy for non-technical audiences. */
  static strip(text) {
    return text.replace(MARKER_PATTERN, "").replace(/ {2,}/g, " ").replace(/ +([.,;:])/g, "$1");
  }
  /**
   * Returns true iff the marker's regulator + date + ruleId match a
   * publication record. Used to validate Claude-generated drafts before
   * they leave Cora.
   */
  static matchesPublication(marker, publication) {
    if (marker.regulatorId.toUpperCase() !== publication.regulatorId.toUpperCase()) return false;
    if (marker.publishedDate !== toIsoDate(publication.publishedAt)) return false;
    const expected = _CitationWatermark.fromPublication(publication).ruleId;
    return marker.ruleId.toLowerCase() === expected.toLowerCase();
  }
};

// src/compliance-freshness/citations/citation-resolver.ts
var CitationResolver = class {
  citationStore;
  publicationIndex;
  now;
  constructor(opts) {
    this.citationStore = opts.citationStore;
    this.publicationIndex = opts.publicationIndex ?? null;
    this.now = opts.now ?? (() => /* @__PURE__ */ new Date());
  }
  /** Resolve a single marker (object or marker text). Returns null on parse failure. */
  async resolve(input) {
    const marker = typeof input === "string" ? parseSingleMarker(input) : input;
    if (!marker) return null;
    const raw = typeof input === "string" ? input : CitationWatermark.format(marker);
    return this.resolveMarker(marker, raw);
  }
  /** Resolve every marker in a text. Returns one entry per marker occurrence (duplicates kept). */
  async resolveAll(text) {
    const found = CitationWatermark.parseMarkers(text);
    const out = [];
    for (const { marker, raw } of found) {
      out.push(await this.resolveMarker(marker, raw));
    }
    return out;
  }
  async resolveMarker(marker, raw) {
    const publication = this.publicationIndex ? await this.publicationIndex.findByMarker(marker) : null;
    let source = null;
    if (marker.citationSourceId) {
      source = await this.citationStore.get(marker.citationSourceId);
    }
    if (!source) {
      source = await this.citationStore.get(marker.ruleId);
    }
    return {
      marker,
      rawMarker: raw,
      source,
      publication,
      retrievalDate: this.now()
    };
  }
};
function parseSingleMarker(text) {
  const found = CitationWatermark.parseMarkers(text);
  return found[0]?.marker ?? null;
}

// src/compliance-freshness/citations/staleness-detector.ts
var MS_PER_DAY = 24 * 60 * 60 * 1e3;
var StalenessDetector = class {
  thresholds;
  publicationIndex;
  constructor(options = {}) {
    this.thresholds = { ...DEFAULT_FRESHNESS_DAYS, ...options.freshnessDays ?? {} };
    this.publicationIndex = options.publicationIndex ?? null;
  }
  thresholdFor(regulatorId) {
    return this.thresholds[regulatorId] ?? this.thresholds.default ?? 60;
  }
  /** Scan a text body for citation markers and report staleness. */
  async detectInText(text, asOf = /* @__PURE__ */ new Date()) {
    const markers = CitationWatermark.parseMarkers(text);
    return this.detectMarkers(
      markers.map((m) => ({ marker: m.marker, raw: m.raw })),
      asOf
    );
  }
  /** Lower-level entry: pass parsed markers directly (Cora uses this after drafting). */
  async detectMarkers(markers, asOf = /* @__PURE__ */ new Date()) {
    const findings = [];
    let freshCount = 0;
    let staleCount = 0;
    let unresolvedCount = 0;
    for (const { marker, raw } of markers) {
      const finding = await this.evaluate(marker, raw, asOf);
      findings.push(finding);
      if (finding.status === "fresh") freshCount++;
      else if (finding.status === "stale") staleCount++;
      else unresolvedCount++;
    }
    return {
      totalMarkers: findings.length,
      freshCount,
      staleCount,
      unresolvedCount,
      findings,
      asOf
    };
  }
  async evaluate(marker, raw, asOf) {
    const threshold = this.thresholdFor(marker.regulatorId);
    const published = parseIsoDate(marker.publishedDate);
    if (!published) {
      return {
        marker,
        rawMarker: raw,
        regulatorId: marker.regulatorId,
        publishedDate: marker.publishedDate,
        ageDays: Number.POSITIVE_INFINITY,
        thresholdDays: threshold,
        status: "unresolved"
      };
    }
    const ageDays = Math.max(0, Math.floor((asOf.getTime() - published.getTime()) / MS_PER_DAY));
    const status = ageDays > threshold ? "stale" : "fresh";
    let supersededBy;
    if (status === "stale" && this.publicationIndex) {
      supersededBy = await this.findNewerWithSameRuleId(marker, published) ?? void 0;
    }
    return {
      marker,
      rawMarker: raw,
      regulatorId: marker.regulatorId,
      publishedDate: marker.publishedDate,
      ageDays,
      thresholdDays: threshold,
      status,
      supersededBy
    };
  }
  async findNewerWithSameRuleId(marker, publishedAfter) {
    if (!this.publicationIndex) return null;
    const candidates = await this.publicationIndex.listByRegulatorSince(
      marker.regulatorId,
      publishedAfter
    );
    const wanted = marker.ruleId.toLowerCase();
    const newer = candidates.filter((p) => p.publishedAt.getTime() > publishedAfter.getTime()).filter((p) => {
      const cid = (p.citationId ?? "").trim().toLowerCase();
      return cid === wanted || CitationWatermark.fromPublication(p).ruleId.toLowerCase() === wanted;
    }).sort((a, b) => b.publishedAt.getTime() - a.publishedAt.getTime());
    return newer[0] ?? null;
  }
};
function parseIsoDate(text) {
  if (!/^\d{4}-\d{2}-\d{2}$/.test(text)) return null;
  const date = /* @__PURE__ */ new Date(`${text}T00:00:00Z`);
  return Number.isNaN(date.getTime()) ? null : date;
}

// src/compliance-freshness/citations/memory-publication-index.ts
var InMemoryPublicationIndex = class {
  byHash = /* @__PURE__ */ new Map();
  async upsert(publication) {
    this.byHash.set(publication.hash, publication);
  }
  async getByHash(hash) {
    return this.byHash.get(hash) ?? null;
  }
  async findByMarker(marker) {
    const wantedRule = marker.ruleId.toLowerCase();
    for (const pub of this.byHash.values()) {
      if (pub.regulatorId.toUpperCase() !== marker.regulatorId.toUpperCase()) continue;
      if (pub.publishedAt.toISOString().slice(0, 10) !== marker.publishedDate) continue;
      const derived = CitationWatermark.fromPublication(pub).ruleId.toLowerCase();
      if (derived === wantedRule) {
        return pub;
      }
    }
    return null;
  }
  async listByRegulatorSince(regulatorId, since) {
    const target = regulatorId.toUpperCase();
    return Array.from(this.byHash.values()).filter((p) => p.regulatorId.toUpperCase() === target).filter((p) => p.publishedAt.getTime() >= since.getTime()).sort((a, b) => a.publishedAt.getTime() - b.publishedAt.getTime());
  }
};

// src/compliance-freshness/cross-reference/types.ts
var DEFAULT_REGULATOR_VERTICALS = {
  AUSTRAC: ["financial-advisory", "accounting", "legal", "real-estate"],
  ASIC: ["financial-advisory", "accounting"],
  ATO: ["accounting", "financial-advisory"],
  TPB: ["accounting"],
  AFCA: ["financial-advisory"],
  AHPRA: ["healthcare", "gp", "allied-health", "dental"],
  NHMRC: ["healthcare"],
  NDIS_COMMISSION: ["ndis-provider", "allied-health"],
  OAIC: ["all"],
  ACCC: ["all"],
  FAIR_TRADING_NSW: ["real-estate", "all"],
  FAIR_TRADING_VIC: ["real-estate", "all"],
  FAIR_TRADING_QLD: ["real-estate", "all"]
};

// src/compliance-freshness/cross-reference/cross-reference-matrix.ts
var IMPACT_ORDER = {
  informational: 0,
  advisory: 1,
  mandatory: 2
};
var IMPACT_BY_ORDER = ["informational", "advisory", "mandatory"];
function minImpact(a, b) {
  return IMPACT_ORDER[a] <= IMPACT_ORDER[b] ? a : b;
}
var CrossReferenceMatrix = class {
  adjacency;
  reverseAdjacency;
  nodes;
  constructor(rules) {
    this.adjacency = /* @__PURE__ */ new Map();
    this.reverseAdjacency = /* @__PURE__ */ new Map();
    this.nodes = /* @__PURE__ */ new Set();
    for (const rule of rules) {
      this.nodes.add(rule.primaryRegulatorId);
      const existing = this.adjacency.get(rule.primaryRegulatorId) ?? [];
      existing.push(rule);
      this.adjacency.set(rule.primaryRegulatorId, existing);
      for (const related of rule.relatedRegulatorIds) {
        this.nodes.add(related);
        const reverse = this.reverseAdjacency.get(related) ?? [];
        reverse.push(rule);
        this.reverseAdjacency.set(related, reverse);
      }
    }
  }
  /** All regulator IDs known to the matrix (primary or related). */
  regulators() {
    return Array.from(this.nodes);
  }
  /** Direct neighbours: regulators that `regulatorId` cascades into. */
  directRelated(regulatorId) {
    const rules = this.adjacency.get(regulatorId) ?? [];
    const ids = /* @__PURE__ */ new Set();
    for (const rule of rules) {
      for (const id of rule.relatedRegulatorIds) ids.add(id);
    }
    return Array.from(ids);
  }
  /** Inverse direct neighbours: regulators whose updates cascade INTO `regulatorId`. */
  upstreamOf(regulatorId) {
    const rules = this.reverseAdjacency.get(regulatorId) ?? [];
    return Array.from(new Set(rules.map((r) => r.primaryRegulatorId)));
  }
  /** True iff there is any (directional) edge from `a` to `b`. */
  areDirectlyRelated(a, b) {
    const rules = this.adjacency.get(a) ?? [];
    return rules.some((r) => r.relatedRegulatorIds.includes(b));
  }
  /** Adjacency map as plain primitives (handy for serialisation / debug). */
  toAdjacencyMap() {
    const out = /* @__PURE__ */ new Map();
    for (const [primary, rules] of this.adjacency) {
      const set = /* @__PURE__ */ new Set();
      for (const rule of rules) for (const r of rule.relatedRegulatorIds) set.add(r);
      out.set(primary, set);
    }
    return out;
  }
  /**
   * Returns every cascade path of length 1..maxDepth originating at
   * `regulatorId`. A path's `effectiveImpact` is the MIN impact along the
   * path — a mandatory→advisory chain becomes advisory at the leaf.
   *
   * Stops walking down a branch once it revisits a node (no cycles in output).
   */
  cascade(regulatorId, maxDepth = 2) {
    if (maxDepth < 1) return [];
    const paths = [];
    const walk = (current, depthRemaining, hops, visited, pathImpact) => {
      if (depthRemaining === 0) return;
      const rules = this.adjacency.get(current) ?? [];
      for (const rule of rules) {
        for (const next of rule.relatedRegulatorIds) {
          if (visited.has(next)) continue;
          const hop = { fromRegulatorId: current, toRegulatorId: next, rule };
          const nextHops = [...hops, hop];
          const nextImpact = pathImpact === null ? rule.impactLevel : minImpact(pathImpact, rule.impactLevel);
          paths.push({
            primaryRegulatorId: regulatorId,
            hops: nextHops,
            affectedRegulatorId: next,
            effectiveImpact: nextImpact
          });
          const nextVisited = new Set(visited);
          nextVisited.add(next);
          walk(next, depthRemaining - 1, nextHops, nextVisited, nextImpact);
        }
      }
    };
    const seed = /* @__PURE__ */ new Set([regulatorId]);
    walk(regulatorId, maxDepth, [], seed, null);
    return paths;
  }
  /**
   * Highest impact level reachable from `regulatorId` (direct only).
   * Returns null when no outgoing edges exist.
   */
  maxImpactLevel(regulatorId) {
    const rules = this.adjacency.get(regulatorId) ?? [];
    if (rules.length === 0) return null;
    let max = 0;
    for (const rule of rules) max = Math.max(max, IMPACT_ORDER[rule.impactLevel]);
    return IMPACT_BY_ORDER[max] ?? "informational";
  }
};

// src/compliance-freshness/cross-reference/cascade-detector.ts
var CascadeDetector = class {
  matrix;
  maxDepth;
  collapseDuplicates;
  constructor(matrix, options = {}) {
    this.matrix = matrix;
    this.maxDepth = options.maxDepth ?? 2;
    this.collapseDuplicates = options.collapseDuplicates ?? true;
  }
  /**
   * Detect cascades for a single publication. Returns an empty array when
   * the regulator has no outgoing edges.
   */
  detect(publication) {
    const paths = this.matrix.cascade(publication.regulatorId, this.maxDepth);
    if (paths.length === 0) return [];
    const winners = this.collapseDuplicates ? this.pickStrongestPerTarget(paths) : paths;
    return winners.map((path) => ({
      primaryPublication: publication,
      affectedRegulatorId: path.affectedRegulatorId,
      path,
      predictedImpact: this.predictedImpactFor(publication, path),
      rationale: this.explain(publication, path)
    }));
  }
  /** Detect cascades for a batch of publications, deduped per (publication, affectedRegulator). */
  detectBatch(publications) {
    const out = [];
    for (const pub of publications) {
      for (const implication of this.detect(pub)) {
        out.push(implication);
      }
    }
    return out;
  }
  /** Pick the strongest-impact path per affected regulator. */
  pickStrongestPerTarget(paths) {
    const order = {
      informational: 0,
      advisory: 1,
      mandatory: 2
    };
    const best = /* @__PURE__ */ new Map();
    for (const path of paths) {
      const current = best.get(path.affectedRegulatorId);
      if (current === void 0 || order[path.effectiveImpact] > order[current.effectiveImpact] || order[path.effectiveImpact] === order[current.effectiveImpact] && path.hops.length < current.hops.length) {
        best.set(path.affectedRegulatorId, path);
      }
    }
    return Array.from(best.values());
  }
  /**
   * A publication classified as "determination", "ruling" or "standard-update"
   * upgrades an advisory path to mandatory at the leaf; a media-release-only
   * publication downgrades a mandatory path to advisory.
   */
  predictedImpactFor(publication, path) {
    const base = path.effectiveImpact;
    switch (publication.classification) {
      case "determination":
      case "ruling":
      case "standard-update":
      case "regulatory-guide":
        return base === "informational" ? "advisory" : base === "advisory" ? "mandatory" : base;
      case "media-release":
        return base === "mandatory" ? "advisory" : base === "advisory" ? "informational" : base;
      default:
        return base;
    }
  }
  explain(publication, path) {
    const hopText = path.hops.map((h) => `${h.fromRegulatorId} \u2192 ${h.toRegulatorId} (${h.rule.impactLevel})`).join("; ");
    return `${publication.regulatorId} published "${publication.title}" (${publication.classification}). Cascade path: ${hopText}. Predicted impact at ${path.affectedRegulatorId}: ${this.predictedImpactFor(publication, path)}.`;
  }
};

// src/compliance-freshness/cross-reference/vertical-impact-mapper.ts
var IMPACT_WEIGHT = {
  informational: 0.4,
  advisory: 0.7,
  mandatory: 1
};
var VerticalImpactMapper = class {
  regulatorVerticals;
  xrefWeight;
  constructor(options = {}) {
    const merged = { ...DEFAULT_REGULATOR_VERTICALS, ...options.regulatorVerticals ?? {} };
    this.regulatorVerticals = new Map(Object.entries(merged));
    this.xrefWeight = options.crossReferenceConfidenceWeight ?? 0.6;
  }
  verticalsFor(regulatorId) {
    return this.regulatorVerticals.get(regulatorId) ?? [];
  }
  /**
   * Map publications + cascades to per-vertical impact records.
   *
   * One VerticalImpact per affected vertical. Confidence aggregates evidence:
   *   - scanner-declared vertical on a contributing publication contributes 1.0
   *   - cross-reference-derived contribution scales by xrefWeight × impact weight
   * Final confidence is clipped to [0, 1].
   */
  mapImpacts(publications, cascades) {
    const accumulator = /* @__PURE__ */ new Map();
    const bump = (vertical) => {
      let entry = accumulator.get(vertical);
      if (!entry) {
        entry = { publications: /* @__PURE__ */ new Set(), regulators: /* @__PURE__ */ new Set(), cascades: [], confidence: 0 };
        accumulator.set(vertical, entry);
      }
      return entry;
    };
    for (const pub of publications) {
      const decls = pub.industryVerticals ?? [];
      for (const v of decls) {
        const entry = bump(v);
        entry.publications.add(pub);
        entry.regulators.add(pub.regulatorId);
        entry.confidence = Math.min(1, entry.confidence + 1);
      }
      for (const v of this.verticalsFor(pub.regulatorId)) {
        if (decls.includes(v)) continue;
        const entry = bump(v);
        entry.publications.add(pub);
        entry.regulators.add(pub.regulatorId);
        entry.confidence = Math.min(1, entry.confidence + this.xrefWeight * 0.5);
      }
    }
    for (const cascade of cascades) {
      const reachedVerticals = new Set(
        this.verticalsFor(cascade.affectedRegulatorId)
      );
      const weight = this.xrefWeight * IMPACT_WEIGHT[cascade.predictedImpact];
      for (const v of reachedVerticals) {
        const entry = bump(v);
        entry.publications.add(cascade.primaryPublication);
        entry.regulators.add(cascade.affectedRegulatorId);
        entry.cascades.push(cascade);
        entry.confidence = Math.min(1, entry.confidence + weight);
      }
    }
    return Array.from(accumulator.entries()).map(([vertical, entry]) => ({
      vertical,
      affectedRegulatorIds: Array.from(entry.regulators).sort(),
      publications: Array.from(entry.publications),
      cascadeImplications: entry.cascades,
      confidence: Math.round(entry.confidence * 100) / 100
    })).sort((a, b) => b.confidence - a.confidence);
  }
};

// src/compliance-freshness/sub-agents/vera.ts
import { randomUUID as randomUUID2 } from "crypto";
var Vera = class {
  orchestrator;
  stalenessDetector;
  cascadeDetector;
  verticalImpactMapper;
  briefingStore;
  publicationIndex;
  logger;
  now;
  constructor(options = {}) {
    if (options.scannerOrchestrator) {
      this.orchestrator = options.scannerOrchestrator;
    } else if (options.scanners && options.scanners.length > 0) {
      const orch = new ScannerOrchestrator(
        options.logger ? { logger: options.logger } : {}
      );
      for (const scanner of options.scanners) orch.registerScanner(scanner);
      this.orchestrator = orch;
    } else {
      this.orchestrator = null;
    }
    this.stalenessDetector = options.stalenessDetector ?? null;
    this.cascadeDetector = options.cascadeDetector ?? null;
    this.verticalImpactMapper = options.verticalImpactMapper ?? null;
    this.briefingStore = options.briefingStore ?? null;
    this.publicationIndex = options.publicationIndex ?? null;
    this.logger = options.logger ?? null;
    this.now = options.now ?? (() => /* @__PURE__ */ new Date());
  }
  /**
   * Run a single watchdog sweep:
   *   1. Poll scanners for new publications since `since`
   *   2. Update the publication index (if provided)
   *   3. Predict cascades from the new publications
   *   4. Scan published briefings for citation staleness (best-effort)
   *   5. Return one VeraAlert per actionable finding
   */
  async sweep(opts = {}) {
    const alerts = [];
    const publications = await this.runScanners(opts.since);
    await this.indexPublications(publications);
    for (const pub of publications) {
      alerts.push(this.publicationAlert(pub));
    }
    if (this.cascadeDetector && publications.length > 0) {
      const cascades = this.cascadeDetector.detectBatch(publications);
      const verticalImpacts = this.verticalImpactMapper?.mapImpacts(publications, cascades) ?? [];
      for (const impl of cascades) {
        const verticalsForAlert = verticalImpacts.filter(
          (vi) => vi.affectedRegulatorIds.includes(impl.affectedRegulatorId)
        );
        alerts.push({
          id: randomUUID2(),
          type: "cascade",
          severity: this.severityFromImpact(impl.predictedImpact),
          briefingIds: [],
          regulatorIds: [impl.primaryPublication.regulatorId, impl.affectedRegulatorId],
          verticals: verticalsForAlert.map((v) => v.vertical),
          summary: this.cascadeSummary(impl),
          rationale: impl.rationale,
          publications: [impl.primaryPublication],
          cascades: [impl],
          verticalImpacts: verticalsForAlert,
          detectedAt: this.now()
        });
      }
    }
    const stale = await this.staleAlerts(opts.operatorIds ?? []);
    alerts.push(...stale);
    return alerts;
  }
  /**
   * Run sweep on an interval. Returns a disposer.
   *
   * Errors thrown by `sweep()` are logged but don't stop the loop — Vera is a
   * watchdog; it must survive transient scanner failures.
   */
  startContinuous(intervalMinutes, onAlert, opts = {}) {
    let stopped = false;
    let lastSweep = this.now();
    const run = async () => {
      if (stopped) return;
      try {
        const alerts = await this.sweep({
          since: lastSweep,
          ...opts.operatorIds ? { operatorIds: opts.operatorIds } : {}
        });
        lastSweep = this.now();
        for (const alert of alerts) onAlert(alert);
      } catch (err) {
        this.logger?.error("vera sweep failed", { err: errorMessage(err) });
      }
    };
    const handle = setInterval(run, intervalMinutes * 60 * 1e3);
    void run();
    return () => {
      stopped = true;
      clearInterval(handle);
    };
  }
  async runScanners(since) {
    if (!this.orchestrator) return [];
    try {
      return await this.orchestrator.runOnce(since);
    } catch (err) {
      this.logger?.error("vera scanner orchestration failed", { err: errorMessage(err) });
      return [];
    }
  }
  async indexPublications(publications) {
    if (!this.publicationIndex || publications.length === 0) return;
    for (const pub of publications) {
      try {
        await this.publicationIndex.upsert(pub);
      } catch (err) {
        this.logger?.warn("publication index upsert failed", {
          hash: pub.hash,
          err: errorMessage(err)
        });
      }
    }
  }
  async staleAlerts(operatorIds) {
    if (!this.stalenessDetector || !this.briefingStore || operatorIds.length === 0) {
      return [];
    }
    const out = [];
    for (const operatorId of operatorIds) {
      let briefings;
      try {
        briefings = await this.briefingStore.listPublished(operatorId);
      } catch (err) {
        this.logger?.warn("listPublished failed", { operatorId, err: errorMessage(err) });
        continue;
      }
      for (const briefing of briefings) {
        const report = await this.stalenessDetector.detectInText(briefing.content, this.now());
        if (report.staleCount === 0) continue;
        const staleFindings = report.findings.filter((f) => f.status === "stale");
        out.push({
          id: randomUUID2(),
          type: "staleness",
          severity: this.severityFromStaleness(staleFindings.length, report.totalMarkers),
          briefingIds: [briefing.id],
          regulatorIds: Array.from(new Set(staleFindings.map((f) => f.regulatorId))),
          verticals: [],
          summary: `${staleFindings.length} of ${report.totalMarkers} citations in "${briefing.title}" exceed freshness threshold`,
          rationale: staleFindings.map(
            (f) => `${f.regulatorId} citation "${f.marker.ruleId}" is ${f.ageDays}d old (threshold ${f.thresholdDays}d)${f.supersededBy ? `; superseded by "${f.supersededBy.title}"` : ""}`
          ).join("\n"),
          publications: staleFindings.map((f) => f.supersededBy).filter((p) => p !== void 0),
          cascades: [],
          verticalImpacts: [],
          detectedAt: this.now()
        });
      }
    }
    return out;
  }
  publicationAlert(pub) {
    return {
      id: randomUUID2(),
      type: "publication",
      severity: this.severityForPublication(pub),
      briefingIds: [],
      regulatorIds: [pub.regulatorId],
      verticals: pub.industryVerticals ?? [],
      summary: `${pub.regulatorId}: ${pub.title}`,
      rationale: pub.summary ?? pub.title,
      publications: [pub],
      cascades: [],
      verticalImpacts: [],
      detectedAt: this.now()
    };
  }
  cascadeSummary(impl) {
    return `${impl.primaryPublication.regulatorId} publication implies action at ${impl.affectedRegulatorId}: "${impl.primaryPublication.title}"`;
  }
  severityForPublication(pub) {
    switch (pub.classification) {
      case "determination":
      case "ruling":
      case "standard-update":
        return "high";
      case "regulatory-guide":
      case "policy-update":
      case "notice":
        return "medium";
      default:
        return "low";
    }
  }
  severityFromImpact(impact) {
    if (impact === "mandatory") return "critical";
    if (impact === "advisory") return "medium";
    return "low";
  }
  severityFromStaleness(staleCount, totalMarkers) {
    if (totalMarkers === 0) return "low";
    const ratio = staleCount / totalMarkers;
    if (ratio >= 0.5 || staleCount >= 5) return "critical";
    if (ratio >= 0.25 || staleCount >= 3) return "high";
    if (staleCount >= 2) return "medium";
    return "low";
  }
};
function errorMessage(err) {
  if (err instanceof Error) return err.message;
  return String(err);
}

// src/compliance-freshness/sub-agents/cora.ts
var DEFAULT_MAX_PUBS = 8;
var MS_PER_DAY2 = 24 * 60 * 60 * 1e3;
var IMPACT_ORDER2 = {
  informational: 0,
  advisory: 1,
  mandatory: 2
};
var IMPACT_BY_ORDER2 = ["informational", "advisory", "mandatory"];
var Cora = class {
  anthropic;
  publicationIndex;
  citationResolver;
  stalenessDetector;
  cascadeDetector;
  verticalImpactMapper;
  model;
  maxTokens;
  lookbackDays;
  now;
  constructor(opts) {
    this.anthropic = opts.anthropic;
    this.publicationIndex = opts.publicationIndex;
    this.citationResolver = opts.citationResolver;
    this.stalenessDetector = opts.stalenessDetector;
    this.cascadeDetector = opts.cascadeDetector;
    this.verticalImpactMapper = opts.verticalImpactMapper;
    this.model = opts.model ?? "claude-sonnet-4-6";
    this.maxTokens = opts.maxTokens ?? 2048;
    this.lookbackDays = opts.publicationLookbackDays ?? 365;
    this.now = opts.now ?? (() => /* @__PURE__ */ new Date());
  }
  /**
   * Build a citation-watermarked briefing for the requested topic.
   *
   * If `request.regulatorIds` is empty the briefing falls through to a
   * stub draft (no publications, no markers) — callers should always
   * scope at least one regulator.
   */
  async draftBriefing(request) {
    const asOf = request.asOf ?? this.now();
    const since = new Date(asOf.getTime() - this.lookbackDays * MS_PER_DAY2);
    const cap = request.maxPublications ?? DEFAULT_MAX_PUBS;
    const groundingPublications = await this.gatherPublications(request.regulatorIds, since, cap);
    const { title, content } = await this.callClaude(request, groundingPublications);
    const citations = await this.citationResolver.resolveAll(content);
    const markers = CitationWatermark.parseMarkers(content);
    const stalenessReport = await this.stalenessDetector.detectMarkers(
      markers.map((m) => ({ marker: m.marker, raw: m.raw })),
      asOf
    );
    const cascades = this.cascadeDetector.detectBatch(groundingPublications);
    const verticalImpacts = this.verticalImpactMapper.mapImpacts(groundingPublications, cascades);
    return {
      title,
      content,
      citations,
      stalenessReport,
      cascades,
      verticalImpacts,
      maxImpact: this.maxImpactAcross(cascades),
      generatedAt: this.now()
    };
  }
  /** Pull publications for the requested regulators, newest-first, up to `cap`. */
  async gatherPublications(regulatorIds, since, cap) {
    const merged = [];
    for (const regulatorId of regulatorIds) {
      const pubs = await this.publicationIndex.listByRegulatorSince(regulatorId, since);
      merged.push(...pubs);
    }
    merged.sort((a, b) => b.publishedAt.getTime() - a.publishedAt.getTime());
    return merged.slice(0, cap);
  }
  async callClaude(request, publications) {
    const pubsBlock = publications.length === 0 ? "(No grounding publications found for this scope. Decline to draft and surface the gap.)" : publications.map((p, i) => {
      const marker = CitationWatermark.format(CitationWatermark.fromPublication(p));
      return [
        `[${i + 1}] ${p.regulatorId} \u2014 ${p.title}`,
        `    Published: ${p.publishedAt.toISOString().slice(0, 10)}`,
        `    Classification: ${p.classification}`,
        `    Citation marker: ${marker}`,
        p.summary ? `    Summary: ${p.summary}` : ""
      ].filter(Boolean).join("\n");
    }).join("\n\n");
    const prompt = `You are Cora, a regulatory-navigation sub-agent producing client-ready compliance briefings for Australian operators.

## Briefing request
Topic: ${request.topic}
Audience: ${request.audience}
Regulators in scope: ${request.regulatorIds.join(", ") || "(none)"}

## Grounding publications
${pubsBlock}

## Drafting rules
1. EVERY factual claim about a regulator's position MUST be followed by the corresponding citation marker shown above. Markers look like \`[citation:austrac/2026-05-15/rule-x]\` \u2014 copy them verbatim.
2. Do NOT invent citation markers, regulator names, or rule IDs. If the grounding publications don't support a claim, omit the claim.
3. Use plain professional Australian English. No US spelling.
4. Output exactly two sections, separated by the literal line "---BODY---":
     Line 1: a Title Case briefing title (no leading "#")
     Then "---BODY---"
     Then the Markdown body. Body uses ## headings. Keep it under 600 words.
5. Do not write a preamble before the title. No closing sign-off.

Respond now.`;
    const response = await this.anthropic.messages.create({
      model: this.model,
      max_tokens: this.maxTokens,
      messages: [{ role: "user", content: prompt }]
    });
    const firstBlock = response.content[0];
    const raw = firstBlock !== void 0 && firstBlock.type === "text" ? firstBlock.text : "";
    const delim = "---BODY---";
    const idx = raw.indexOf(delim);
    if (idx === -1) {
      return {
        title: `${request.topic} briefing`,
        content: raw.trim()
      };
    }
    return {
      title: raw.slice(0, idx).trim() || `${request.topic} briefing`,
      content: raw.slice(idx + delim.length).trim()
    };
  }
  maxImpactAcross(cascades) {
    if (cascades.length === 0) return null;
    let max = 0;
    for (const c of cascades) max = Math.max(max, IMPACT_ORDER2[c.predictedImpact]);
    return IMPACT_BY_ORDER2[max] ?? "informational";
  }
};
export {
  AUSTRALIAN_CROSS_REFERENCE_RULES,
  AhpraScanner,
  AsicScanner,
  AtoScanner,
  AustracScanner,
  CascadeDetector,
  CitationResolver,
  CitationWatermark,
  Cora,
  CrossReferenceMap,
  CrossReferenceMatrix,
  DEFAULT_FRESHNESS_DAYS,
  DEFAULT_REGULATOR_VERTICALS,
  DriftDetector,
  InMemoryBriefingStore,
  InMemoryCitationStore,
  InMemoryInterventionStore,
  InMemoryPublicationIndex,
  InMemoryRegulatorPublicationFetcher,
  InMemoryVersionHistoryStore,
  InterventionDrafter,
  KNOWN_AUSTRALIAN_REGULATORS,
  NdisCommissionScanner,
  OaicScanner,
  RegulatorWatcher,
  ScannerOrchestrator,
  StalenessDetector,
  Vera,
  VersionHistory,
  VerticalImpactMapper,
  computePublicationHash
};
//# sourceMappingURL=index.mjs.map