Source: lib/core/trace/contracts.js

/**
 * @file contracts.js
 * @description Checks declared compiler contracts against what a trace shows
 * the code actually did.
 *
 * Avenx's contracts (`static`, `pure`, `deterministic`, `isolated`) are
 * declared in a component and checked by the compiler with pattern matching
 * over source text. That catches the obvious cases and misses everything
 * reached indirectly: a computed that calls a helper that calls `Date.now()`
 * looks pure to a regular expression.
 *
 * A trace closes that gap without any new machinery. It already records where
 * every non-deterministic global was read and where every state write
 * happened, and every node knows its causal ancestry — so asking "did anything
 * inside a unit that declared itself `deterministic` read the clock?" is a walk
 * up the parent chain. The contract stops being a build-time assertion and
 * becomes a claim checked against the run.
 *
 * The diagnostic codes are the compiler's existing ones, deliberately: a
 * developer who has seen AVX_W33 at build time should recognise it when it
 * arrives from a recording, and `avenx explain AVX_W33` should still be the
 * right thing to type.
 * @module lib/core/trace/contracts
 */

import { AvenxErrorCodes } from '../runtime/AvenxError.js';
import { TraceNodeType, indexNodes } from './schema.js';

/**
 * Walks a node's ancestry, nearest first.
 * @param {object} node - The starting node.
 * @param {Map<number, object>} byId - Nodes keyed by id.
 * @yields {object} Each ancestor, nearest first.
 */
function* ancestors(node, byId) {
  let current = node;
  while (current && current.parent !== null && current.parent !== undefined) {
    const parent = byId.get(current.parent);
    if (!parent) {
      return;
    }
    yield parent;
    current = parent;
  }
}

/**
 * Finds the nearest ancestor that declared a given contract.
 * @param {object} node - The starting node.
 * @param {Map<number, object>} byId - Nodes keyed by id.
 * @param {string} contract - The contract name.
 * @returns {object|null} The declaring ancestor, if any.
 */
function nearestDeclaring(node, byId, contract) {
  for (const ancestor of ancestors(node, byId)) {
    if (Array.isArray(ancestor.contracts) && ancestor.contracts.includes(contract)) {
      return ancestor;
    }
  }
  return null;
}

/**
 * Describes a unit for a diagnostic message.
 * @param {object} node - An action or computed node.
 * @returns {string} A short label such as `CartItem.incQty()`.
 */
function label(node) {
  const ownerName = node.component || node.bridge;
  const owner = ownerName ? `${ownerName}.` : '';
  if (node.type === TraceNodeType.ACTION) {
    return `${owner}${node.name}()`;
  }
  if (node.type === TraceNodeType.COMPUTED) {
    return `${owner}${node.name}`;
  }
  return owner || node.type;
}

/**
 * Checks a trace's declared contracts against what it recorded.
 *
 * Two contracts are checkable from a trace:
 *
 * - `deterministic` — a unit that declared it must not have read `Date.now()`,
 *   `new Date()` or `Math.random()`. The trace records exactly where those
 *   reads happened.
 * - `pure` — a unit that declared it must not have mutated reactive state.
 *   The trace records every write and what caused it.
 *
 * `static` and `isolated` are structural claims the compiler can decide
 * completely from source, so a trace adds nothing to them and they are not
 * re-checked here.
 * @param {object} trace - The trace to check.
 * @returns {Array<{code: string, contract: string, unit: string, detail: string, nodeId: number}>}
 *   One entry per violation, in recorded order.
 */
export function findContractViolations(trace) {
  const nodes = (trace && trace.nodes) || [];
  const byId = indexNodes({ nodes });
  const violations = [];

  for (const node of nodes) {
    if (node.type === TraceNodeType.GLOBAL) {
      const declaring = nearestDeclaring(node, byId, 'deterministic');
      if (declaring) {
        violations.push({
          code: AvenxErrorCodes.COMPILER_CONTRACT_DETERMINISTIC_VIOLATION,
          contract: 'deterministic',
          unit: label(declaring),
          detail: `read ${node.source} during this trace`,
          nodeId: node.id,
        });
      }
      continue;
    }

    if (node.type === TraceNodeType.WRITE) {
      const declaring = nearestDeclaring(node, byId, 'pure');
      if (declaring) {
        violations.push({
          code: AvenxErrorCodes.COMPILER_CONTRACT_PURE_VIOLATION,
          contract: 'pure',
          unit: label(declaring),
          detail: `wrote ${node.path} during this trace`,
          nodeId: node.id,
        });
      }
    }
  }

  return violations;
}

/**
 * Renders a violation as a single diagnostic line.
 * @param {object} violation - A violation from {@link findContractViolations}.
 * @returns {string} A line suitable for `avenx trace view`.
 */
export function formatViolation(violation) {
  return `${violation.code}  ${violation.unit} is declared \`${violation.contract}\` but ${violation.detail}`;
}