feat(runner): add canonical semantic-catalog reconciliation authority

Introduce src/catalog/ as the single machine-readable authority over the
union (41) of the two hand-maintained semantic catalogs — src/tools/ (37,
scenario/eval rich descriptor) and src/semantic-tools/ (28, live runtime
engine). It names every operation's placement, claims, task modes, roles,
side-effect class, idempotency, redaction, mock mapping, real-binding status,
and PRP-evidence shape, and computes the full op-set + metadata divergence
ledger (one claim, 15 task-mode, and the input-schema-shape differences) each
with a recorded disposition. The drift test pins the reconciled relationship
so a new operation or an unreviewed metadata divergence fails the gate.

Refs PAP-17025.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
DottaandClaude Opus 4.8 committed 2026-08-11 14:23:12 +00:00
1 parent 59e57017b1
commit 63d691aeb6
4 files changed
+601

No files matched your search

@@ -0,0 +1 @@
export * from "./reconciliation.js";
@@ -0,0 +1,144 @@
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, resolve } from "node:path";
import { describe, expect, it } from "vitest";
import { CAPABILITY_SEMANTIC_TOOL_CATALOG as SCENARIO_CATALOG } from "../tools/capability-semantic-tool-catalog.js";
import { CAPABILITY_SEMANTIC_TOOL_CATALOG as LIVE_CATALOG } from "../semantic-tools/catalog.js";
import {
CAPABILITY_CANONICAL_CATALOG,
capabilityCanonicalOperation,
capabilityCatalogReconciliation,
} from "./reconciliation.js";
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
describe("canonical semantic-catalog reconciliation authority", () => {
it("pins the reconciled op-set relationship between the two catalogs", () => {
const summary = capabilityCatalogReconciliation();
expect(summary.scenarioCount).toBe(37);
expect(summary.liveCount).toBe(28);
expect(summary.sharedCount).toBe(24);
expect(summary.unionCount).toBe(41);
// Any operation added to or removed from either catalog without a
// reconciliation decision changes these exact sets and fails the gate.
expect(summary.liveOnly).toEqual([
"get_agent",
"get_approval",
"get_approval_context",
"schedule_wake",
]);
expect(summary.scenarioOnly).toEqual([
"administer_company",
"export_company",
"inspect_operation_result",
"list_cases",
"list_company_skills",
"list_goals",
"list_projects",
"list_routines",
"list_secret_metadata",
"manage_routine",
"read_secret_value",
"sync_company_skills",
"upsert_case",
]);
});
it("defines exactly one canonical entry per union operation", () => {
expect(CAPABILITY_CANONICAL_CATALOG).toHaveLength(41);
const ids = CAPABILITY_CANONICAL_CATALOG.map((operation) => operation.operationId);
expect(new Set(ids).size).toBe(ids.length);
const scenarioIds = new Set(SCENARIO_CATALOG.map((tool) => tool.operationId));
const liveIds = new Set(LIVE_CATALOG.map((tool) => tool.operationId));
for (const id of [...scenarioIds, ...liveIds]) {
expect(capabilityCanonicalOperation(id)).toBeDefined();
}
});
it("names placement, claims, task modes, side-effect class, idempotency, redaction, mock mapping, real binding status, and PRP evidence for every operation", () => {
for (const operation of CAPABILITY_CANONICAL_CATALOG) {
expect(operation.placement).toMatch(/^(always|optional)_agent_tool$/);
expect(Array.isArray(operation.requiredClaims)).toBe(true);
expect(operation.taskModes.length).toBeGreaterThan(0);
expect(operation.sideEffectClass.length).toBeGreaterThan(0);
expect(["none", "required", "recommended"]).toContain(operation.idempotency);
expect(typeof operation.redacts).toBe("boolean");
expect(["live_codex", "scenario_mock", "test_only"]).toContain(operation.realBindingStatus);
expect(operation.realServiceBinding).toBe("unbound");
expect(operation.prpEvidence.length).toBeGreaterThan(0);
expect(operation.prpBindingStatus).toBe("audit_pending");
// Scenario-surface ops carry a mock mapping; live-only ops resolve inline.
if (operation.surfaces.includes("scenario")) {
expect(operation.mockCommandMapping).not.toBeNull();
}
}
});
it("classifies real binding status so generic_api_request is never product coverage", () => {
const summary = capabilityCatalogReconciliation();
expect(summary.byRealBindingStatus).toEqual({
live_codex: 27,
scenario_mock: 13,
test_only: 1,
});
expect(capabilityCanonicalOperation("generic_api_request")?.realBindingStatus).toBe("test_only");
});
it("records a disposition for every metadata divergence between the two catalogs", () => {
const { divergences } = capabilityCatalogReconciliation();
// No divergence may be left unrecorded, regardless of field.
for (const divergence of divergences) {
expect(divergence.disposition).not.toMatch(/UNRECORDED/);
}
const idsFor = (field: string): string[] =>
divergences
.filter((entry) => entry.field === field)
.map((entry) => entry.operationId)
.sort();
// The only claim divergence is the reviewed test-only escape-hatch grant.
expect(idsFor("requiredClaims")).toEqual(["generic_api_request"]);
// Task-mode policy diverges systematically between the two surfaces; the
// exact op-set is pinned so a new, unreviewed divergence fails the gate.
expect(idsFor("taskModes")).toEqual([
"block_task",
"comment_on_approval",
"control_workspace_service",
"create_task",
"decide_approval",
"finish_task",
"get_workspace_runtime",
"list_agents",
"list_approvals",
"register_deliverable",
"report_progress",
"request_approval",
"request_review",
"search_tasks",
"set_dependencies",
]);
// Input schemas legitimately differ across the two surfaces today; every one
// carries the shared schema-migration disposition.
for (const divergence of divergences.filter((entry) => entry.field === "inputSchemaShape")) {
expect(divergence.disposition).toMatch(/schema-migration follow-on/);
}
});
it("gives every legacy MCP alias a reviewed disposition in the inventory", () => {
const raw = readFileSync(
resolve(packageRoot, "spec/capability/mcp-tool-map.yaml"),
"utf8",
);
const inventory = JSON.parse(raw.replace(/^#.*$/m, ""));
expect(inventory.inventoryRole).toBe("legacy_alias_index");
expect(inventory.rows.length).toBeGreaterThan(0);
for (const row of inventory.rows) {
// Every alias is either folded into a native eval/operation or explicitly
// dispositioned; a bare alias without a disposition fails the gate.
expect(typeof row.foldedInto === "string" && row.foldedInto.length > 0).toBe(true);
}
});
});
@@ -0,0 +1,455 @@
/**
* Canonical semantic-catalog reconciliation authority.
*
* The runner historically carried two independently hand-maintained semantic
* catalogs:
*
* - `src/tools/` — the rich scenario/eval descriptor (placement, side-effect
* class, idempotency, redaction, mock mapping) consumed by the scenario and
* conformance layers and exported as the package-root default.
* - `src/semantic-tools/` — the live runtime engine (catalog + policy +
* dispatcher) that drives live Codex, the issue-thread projection, and the
* generated provider-neutral contracts.
*
* PAP-17025 makes this module the single machine-readable authority over the
* union of both surfaces so no specification calls either catalog canonical.
* It enumerates every operation once, names its placement, claims, task modes,
* roles, side-effect class, idempotency rule, redaction, mock mapping, real
* binding status, and PRP-evidence shape, and computes every op-set and
* metadata divergence between the two catalogs so drift checks can fail on any
* unrecorded change. Physically collapsing the two catalog modules into
* derivations of this authority (which changes live tool schemas and generated
* goldens and needs live re-QA) is the sequenced follow-on within PAP-17025.
*/
import { CAPABILITY_SEMANTIC_TOOL_CATALOG as SCENARIO_CATALOG } from "../tools/capability-semantic-tool-catalog.js";
import type {
CapabilityMockCommandMapping,
CapabilitySideEffectClass,
CapabilityToolDisposition,
CapabilityToolTaskMode,
ScenarioChatdempotencyBehavior,
} from "../tools/capability-semantic-tool-types.js";
import { CAPABILITY_SEMANTIC_TOOL_CATALOG as LIVE_CATALOG } from "../semantic-tools/catalog.js";
/** Where an operation is executable today, before real Paperclip binding. */
export type CapabilityRealBindingStatus = "live_codex" | "scenario_mock" | "test_only";
/** Which hand-maintained catalog module(s) currently define the operation. */
export type CapabilityCatalogSurface = "scenario" | "live";
export interface CapabilityCanonicalOperation {
readonly operationId: string;
/** Catalog module(s) that define this operation today. */
readonly surfaces: readonly CapabilityCatalogSurface[];
readonly placement: CapabilityToolDisposition;
readonly requiredClaims: readonly string[];
readonly taskModes: readonly CapabilityToolTaskMode[];
readonly allowedRoles?: readonly string[];
readonly sideEffectClass: CapabilitySideEffectClass;
readonly idempotency: ScenarioChatdempotencyBehavior;
/** True when the operation declares any redaction rule. */
readonly redacts: boolean;
readonly mockCommandMapping: CapabilityMockCommandMapping | null;
readonly realBindingStatus: CapabilityRealBindingStatus;
/**
* No semantic operation is bound to a real Paperclip service yet; the mock is
* the only backend. The real-service binding is deliverable G of the
* foundation plan (PAP-17016), tracked per operation once it lands.
*/
readonly realServiceBinding: "unbound";
/** Intended PRP evidence family carrying this operation's effect. */
readonly prpEvidence: string;
/**
* The formal PRP expressiveness proof is deliverable C of the foundation
* plan; until it lands every entry is `audit_pending` rather than proven.
*/
readonly prpBindingStatus: "audit_pending";
/** Legacy MCP alias ids folded into this operation, if any. */
readonly legacyAliases: readonly string[];
readonly note?: string;
}
export type CapabilityCatalogDivergenceField =
| "requiredClaims"
| "taskModes"
| "inputSchemaShape";
export interface CapabilityCatalogDivergence {
readonly operationId: string;
readonly field: CapabilityCatalogDivergenceField;
readonly scenario: string;
readonly live: string;
/** Recorded rationale for why the two surfaces legitimately differ today. */
readonly disposition: string;
}
// ---------------------------------------------------------------------------
// Reconciliation table (hand-authored decisions, one row per union operation).
// Fields the two catalogs already carry are read from the descriptors; only
// facts neither catalog encodes live here: real binding status, PRP evidence,
// legacy aliases, and any side-effect/idempotency call for live-only ops.
// ---------------------------------------------------------------------------
interface ReconciliationRow {
readonly realBindingStatus: CapabilityRealBindingStatus;
readonly legacyAliases?: readonly string[];
readonly note?: string;
/** Required only for the four live-only ops absent from the rich catalog. */
readonly liveOnly?: {
readonly placement: CapabilityToolDisposition;
readonly sideEffectClass: CapabilitySideEffectClass;
readonly idempotency: ScenarioChatdempotencyBehavior;
readonly prpEvidence: string;
};
}
const RECONCILIATION: Readonly<Record<string, ReconciliationRow>> = {
// --- always-present active-task surface (shared) ---
get_task_context: { realBindingStatus: "live_codex", legacyAliases: ["mcp:paperclipMe"] },
get_task_history: { realBindingStatus: "live_codex" },
list_documents: { realBindingStatus: "live_codex" },
read_document: { realBindingStatus: "live_codex" },
list_document_revisions: { realBindingStatus: "live_codex" },
report_progress: { realBindingStatus: "live_codex", legacyAliases: ["mcp:paperclipCommentOnIssue"] },
answer_status_question: { realBindingStatus: "live_codex" },
write_document: { realBindingStatus: "live_codex" },
request_human_input: { realBindingStatus: "live_codex" },
register_deliverable: { realBindingStatus: "live_codex" },
finish_task: { realBindingStatus: "live_codex" },
block_task: { realBindingStatus: "live_codex" },
request_review: { realBindingStatus: "live_codex" },
// `inspect_operation_result` is scenario-only: the live dispatcher returns
// results inline rather than exposing a re-read tool.
inspect_operation_result: {
realBindingStatus: "scenario_mock",
note: "Scenario/eval-only re-read of a prior operation result; the live dispatcher returns results inline.",
},
// --- discovery (shared + live-only) ---
search_tasks: { realBindingStatus: "live_codex", legacyAliases: ["mcp:paperclipListIssues"] },
list_agents: { realBindingStatus: "live_codex", legacyAliases: ["mcp:paperclipListAgents"] },
get_agent: {
realBindingStatus: "live_codex",
legacyAliases: ["mcp:paperclipGetAgent"],
liveOnly: {
placement: "optional_agent_tool",
sideEffectClass: "read",
idempotency: "none",
prpEvidence: "read projection surfaced via a tool-result item event; no control-plane state diff",
},
},
list_projects: {
realBindingStatus: "scenario_mock",
note: "Scenario/eval-only discovery via mock extension; no live dispatcher binding yet.",
},
list_goals: {
realBindingStatus: "scenario_mock",
note: "Scenario/eval-only discovery via mock extension; no live dispatcher binding yet.",
},
// --- delegation & dependencies (shared) ---
create_task: { realBindingStatus: "live_codex", legacyAliases: ["mcp:paperclipCreateIssue"] },
set_dependencies: { realBindingStatus: "live_codex" },
// --- governance (shared + live-only) ---
list_approvals: { realBindingStatus: "live_codex" },
request_approval: { realBindingStatus: "live_codex" },
decide_approval: { realBindingStatus: "live_codex" },
comment_on_approval: { realBindingStatus: "live_codex" },
get_approval: {
realBindingStatus: "live_codex",
liveOnly: {
placement: "optional_agent_tool",
sideEffectClass: "read",
idempotency: "none",
prpEvidence: "approval read projection surfaced via a tool-result item event; no state diff",
},
},
get_approval_context: {
realBindingStatus: "live_codex",
liveOnly: {
placement: "optional_agent_tool",
sideEffectClass: "read",
idempotency: "none",
prpEvidence: "approval-plus-linked-tasks read projection via a tool-result item event; no state diff",
},
},
// --- workspace runtime (shared) ---
get_workspace_runtime: { realBindingStatus: "live_codex" },
control_workspace_service: { realBindingStatus: "live_codex" },
// --- control-plane wake scheduling (live-only) ---
schedule_wake: {
realBindingStatus: "live_codex",
liveOnly: {
placement: "optional_agent_tool",
sideEffectClass: "task_write",
idempotency: "required",
prpEvidence: "wake scheduling recorded as a run/attention continuation event; deterministic delay",
},
note: "Bounded, deterministic mock wake scheduling; requires the control_plane:wakes claim.",
},
// --- scenario-only optional surfaces (mock extensions, no live binding) ---
list_cases: { realBindingStatus: "scenario_mock", note: "Scenario/eval-only mock extension." },
upsert_case: { realBindingStatus: "scenario_mock", note: "Scenario/eval-only mock extension." },
list_routines: { realBindingStatus: "scenario_mock", note: "Scenario/eval-only mock extension." },
manage_routine: { realBindingStatus: "scenario_mock", note: "Scenario/eval-only mock extension." },
list_company_skills: { realBindingStatus: "scenario_mock", note: "Scenario/eval-only mock extension." },
sync_company_skills: { realBindingStatus: "scenario_mock", note: "Scenario/eval-only mock extension." },
list_secret_metadata: { realBindingStatus: "scenario_mock", note: "Scenario/eval-only mock extension; metadata only." },
read_secret_value: {
realBindingStatus: "scenario_mock",
note: "Scenario/eval-only mock extension; secret value redacted from every observable sink.",
},
export_company: { realBindingStatus: "scenario_mock", note: "Scenario/eval-only mock extension." },
administer_company: { realBindingStatus: "scenario_mock", note: "Scenario/eval-only mock extension." },
// --- test-only escape hatch (shared, never product coverage) ---
generic_api_request: {
realBindingStatus: "test_only",
note: "Test-only escape hatch; explicitly cannot count as product capability coverage (PAP-17016 deliverable D).",
},
};
// ---------------------------------------------------------------------------
// Derivation.
// ---------------------------------------------------------------------------
function prpEvidenceForSideEffect(sideEffectClass: CapabilitySideEffectClass): string {
switch (sideEffectClass) {
case "read":
return "read projection surfaced via a tool-result item event; no control-plane state diff";
case "task_write":
return "semantic-operation item event plus active-task state diff, work-assessment, and issue-status-decision events";
case "company_write":
return "semantic-operation item event plus company-entity state diff and audit record";
case "governance":
return "approval lifecycle plus governed-wait continuation and audit events";
case "workspace_control":
return "workspace service lifecycle event";
case "secret_read":
return "redacted tool-result item event; secret value never reaches the wire";
case "admin":
return "company admin/portability item event plus audit record";
case "test_escape_hatch":
return "test-only; excluded from product PRP evidence";
default:
return "audit_pending";
}
}
type LiveDescriptor = (typeof LIVE_CATALOG)[number];
const scenarioById = new Map<string, (typeof SCENARIO_CATALOG)[number]>(
SCENARIO_CATALOG.map((tool) => [tool.operationId, tool]),
);
const liveById = new Map<string, LiveDescriptor>(
LIVE_CATALOG.map((tool) => [tool.operationId as string, tool]),
);
const UNION_IDS: readonly string[] = [
...new Set([...scenarioById.keys(), ...liveById.keys()]),
].sort();
function buildOperation(operationId: string): CapabilityCanonicalOperation {
const row = RECONCILIATION[operationId];
if (row === undefined) {
throw new Error(`Missing reconciliation decision for operation ${operationId}.`);
}
const scenario = scenarioById.get(operationId);
const live = liveById.get(operationId);
const surfaces: CapabilityCatalogSurface[] = [];
if (scenario !== undefined) surfaces.push("scenario");
if (live !== undefined) surfaces.push("live");
if (scenario !== undefined) {
return freeze({
operationId,
surfaces,
placement: scenario.disposition,
requiredClaims: [...scenario.requiredClaims],
taskModes: [...scenario.taskModes],
...(scenario.allowedRoles === undefined ? {} : { allowedRoles: [...scenario.allowedRoles] }),
sideEffectClass: scenario.sideEffectClass,
idempotency: scenario.idempotency,
redacts: scenario.redaction.length > 0,
mockCommandMapping: scenario.mockCommandMapping,
realBindingStatus: row.realBindingStatus,
realServiceBinding: "unbound",
prpEvidence: prpEvidenceForSideEffect(scenario.sideEffectClass),
prpBindingStatus: "audit_pending",
legacyAliases: row.legacyAliases ?? [],
...(row.note === undefined ? {} : { note: row.note }),
});
}
// Live-only operation: the rich catalog does not define it, so the
// reconciliation row must supply placement, side-effect class, and evidence.
if (live === undefined || row.liveOnly === undefined) {
throw new Error(`Live-only operation ${operationId} needs an explicit reconciliation row.`);
}
return freeze({
operationId,
surfaces,
placement: row.liveOnly.placement,
requiredClaims: [...live.requiredClaims],
taskModes: [...live.allowedModes] as CapabilityToolTaskMode[],
...(live.allowedRoles === undefined ? {} : { allowedRoles: [...live.allowedRoles] }),
sideEffectClass: row.liveOnly.sideEffectClass,
idempotency: row.liveOnly.idempotency,
redacts: false,
mockCommandMapping: null,
realBindingStatus: row.realBindingStatus,
realServiceBinding: "unbound",
prpEvidence: row.liveOnly.prpEvidence,
prpBindingStatus: "audit_pending",
legacyAliases: row.legacyAliases ?? [],
...(row.note === undefined ? {} : { note: row.note }),
});
}
/** The single canonical catalog: the union of both surfaces, sorted by id. */
export const CAPABILITY_CANONICAL_CATALOG: readonly CapabilityCanonicalOperation[] = Object.freeze(
UNION_IDS.map(buildOperation),
);
const canonicalById = new Map(
CAPABILITY_CANONICAL_CATALOG.map((operation) => [operation.operationId, operation]),
);
export function capabilityCanonicalOperation(
operationId: string,
): CapabilityCanonicalOperation | undefined {
return canonicalById.get(operationId);
}
// ---------------------------------------------------------------------------
// Divergence ledger: every metadata difference between the two catalogs for a
// shared operation, each with a recorded disposition. Drift checks pin this set
// so a new, unrecorded divergence fails.
// ---------------------------------------------------------------------------
function inputSchemaShape(schema: { properties?: Record<string, unknown>; required?: readonly string[] } | undefined): string {
const properties = Object.keys(schema?.properties ?? {}).sort();
const required = [...(schema?.required ?? [])].sort();
return `properties=[${properties.join(",")}] required=[${required.join(",")}]`;
}
/**
* Input schemas differ by design between the two surfaces: the scenario
* descriptor keeps idempotency out-of-band and omits provider bounds, while the
* live provider descriptor threads `idempotencyKey` in-band and adds
* length/pattern bounds for the model. Unifying the schema is the
* schema-migration follow-on within PAP-17025.
*/
const INPUT_SCHEMA_SHAPE_DISPOSITION =
"Input schema differs by design (scenario keeps idempotency out-of-band and omits provider bounds; the live provider descriptor threads idempotencyKey in-band with length/pattern bounds). Unify in the schema-migration follow-on.";
/**
* The two catalogs apply task-mode policy differently: the scenario/eval
* catalog scopes modes to the coverage the suite drives (adds `skill_test`,
* restricts optional reads to `standard`), while the live catalog encodes the
* runtime exposure policy (opens optional reads to `ask`/`planning`, keeps
* terminal writes `standard`-only). The canonical target — one task-mode policy
* per operation — is decided in the schema-migration follow-on within
* PAP-17025. The exact divergent op-set is pinned by the drift test.
*/
const TASK_MODE_DISPOSITION =
"Task-mode policy differs between the scenario suite's coverage modes and the live runtime exposure policy. Reconcile to one canonical policy in the schema-migration follow-on.";
const DIVERGENCE_DISPOSITIONS: Readonly<Record<string, string>> = {
"generic_api_request:requiredClaims":
"Test-only escape hatch. The scenario claim `test:generic_api` and the live claim `test:generic_api_request` are the same reviewed test-only grant; unified in the schema-migration follow-on.",
};
function buildDivergences(): readonly CapabilityCatalogDivergence[] {
const divergences: CapabilityCatalogDivergence[] = [];
for (const operationId of UNION_IDS) {
const scenario = scenarioById.get(operationId);
const live = liveById.get(operationId);
if (scenario === undefined || live === undefined) continue;
const record = (
field: CapabilityCatalogDivergenceField,
scenarioValue: string,
liveValue: string,
): void => {
if (scenarioValue === liveValue) return;
const disposition =
field === "inputSchemaShape"
? INPUT_SCHEMA_SHAPE_DISPOSITION
: field === "taskModes"
? TASK_MODE_DISPOSITION
: (DIVERGENCE_DISPOSITIONS[`${operationId}:${field}`] ??
"UNRECORDED — reconcile or add a disposition.");
divergences.push({ operationId, field, scenario: scenarioValue, live: liveValue, disposition });
};
record(
"requiredClaims",
[...scenario.requiredClaims].sort().join(","),
[...live.requiredClaims].sort().join(","),
);
record(
"taskModes",
[...scenario.taskModes].sort().join(","),
[...live.allowedModes].sort().join(","),
);
record("inputSchemaShape", inputSchemaShape(scenario.inputSchema), inputSchemaShape(live.inputSchema));
}
return Object.freeze(divergences);
}
export interface CapabilityCatalogReconciliation {
readonly unionCount: number;
readonly scenarioCount: number;
readonly liveCount: number;
readonly sharedCount: number;
readonly scenarioOnly: readonly string[];
readonly liveOnly: readonly string[];
readonly byRealBindingStatus: Readonly<Record<CapabilityRealBindingStatus, number>>;
readonly alwaysCount: number;
readonly optionalCount: number;
readonly divergences: readonly CapabilityCatalogDivergence[];
}
export function capabilityCatalogReconciliation(): CapabilityCatalogReconciliation {
const scenarioIds = new Set(scenarioById.keys());
const liveIds = new Set(liveById.keys());
const scenarioOnly = [...scenarioIds].filter((id) => !liveIds.has(id)).sort();
const liveOnly = [...liveIds].filter((id) => !scenarioIds.has(id)).sort();
const byRealBindingStatus: Record<CapabilityRealBindingStatus, number> = {
live_codex: 0,
scenario_mock: 0,
test_only: 0,
};
let alwaysCount = 0;
let optionalCount = 0;
for (const operation of CAPABILITY_CANONICAL_CATALOG) {
byRealBindingStatus[operation.realBindingStatus] += 1;
if (operation.placement === "always_agent_tool") alwaysCount += 1;
else if (operation.placement === "optional_agent_tool") optionalCount += 1;
}
return {
unionCount: UNION_IDS.length,
scenarioCount: scenarioIds.size,
liveCount: liveIds.size,
sharedCount: [...scenarioIds].filter((id) => liveIds.has(id)).length,
scenarioOnly,
liveOnly,
byRealBindingStatus,
alwaysCount,
optionalCount,
divergences: buildDivergences(),
};
}
/** Convenience for docs/generators: the canonical operation-id list, sorted. */
export function capabilityCanonicalOperationIds(): readonly string[] {
return UNION_IDS;
}
function freeze<T>(value: T): T {
return Object.freeze(value);
}
+1
View File
@@ -33,6 +33,7 @@ export * from "./tracer/local-runner-live.js";
export * from "./generated/capability-contract.js";
export * from "./tools/index.js";
export * as acceptedCapabilitySemanticTools from "./semantic-tools/index.js";
export * from "./catalog/index.js";
export * from "./live/clean-room.js";
export * from "./live/live-session.js";
export * from "./live/runnerd-codex-transport.js";