Skip to content

JSON output

Pass -j or --json to any command for machine-readable output on stdout.

Human reports, banners, tips, and footers are suppressed. Errors still surface on stderr when not using --silent.

Shape

ts
type CliJsonEnvelope<K extends string, D> = {
  ok: boolean;
  kind: K;           // command name, e.g. 'validate' | 'inventory'
  data: D;           // command-specific payload
  issues: Issue[];   // structured errors/warnings
  meta: {
    apiVersion: string;   // envelope contract version (currently '1')
    cwd?: string;
    durationMs?: number;
    command?: string;
    schemaVersion?: string;
  };
};

type Issue = {
  severity: 'error' | 'warning' | 'info';
  code: string;
  message: string;
  path?: string;
};

apiVersion is RESULT_API_VERSION from @expgov/cli/core or @expgov/core — bump when the envelope shape changes.

--json already suppresses all human output; --silent is redundant when JSON mode is active.

Detail flags: With --json, -v / --verbose and -F / --full expand data with the same list payloads humans see. List policy is shared: -T / --top and -F / --full truncate or uncap JSON arrays the same way as human lists.

When a list is truncated, JSON includes a stable data.listGuidance block carrying both the flag and the guidance in one place:

json
{
  "listGuidance": {
    "truncated": true,
    "note": "symbols: 91 more hidden (showing 10 of 101). Use -F/--full for all rows, or -T/--top <n> to raise the cap."
  }
}

Agents should check listGuidance.truncated before assuming a list is complete; listGuidance.note explains how to expand it. Uncapped runs still emit listGuidance: { "truncated": false } (no note) whenever list sections are present.

Insights

Commands that emit insights always include data.insights as:

ts
{
  lines: Array<{ key: string; text: string }>; // max 5; may be []
  // plus optional command-specific typed fields
}

Empty insights are { "lines": [] } (never null). Human mode hides the Insights block when lines is empty.

Delta sign convention (typed fields and +/− text):

CommandPositive means
diffGrowth on the right side (right − left)
trendGrowth on the later tag (tags oldest → newest)
timelineGrowth on the newer commit (rows newest-first; delta = this row − older row below; oldest row delta is null)

Exit codes

CodeMeaning
0Command succeeded (ok: true)
1Validation or governance failure (validate with violations)
otherUnexpected errors

Thrown and usage errors

Errors raised before a command can finish also emit the standard envelope. The command stays in kind / meta.command; structured error details live at data.error:

bash
expgov diff missing-tag..HEAD -j
json
{
  "ok": false,
  "kind": "diff",
  "data": {
    "error": {
      "code": "unknown_ref",
      "message": "Unknown git ref \"missing-tag\"",
      "details": {
        "ref": "missing-tag",
        "suggestion": "Known version tags: v1.0.0, v1.0.1"
      }
    }
  },
  "issues": [
    {
      "severity": "error",
      "code": "unknown_ref",
      "message": "Unknown git ref \"missing-tag\""
    }
  ],
  "meta": { "apiVersion": "1", "command": "diff", "durationMs": 0 }
}

This applies to domain errors (unknown_ref, invalid_range, barrel_missing), unexpected execution errors (unexpected_error), and CLI parser errors (usage). Stdout remains parseable JSON and the process still exits non-zero.

Examples

validate (pass)

bash
expgov validate --json
json
{
  "ok": true,
  "kind": "validate",
  "data": {
    "passed": true,
    "violations": [],
    "notes": [],
    "sdkTiers": { "stable": 80, "advanced": 0, "internal": 0, "unclassified": 0 }
  },
  "issues": [],
  "meta": {
    "apiVersion": "1",
    "cwd": "/path/to/project",
    "durationMs": 42,
    "command": "validate"
  }
}

validate (fail)

When checks fail, ok is false, issues lists structured violations, and the process exits 1:

json
{
  "ok": false,
  "kind": "validate",
  "data": {
    "passed": false,
    "violations": ["3 unclassified root flat export(s)"],
    "notes": []
  },
  "issues": [
    {
      "severity": "error",
      "code": "expgov.validate.violation",
      "message": "3 unclassified root flat export(s)"
    }
  ],
  "meta": { "apiVersion": "1", "command": "validate", "durationMs": 38 }
}

validate --since

Baseline vs working tree (CLI --since, or git.compatBaseline when the flag is omitted; CLI wins). Removals use the same issue code as diff --fail-on-removed. data.since is the effective resolved ref ('latest-tag' becomes the newest matching tag):

bash
expgov validate --since v1.0.0 -j
json
{
  "ok": false,
  "kind": "validate",
  "data": {
    "passed": false,
    "violations": ["1 flat export removed: legacyHelper"],
    "notes": [],
    "since": "v1.0.0",
    "sinceLabel": "v1.0.0 → working tree",
    "added": ["newApi"],
    "removed": ["legacyHelper"]
  },
  "issues": [
    {
      "severity": "error",
      "code": "expgov.diff.exports_removed",
      "message": "1 flat export removed: legacyHelper"
    }
  ],
  "meta": { "apiVersion": "1", "command": "validate", "durationMs": 52 }
}

inventory

Default JSON is summary-only. Pass -v or -F to include root flat symbols and namespaces. Lists honor the same -T / -F policy as human verbose mode (default top 10; -F = uncapped, top serializes as null):

When --tier / --category / --namespace / --module / --subpath are set, JSON includes data.filters with only the active keys (omitted entirely when none apply).

Warn diagnostics (direct barrel decls, unreachable module exports) appear in top-level issues[] with ok: true — they do not fail the command. Codes: expgov.inventory.direct_barrel_export, expgov.inventory.unreachable_module_exports. Optional samples[] lists export names; human Diagnostics shows path + message, then up to 3 samples on the next line (and respects -T/-F for how many diagnostic rows print).

bash
expgov inventory -v -j          # top 10 symbols + namespacesHidden
expgov inventory -v -T 5 -j     # top 5
expgov inventory -F -j          # all symbols (same as -v -F)
json
{
  "ok": true,
  "kind": "inventory",
  "data": {
    "ref": "worktree",
    "sha": "__worktree__",
    "summary": { "root": { "flat": 80, "namespace": 0 }, "subpaths": [] },
    "cache": { "status": "hit" },
    "insights": {
      "lines": [{ "key": "largest-module", "text": "largest module: … (N edges, M flats)" }],
      "largestModule": { "path": "packages/core/src/…", "count": 12 }
    },
    "top": 10,
    "symbols": [
      {
        "name": "runValidate",
        "tier": "stable",
        "category": "run",
        "symbolKind": "function",
        "targetSubpath": "./commands/validate",
        "module": "packages/core/src/commands/validate.ts"
      }
    ],
    "namespaces": [],
    "symbolsHidden": 70,
    "namespacesHidden": 0,
    "listGuidance": {
      "truncated": true,
      "note": "symbols: 70 more hidden (showing 10 of 80). Use -F/--full for all rows, or -T/--top <n> to raise the cap."
    }
  },
  "issues": [],
  "meta": { "apiVersion": "1", "command": "inventory", "durationMs": 12 }
}

Use summary.root.flat for the true total; symbols.length + symbolsHidden matches that total when detail is present. Omit -v/-F/--names-only and those list fields are absent.

With --names-only (alone or with -v/-F), detail arrays are bare name strings and data.namesOnly is true:

json
{
  "namesOnly": true,
  "symbols": ["runValidate", "runInventory"],
  "namespaces": ["Cli"]
}

graph

Default JSON is analytics + target groups. Pass -v, -F, or --names-only to include re-export edges[] under the same -T/-F list policy + listGuidance. With --names-only, edges is unique sorted symbol names and data.namesOnly is true.

bash
expgov graph -v -j
expgov graph -F -j
json
{
  "ok": true,
  "kind": "graph",
  "data": {
    "ref": "worktree",
    "edgeCount": 120,
    "targetGroups": [{ "targetSubpath": "./commands", "flat": 9, "namespace": 0 }],
    "analytics": { "edgeDensity": 1.2, "hottestModule": { "path": "…", "edges": 12 } },
    "insights": { "lines": [] },
    "top": 10,
    "edges": [
      {
        "kind": "flat-reexport",
        "from": "packages/core/src/index.ts",
        "symbol": "runValidate",
        "toModule": "packages/core/src/commands/validate.ts",
        "targetSubpath": "./commands/validate"
      }
    ],
    "edgesHidden": 110,
    "listGuidance": {
      "truncated": true,
      "note": "edges: 110 more hidden (showing 10 of 120). Use -F/--full for all rows, or -T/--top <n> to raise the cap."
    }
  },
  "issues": [],
  "meta": { "apiVersion": "1", "command": "graph", "durationMs": 18 }
}

edgeCount is the true total; edges.length + edgesHidden matches it when detail is present.

diff

Default JSON always includes complete added / removed name arrays (and tierViolations) — do not truncate those for CI. Pass -v or -F for rich symbol detail under the shared -T/-F list policy:

bash
expgov diff v1.0.0..HEAD -v -j
expgov diff v1.0.0..HEAD -F -j
json
{
  "ok": true,
  "kind": "diff",
  "data": {
    "rangeLabel": "v1.0.0 → HEAD",
    "added": ["newApi"],
    "removed": ["legacyHelper"],
    "tierViolations": [],
    "top": 10,
    "addedDetail": [
      {
        "name": "newApi",
        "tier": "stable",
        "category": "run",
        "symbolKind": "function",
        "targetSubpath": ".",
        "module": "packages/core/src/commands/new.ts"
      }
    ],
    "removedDetail": [
      {
        "name": "legacyHelper",
        "tier": "stable",
        "category": "other",
        "symbolKind": "function",
        "targetSubpath": ".",
        "module": "packages/core/src/legacy.ts"
      }
    ],
    "addedDetailHidden": 0,
    "removedDetailHidden": 0,
    "listGuidance": { "truncated": false }
  },
  "issues": [],
  "meta": { "apiVersion": "1", "command": "diff", "durationMs": 40 }
}

Use added / removed for complete name sets; use *Detail when agents need tier/module metadata. Omit -v/-F and detail fields are absent.

kind values

kindCommand
inventoryexpgov inventory
diffexpgov diff
validateexpgov validate
doctorexpgov doctor
suggestexpgov suggest
trendexpgov trend
timelineexpgov timeline
graphexpgov graph

CI usage

bash
pnpm build
expgov validate --since v1.0.0 --json > validate.json
test "$(jq -r .ok validate.json)" = "true"

When --since is set, data also includes since, sinceLabel, added, and removed. Failures may mix expgov.validate.violation with expgov.diff.exports_removed in issues[].

Surface-only regression gate (opt-in on diff):

bash
expgov diff v1.0.0..HEAD --fail-on-removed --json > diff.json
test "$(jq -r .ok diff.json)" = "true"

Failing diff runs emit issues[] with expgov.diff.exports_removed and/or expgov.diff.tier_violation. Parse ok and issues for automation. Exit-code-only gates work without --json.