harn-stdlib 0.10.130

Embedded Harn standard library source catalog
Documentation
// The declared-artifact contract: what a run was supposed to produce, and
// whether it exists when the run reaches its own end (harn#7915, cause 3).
//
// A model that calls tools successfully and then does the wrong thing leaves no
// in-run signal at all. Every tool call succeeded, so the tool tally in
// `std/agent/loop_terminal` is silent by construction and must stay silent. The
// missing evidence has to come from somewhere else, and the only party who
// knows what the run was for is the caller.
//
// So the caller declares it. `require_artifacts` names the paths the run is
// supposed to leave behind, and a declared path that does not exist is positive
// evidence of a run that did not do the work, in exactly the same way a failed
// tool call is. The check is deterministic, costs no model call, and does not
// consult the completion judge.
//
// WHAT THIS IS NOT. It is not a verifier. An existing file whose contents are
// wrong passes here, and should: proving the contents correct is the judge's
// job or the caller's own verification step. This answers the one question that
// can be answered without a model, and answers it honestly.
//
// A THIRD ANSWER, ON PURPOSE, AND WHY `exists` CANNOT GIVE IT. Existence has
// two answers only when the path is visible to this run at all. Under a
// capability policy that does not cover the path, `harness.fs.exists` returns
// FALSE rather than failing: the runtime collapses scope denial into absence by
// design, and its own capability-roots test says so. Auditing with `exists`
// would therefore convict a run for a path it was never permitted to look at,
// which blames the model for the host's policy.
//
// So the audit probes with `harness.fs.status`, the typed sibling that reports
// `scope_denied`, `read_only_denied` and `stat_error` distinctly from `missing`.
// Only `missing` convicts. Everything undecidable lands in `unverifiable`, and
// that list rides on the run record, so a contract that could not be checked
// can never read as a contract that was satisfied.
//
/**
 * The result of checking one run's declared artifacts.
 *
 * `declared` is the caller's list after normalization, so a reader can tell an
 * empty contract from an unread one. The three lists partition it: every
 * declared path lands in exactly one of present (implied by absence from the
 * other two), `missing`, or `unverifiable`.
 */
pub type AgentDeclaredArtifactAudit = {
  declared: list<string>,
  missing: list<string>,
  unverifiable: list<string>,
}

/**
 * The `harness.fs.status` statuses that mean the declared artifact is there.
 *
 * Listed rather than inferred so an unrecognized future status falls into
 * `unverifiable` instead of silently reading as present. A new status the
 * runtime adds must be classified deliberately, not absorbed by a default.
 */
const __AGENT_DECLARED_ARTIFACT_PRESENT_STATUSES: list<string> = [
  "present_file",
  "present_dir",
  "present_other",
]

/** One declared path's verdict: it is there, it is not, or it could not be told. */
pub type AgentDeclaredArtifactVerdict = "present" | "missing" | "unverifiable"

/**
 * Classify one `harness.fs.status` status string.
 *
 * Exported and pure so the rule that decides a conviction can be falsified
 * directly over every status the runtime documents, without needing a host
 * whose capability policy denies a read.
 *
 * `missing` is the only status that convicts. `scope_denied`,
 * `read_only_denied`, `stat_error` and anything unrecognized are undecidable
 * facts about the host rather than about the run.
 *
 * @effects: []
 * @errors: []
 * @api_stability: experimental
 * @example: agent_declared_artifact_verdict("scope_denied")
 */
pub fn agent_declared_artifact_verdict(status: string) -> AgentDeclaredArtifactVerdict {
  if contains(__AGENT_DECLARED_ARTIFACT_PRESENT_STATUSES, status) {
    return "present"
  }
  if status == "missing" {
    return "missing"
  }
  return "unverifiable"
}

fn __agent_declared_artifacts_from_options(opts: dict) -> list<string> {
  // Normalized once here so the terminal region, the evidence record, and any
  // future reader all see the same list. Blank entries are dropped rather than
  // checked: an empty path is a caller typo, and probing it would convict a run
  // for a mistake it did not make.
  const declared = opts?.require_artifacts
  if declared == nil {
    return []
  }
  let paths: list<string> = []
  for entry in declared {
    const path = trim(to_string(entry))
    if path != "" && !contains(paths, path) {
      paths = paths.appending(path)
    }
  }
  return paths
}

/**
 * Audit the declared artifacts of one run against the filesystem.
 *
 * Paths resolve the way every other `harness.fs` call resolves them, so a
 * relative path is relative to the same root the run's own tools wrote to.
 *
 * @effects: [fs]
 * @errors: []
 * @api_stability: experimental
 * @example: agent_declared_artifacts_audit(harness.fs, {require_artifacts: ["dist/out.js"]})
 */
pub fn agent_declared_artifacts_audit(fs: HarnessFs, opts: dict) -> AgentDeclaredArtifactAudit {
  const declared = __agent_declared_artifacts_from_options(opts)
  let missing: list<string> = []
  let unverifiable: list<string> = []
  for path in declared {
    // Three-valued on purpose; see the module comment. `missing` is the ONLY
    // status that convicts. A probe that itself failed is a fact about the
    // host, not about the run, and is recorded as such rather than guessed at.
    const status = try {
      to_string(fs.status(path)?.status ?? "stat_error")
    } catch (_error) {
      "stat_error"
    }
    const verdict = agent_declared_artifact_verdict(status)
    if verdict == "missing" {
      missing = missing.appending(path)
    } else if verdict == "unverifiable" {
      unverifiable = unverifiable.appending(path)
    }
  }
  return {declared: declared, missing: missing, unverifiable: unverifiable}
}

/**
 * Whether an audit found positive evidence that the run did not do the work.
 *
 * Named rather than inlined as `len(...) > 0` so that the one rule which
 * convicts stays in the module that owns the contract, and so a reader can see
 * at a glance that `unverifiable` is not part of it.
 *
 * @effects: []
 * @errors: []
 * @api_stability: experimental
 */
pub fn agent_declared_artifacts_convict(audit: AgentDeclaredArtifactAudit) -> bool {
  return len(audit.missing) > 0
}