Output Projection and Omission Contracts

Tracker references: pm-p258tx, pm-cyrfjq, and pm-qhnq6t. The universal row and intent-budget contracts are tracked by pm-sb0tns, pm-cxr0jb, and pm-5t33or. Reachable budgets and cursor-chain amortization are tracked by pm-7hbfch, pm-yekkvt, and pm-sf31yl. Trustworthy collection selectors are tracked by pm-x710qm. Default contract suppression and canonical TOON tables are tracked by pm-gjjurs and pm-5y05kq. Nested evidence continuation is tracked by pm-8nev0o and pm-oahhyc. Exact command-local projection discovery is tracked by pm-q4isdq. Shared graph defaults and scale enforcement are tracked by pm-mfy1ux.

For enforcement of declared MCP input options in repository tests, see MCP Option Conformance.

Agent Quick Context

Bounded reads must say what they withheld. Machine consumers should inspect omission_receipt before treating an absent field as absent project context:

{
  "omission_receipt": {
    "has_omissions": true,
    "omitted_field_group_count": 1,
    "omitted_field_groups": [{ "name": "provenance", "restore_with": "--full" }]
  }
}

The receipt is constant-sized per command. It does not grow with item count, history length, or workspace size. A complete projection still emits an explicit receipt with has_omissions: false, omitted_field_group_count: 0, and an empty omitted_field_groups list.

Mode-Paired Collections

Mutually exclusive output modes emit only their active row collection:

Command mode Active row key Withheld group Restore
activity (digest default) activity_digest event_rows --raw
activity --raw/--compact compact_activity provenance --full
activity --full activity none already complete
history (compact default) compact_history raw_history --full
history --full history none already complete

Inactive row keys are omitted, not zero-filled. This makes a wrong parser loud: reading .activity from compact activity now yields a missing key instead of a plausible empty result that contradicts count.

SDK authors can reuse createOutputOmissionReceipt, resolveModePairedOutputOmissionReceipt, and PM_MODE_PAIRED_OUTPUT_PROJECTION_CONTRACTS from @unbrained/pm-cli/sdk. The CLI uses the same declarations, so package integrations and built-in output cannot drift independently.

Universal Read Rows

Core read results expose row_contract only when callers request --output-row-contract / outputRowContract: true. Keeping discovery metadata off by default makes ordinary reads pay for project data rather than repeating the same selector declaration. The explicit contract remains available whether or not the current page has rows:

{
  "row_contract": {
    "command": "list",
    "row_kind": "collection",
    "row_keys": ["items"],
    "continuation_row_keys": ["items"],
    "fields": "supported",
    "jq_selector": ".row_contract.row_keys[] as $key | getpath($key | split(\".\")) | if type == \"array\" then .[] else if type == \"object\" then to_entries[] else empty end end",
    "toon_encoding": "tabular_when_uniform"
  }
}

The selector is identical for list aliases, context, next, search, activity, history, graph, health, aggregate, duplicates, stats, annotations (comments, notes, learnings), linked resources (files, docs), validation diagnostics, and command contracts. Commands with several collections declare every active dot-delimited path. This keeps nested dependency graph and relationship-context rows addressable as graph.nodes, graph.edges, context.nodes, and context.edges without duplicating them at the envelope root. Array collections produce their elements; object maps produce jq to_entries rows. continuation_row_keys is optional and defaults to row_keys. A command uses it only when independently resumable nested evidence differs from its primary amount-bounded rows. Validate, for example, keeps checks and warnings as primary rows while a rich result can name checks.0.details.missing_resolution_rows as a continuation row. This prevents an inner diagnostic array from disabling --output-limit on the outer checks. toon_encoding: "tabular_when_uniform" declares that an array of flat objects with one shared key set renders as a length-marked TOON table; mixed, nested, or heterogeneous arrays retain the expanded representation. Quoted, separator-bearing, and multiline values use the canonical TOON encoder and round-trip through the strict decoder. Commands without a row collection, including a dependency tree or leaf get, declare row_kind: "none", an empty row_keys array, and omit jq_selector. The absence is therefore distinguishable from a legitimate empty collection. Validation declares both checks and warnings; contracts declares both command_summaries and commands, allowing either projection to stay machine-iterable without a command-specific selector. fields is always explicit, so an agent can distinguish a supported --fields projection from a command that intentionally owns a fixed row shape. NDJSON event streams do not carry an envelope and therefore do not publish a row contract.

Projection mode discovery is command-local even though the compatibility degradation ladder is global. Read output_projection_contracts.commands from a command-scoped contracts call or the full contracts matrix before selecting a whole-result mode. The contract labels the global ladder union_not_per_command, so clients cannot infer that list accepts summary or that health accepts compact. PM_READ_OUTPUT_SURFACE_CONTRACTS[].projection_modes provides the identical canonical matrix to SDK and package consumers.

SDK and package authors can import PM_READ_ROW_CONTRACTS, PM_READ_ROW_JQ_SELECTOR, and resolveReadRowContract from @unbrained/pm-cli/sdk. Existing package declarations are preserved only when command, row_kind, row_keys, optional unique non-empty continuation_row_keys, fields, the conditional jq_selector, and any supplied toon_encoding form a structurally valid row contract; malformed declarations are replaced by the canonical built-in contract when one applies.

Self-Describing SDK Projections

Built-ins and extensions can declare bounded shapes directly on their result:

const result = {
  projection: {
    mode: "summary",
    declared_field_groups: [
      { name: "evidence_rows", restore_with: "--full" },
      { name: "risk_rows", restore_with: "--include risk" },
    ],
    included_field_groups: ["risk_rows"],
  },
};

attachOutputOmissionReceipt validates this declaration and derives the same constant-sized receipt used by built-in commands. Invalid or incomplete declarations are ignored rather than producing an untruthful restore instruction. Package authors therefore own the names and restoration controls for their domain without adding package-specific logic to the CLI renderer.

The following bounded built-ins now use this shared declaration:

Command mode Withheld group Restore
deps --summary selected dependency tree, graph, or relationship context --full
graph --summary result_rows --full
validate --counts diagnostic_rows --full

Passing --full with the corresponding compact flag is a usage error. An explicit full request and the default full request produce the same payload shape; the flag exists as a machine-actionable restore instruction.

Graph row collections default to ten rows, including hierarchy and ordering traversals, audit findings, and slack's critical path. The public GRAPH_QUERY_DEFAULTS contract is available from @unbrained/pm-cli/sdk/graph and @unbrained/pm-cli/sdk/contracts; pm contracts --command graph --flags-only declares the same defaults before invoking a graph query. Analytics still compute over the complete graph. --full restores row collections, while --limit chooses a finite cap; combining those two controls is rejected. Path enumeration retains its separate five-path, eight-edge-depth and 10,000-expansion safety defaults. Audit evidence uses --sample independently of the finding-row limit. The thousand-item graph regression enumerates every registered subcommand, bounds serialized output, and proves full traversal and cursor continuation.

graph impact returns next_cursor when more affected nodes exist, and accepts --after <cursor> to resume in stable breadth-first order. graph impact --full is the explicit unbounded override; combining --full with --limit is rejected. A zero-row page remains resumable: when --limit 0 truncates reachable work, its cursor represents the root boundary so a later positive-limit request can retrieve the first row. Impact cursors bind the root and traversal semantics, not the page size, so a caller may deliberately raise or lower --limit when resuming.

For get, each independently selectable group uses its composable field selector as the restore instruction: --fields children, --fields claim_state, or --fields linked. Combine them in one selector when several groups are needed. This remains truthful for leaf items because an explicit children projection returns an empty rollup instead of silently conflating “no children” with “children were not requested.”

Intent Budgets

Built-in read intents apply valid command-specific defaults and disclose the resolved contract in context_intent:

{
  "context_intent": {
    "command": "list",
    "intent": "triage",
    "token_budget": 3200,
    "estimated_tokens": 1416,
    "within_budget": true,
    "degradation": "bounded_fields_and_rows",
    "declaration_feasible": true,
    "result_omitted": false
  }
}

Use context --for orient|handoff, get --for inspect, list --for triage, next --for execute, or search --for discover. Explicit caller projection options win over intent defaults, while the intent ceiling still applies. Selecting an intent is an explicit request for its bounded shape: get --for inspect defaults to standard depth. List and search derive their default page size from the effective token ceiling and a conservative per-row cost, so raising --token-budget increases useful rows instead of paying the same fixed envelope cost for every page. Context applies the same principle to its focus and activity limits so the built-in orientation declaration remains feasible on a large tracker. All five intent commands accept the same --token-budget override. Explicit --depth and --limit controls still win.

If a selected result exceeds its budget, long explanatory strings compact first, followed by deterministic root-row reduction that retains at least one useful row and reports budget_row_compaction. Only a result whose minimum useful projection cannot fit becomes budget_receipt_only. That receipt sets declaration_feasible: false, result_omitted: true, and within_budget: false; fitting the refusal envelope does not make the omitted result truthful. Recovery recommends increasing the ceiling or narrowing the request and never sends an agent to a potentially larger unprojected retry. Explicit overrides below 256 tokens are rejected because the minimum machine-readable receipt cannot fit; malformed or absent overrides retain the declared intent budget.

The first cursor page carries the complete projection, filtering, sorting, completeness, row, and omission contracts. Continuation pages replace those chain-invariant blocks with continuation_contract, which carries the cursor's query fingerprint and a compact restore instruction. Continuations retain only the rows and next cursor needed to advance the chain; page counts, total, truncation, timestamps, and the full intent receipt are referenced from the first page instead of being re-emitted. Calls without --for remain byte-compatible with the ordinary projection path apart from the universal row contract.

The mandatory calibration gate generates both a two-item workspace and a 2,243-item current-scale workspace. It validates all five intents, removes one receipt as an enforcement negative control, traverses every list and search cursor without duplicates or omissions, and reconstructs repeated first-page metadata as a continuation negative control. The checked-in report is scripts/release/context-intent-calibration.json.

Intent 2-item tokens / budget 2,243-item tokens / budget Current-scale rows Degradation
context:orient 735 / 2,400 1,028 / 2,400 3 bounded sections
get:inspect 401 / 3,200 415 / 3,200 item envelope standard item
list:triage 443 / 3,200 3,189 / 3,200 69 budget-derived rows
next:execute 395 / 1,200 1,171 / 1,200 14 budget-derived rows
search:discover 350 / 1,800 1,761 / 1,800 27 budget-derived rows

Whole-answer cursor cost is measured against the unprojected single call for the identical ordered row set:

Family Rows Pages Optimized bytes/row Optimized walk Repeated-metadata control Unbounded call Walk / unbounded
list:triage 1,998 31 172.93 345,519 B 387,489 B 1,558,741 B 0.2217
search:discover 1,998 66 202.50 404,591 B 470,046 B 1,681,847 B 0.2406

These are corpus-generated figures, not live tracker payloads. The generated corpus contains 2,243 items; the status:all query intentionally excludes 245 canceled fixtures under the command's current status-selection contract.

Health Verdicts

Tracked by pm-fkohe8 and pm-du93sr.

health --check-only defaults to a summary verdict: all checks still run, but passing check evidence bodies are empty. Use health --check-only --full when diagnostic evidence is required. Explicit --brief or --summary retains the existing fast check-only mode that skips expensive optional scans.

Use top-level ok to decide health. Each checks[].ok uses the same rule: true means no gate-failing findings belong to that check. A check can therefore have status: "warn" and ok: true when all its findings are advisory. failed_because names every deciding warning, including findings beyond the bounded warning sample. findings[].severity distinguishes advisory from gate_failing evidence without parsing prose.

The verdict receipt names authority: "ok" and the invocation's exit_code. strict_exit: true and require_merge_drivers: true appear only when enabled; omission means false. The default invocation reports blocking findings without changing the process exit code. --strict-exit (alias --fail-on-warn) requires clone-local merge drivers and exits 1 when ok is false. Other advisory warnings retain their policy. Compare invocations with the same scan and policy options: enabling strict mode can legitimately change a missing-driver finding from advisory to blocking. The SDK accepts strictExit and failOnWarn with the same semantics, including MCP dispatch. Required merge-driver enforcement always runs the integrity check, even when skipIntegrity or an explicit compact check-only projection would otherwise skip optional integrity work.

The required token corpus measures health --check-only on its medium workspace under the health-default ceiling. Its positive and negative controls run in the same repository quality composition as the other answer budgets. Full evidence remains an explicit restore path; the verdict receipt does not raise the existing default token ceiling.

Linked-File Repair Rows

Tracked by pm-zw9188.

validate --check-files joins each missing path with its holders, lifecycle states, link fields, and candidate destinations before applying output limits. missing_linked_path_rows contains structured objects in both default and --verbose-file-lists output. The default caps paths and holders per path at 40; verbose output restores both collections. items_truncated and missing_linked_path_rows_truncated identify the two independent omissions.

validate --counts omits false *_truncated markers to keep its summary compact. True truncation markers, ordinary false values, and every zero count remain; its projection receipt declares that diagnostic rows were omitted.

Counts describe the complete scanned population:

  • missing_linked_links_count counts distinct path, holder, and field triples.
  • missing_linked_path_rows_count counts distinct paths.
  • active_missing_linked_links_count and active_missing_linked_paths_count identify actionable work under the configured workflow registry.
  • legacy_closed_missing_linked_links_count is the close-status subset of legacy_terminal_missing_linked_links_count.

Only active missing paths cause the missing-link warning. Historical terminal links remain visible for context reconstruction. Unknown statuses and absent holder metadata remain actionable. Rows and holders prioritize active work. Package authors can build the same pure report with buildLinkedFileRepairReport from @unbrained/pm-cli/sdk/governance and choose an explicit finite ceiling or Infinity for full output.

Same-basename destinations are possibilities requiring review. Missing CSV or path:note literals receive malformed classification and no inferred move; explicit pruning never removes that category. Existing literal filenames are checked before classification, including names containing commas or colons.

Completion Resolver Contract

resolveCompletionTimestamp accepts legacy metadata where all timestamp fields are optional. Its return is discriminated:

const completion = resolveCompletionTimestamp(item);
if (completion.resolved) {
  console.log(completion.timestamp, completion.source);
} else {
  console.log("No evidence-backed completion timestamp");
}

The resolved branch always carries timestamp, source, and fallback. The unresolved branch carries none of them, so TypeScript callers cannot accidentally attribute missing evidence to updated_at.

Compatibility

This is an intentional machine-output correction. Consumers that probed an inactive collection and received [] must branch on projection.mode or read the active row_contract.row_keys. Consumers that already used the populated key continue to receive the same rows.

Dependency token accounting includes the projection declaration and derived receipt. Reported usedTokens and truncation estimates therefore describe the final serialized result, not a pre-receipt intermediate.


Output_projection_contracts remote
Report an issue