Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [3.0.0] - 2026-02-13

### Added

- **`schemaVersion: 1` and `command` fields in all `--json` outputs** — Every CLI command that supports `--json` now includes a `schemaVersion` (integer, currently `1`) and `command` (string) field in its output envelope. The CLI layer is authoritative via the `outputJson()` helper (#205)
- **JSON Schema files** — 13 Draft 2020-12 JSON Schema files in `docs/contracts/cli/` for programmatic validation of every `--json` output. Strict `additionalProperties: false` at top level; open objects where extensibility is intentional (node properties, prefix maps) (#205)
- **Contract validation tests** — `test/contracts.test.js` (17 unit tests) validates schema compilation, envelope requirements, sample payloads, and optional field handling. `test/contracts.integration.test.js` (8 CLI canary tests) executes the real binary and validates output against schemas using `ajv` (#205)
- **CLI Contracts documentation** — `docs/contracts/CLI_CONTRACTS.md` with version policy, command-to-schema table, programmatic validation example, and migration guide (#205)

### Breaking

- **`nodes --json` output wrapped** — Previously returned a bare JSON array; now returns `{ schemaVersion: 1, command: "nodes", nodes: [...] }`. Migration: `jq '.[]'` → `jq '.nodes[]'` (#205)
- **`review --json` output wrapped** — Previously returned a bare JSON array; now returns `{ schemaVersion: 1, command: "review", pending: [...] }`. Migration: `jq '.[].source'` → `jq '.pending[].source'` (#205)

### Changed

- **Test count** — 367 tests across 22 files (was 342 across 20)

## [2.0.0-alpha.5] - 2026-02-13

### Added
Expand Down Expand Up @@ -219,6 +237,7 @@ Complete rewrite from C23 to Node.js on `@git-stunts/git-warp`.
- Docker-based CI/CD
- All C-specific documentation

[3.0.0]: https://github.com/neuroglyph/git-mind/releases/tag/v3.0.0
[2.0.0-alpha.5]: https://github.com/neuroglyph/git-mind/releases/tag/v2.0.0-alpha.5
[2.0.0-alpha.4]: https://github.com/neuroglyph/git-mind/releases/tag/v2.0.0-alpha.4
[2.0.0-alpha.3]: https://github.com/neuroglyph/git-mind/releases/tag/v2.0.0-alpha.3
Expand Down
7 changes: 5 additions & 2 deletions GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -624,13 +624,16 @@ git mind diff HEAD~5..HEAD --prefix module

### JSON output and versioning

The `--json` output includes a `schemaVersion` field (currently `1`). Breaking changes to the JSON structure will increment this version, so downstream tools can detect incompatible output.
All `--json` outputs include a standard envelope with `schemaVersion` (currently `1`) and `command` fields. Breaking changes to any JSON structure will increment `schemaVersion`, so downstream tools can detect incompatible output.

```bash
git mind diff HEAD~5..HEAD --json | jq '.schemaVersion'
git mind status --json | jq '.schemaVersion, .command'
# 1
# "status"
```

JSON Schema files for every command are published in [`docs/contracts/cli/`](docs/contracts/CLI_CONTRACTS.md). See the [CLI Contracts guide](docs/contracts/CLI_CONTRACTS.md) for validation examples and migration notes.

### Nearest-epoch fallback

If a ref doesn't have an exact epoch marker, the diff engine walks up the commit ancestry to find the nearest one. When this happens, the TTY output shows a warning icon next to the endpoint.
Expand Down
86 changes: 86 additions & 0 deletions docs/contracts/CLI_CONTRACTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# CLI JSON Contracts

Every `--json` output from the git-mind CLI includes a standard envelope:

```json
{
"schemaVersion": 1,
"command": "status",
...
}
```

- **`schemaVersion`** — Increments only on breaking changes to the JSON structure. Non-breaking additions (new optional fields) do not increment it.
- **`command`** — The command that produced this output. Useful for routing in pipelines that handle multiple git-mind outputs.

## Version Policy

| Scenario | schemaVersion change |
|----------|---------------------|
| New optional field added | No change |
| Field renamed or removed | Increment |
| Field type changed | Increment |
| Array wrapped in object | Increment |
| New command added | No change (new schema) |

## Command-to-Schema Table

| Command | Schema File |
|---------|-------------|
| `nodes --id <id> --json` | [`node-detail.schema.json`](cli/node-detail.schema.json) |
| `nodes --json` | [`node-list.schema.json`](cli/node-list.schema.json) |
| `status --json` | [`status.schema.json`](cli/status.schema.json) |
| `at <ref> --json` | [`at.schema.json`](cli/at.schema.json) |
| `import --json` | [`import.schema.json`](cli/import.schema.json) |
| `import --from-markdown --json` | [`import.schema.json`](cli/import.schema.json) |
| `export --json` (stdout) | [`export-data.schema.json`](cli/export-data.schema.json) |
| `export --file <path> --json` | [`export-file.schema.json`](cli/export-file.schema.json) |
| `merge --json` | [`merge.schema.json`](cli/merge.schema.json) |
| `doctor --json` | [`doctor.schema.json`](cli/doctor.schema.json) |
| `suggest --json` | [`suggest.schema.json`](cli/suggest.schema.json) |
| `review --json` | [`review-list.schema.json`](cli/review-list.schema.json) |
| `review --batch --json` | [`review-batch.schema.json`](cli/review-batch.schema.json) |
| `diff <ref-a>..<ref-b> --json` | [`diff.schema.json`](cli/diff.schema.json) |

> **Note:** `nodes --id` and `nodes` (list) both emit `"command": "nodes"`. To select the correct schema, check for a top-level `id` field (node-detail) vs. a `nodes` array (node-list).

## Programmatic Validation

```javascript
import Ajv from 'ajv/dist/2020.js';
import schema from './docs/contracts/cli/status.schema.json' with { type: 'json' };

const ajv = new Ajv({ strict: true, allErrors: true });
const validate = ajv.compile(schema);

const output = JSON.parse(execSync('git mind status --json'));
if (!validate(output)) {
console.error(validate.errors);
}
```

## Migration Guide

### `nodes --json` (Breaking)

The output changed from a bare JSON array to a wrapped object.

```bash
# Before (prior to v3.0.0):
git mind nodes --json | jq '.[]'

# After:
git mind nodes --json | jq '.nodes[]'
```
Comment on lines +68 to +74
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Verify the "before" version reference.

Line 66 and 78 reference "v2.0.0-alpha.5 and earlier", but the previous package.json version was 2.0.0-alpha.3. If alpha.5 was never published, this may confuse users trying to identify which versions are affected.

🤖 Prompt for AI Agents
In `@docs/contracts/CLI_CONTRACTS.md` around lines 65 - 71, The doc references
"v2.0.0-alpha.5 and earlier" which appears incorrect; update the version string
in CLI_CONTRACTS.md (both occurrences currently at the two places you noted) to
the correct published prior version (e.g., "v2.0.0-alpha.3 and earlier") or to
the accurate range determined from package.json/tags, ensuring both references
match and adding a brief parenthetical if alpha.5 was never published to avoid
confusion; update the two matching occurrences so the before/after example and
surrounding text are consistent.


### `review --json` (Breaking)

The output changed from a bare JSON array to a wrapped object.

```bash
# Before (prior to v3.0.0):
git mind review --json | jq '.[].source'

# After:
git mind review --json | jq '.pending[].source'
```
60 changes: 60 additions & 0 deletions docs/contracts/cli/at.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/neuroglyph/git-mind/docs/contracts/cli/at.schema.json",
"title": "git-mind at --json",
"description": "Time-travel output from `git mind at <ref> --json`",
"type": "object",
"required": ["schemaVersion", "command", "ref", "sha", "fullSha", "tick", "nearest", "status"],
"additionalProperties": false,
"properties": {
"schemaVersion": { "type": "integer", "const": 1 },
"command": { "type": "string", "const": "at" },
"ref": { "type": "string" },
"sha": { "type": "string", "minLength": 8, "maxLength": 8 },
"fullSha": { "type": "string", "pattern": "^[0-9a-f]+$" },
"tick": { "type": "integer", "minimum": 0 },
"nearest": { "type": "boolean" },
"recordedAt": { "type": ["string", "null"] },
"status": {
"type": "object",
"required": ["nodes", "edges", "health"],
"additionalProperties": false,
"properties": {
"nodes": {
"type": "object",
"required": ["total", "byPrefix"],
"additionalProperties": false,
"properties": {
"total": { "type": "integer", "minimum": 0 },
"byPrefix": {
"type": "object",
"additionalProperties": { "type": "integer", "minimum": 0 }
}
}
},
"edges": {
"type": "object",
"required": ["total", "byType"],
"additionalProperties": false,
"properties": {
"total": { "type": "integer", "minimum": 0 },
"byType": {
"type": "object",
"additionalProperties": { "type": "integer", "minimum": 0 }
}
}
},
"health": {
"type": "object",
"required": ["blockedItems", "lowConfidence", "orphanNodes"],
"additionalProperties": false,
"properties": {
"blockedItems": { "type": "integer", "minimum": 0 },
"lowConfidence": { "type": "integer", "minimum": 0 },
"orphanNodes": { "type": "integer", "minimum": 0 }
}
}
}
}
}
}
147 changes: 147 additions & 0 deletions docs/contracts/cli/diff.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/neuroglyph/git-mind/docs/contracts/cli/diff.schema.json",
"title": "git-mind diff --json",
"description": "Graph diff result from `git mind diff <ref-a>..<ref-b> --json`",
"type": "object",
"required": ["schemaVersion", "command", "from", "to", "nodes", "edges", "summary", "stats"],
"additionalProperties": false,
"properties": {
"schemaVersion": { "type": "integer", "const": 1 },
"command": { "type": "string", "const": "diff" },
"from": { "$ref": "#/$defs/diffEndpoint" },
"to": { "$ref": "#/$defs/diffEndpoint" },
"nodes": {
"type": "object",
"required": ["added", "removed", "total"],
"additionalProperties": false,
"properties": {
"added": { "type": "array", "items": { "type": "string" } },
"removed": { "type": "array", "items": { "type": "string" } },
"total": {
"type": "object",
"required": ["before", "after"],
"additionalProperties": false,
"properties": {
"before": { "type": "integer", "minimum": 0 },
"after": { "type": "integer", "minimum": 0 }
}
}
}
},
"edges": {
"type": "object",
"required": ["added", "removed", "total"],
"additionalProperties": false,
"properties": {
"added": {
"type": "array",
"items": { "$ref": "#/$defs/edgeDiffEntry" }
},
"removed": {
"type": "array",
"items": { "$ref": "#/$defs/edgeDiffEntry" }
},
"total": {
"type": "object",
"required": ["before", "after"],
"additionalProperties": false,
"properties": {
"before": { "type": "integer", "minimum": 0 },
"after": { "type": "integer", "minimum": 0 }
}
}
}
},
"summary": {
"type": "object",
"required": ["nodesByPrefix", "edgesByType"],
"additionalProperties": false,
"properties": {
"nodesByPrefix": {
"type": "object",
"additionalProperties": {
"type": "object",
"required": ["before", "after"],
"additionalProperties": false,
"properties": {
"before": { "type": "integer", "minimum": 0 },
"after": { "type": "integer", "minimum": 0 }
}
}
},
"edgesByType": {
"type": "object",
"additionalProperties": {
"type": "object",
"required": ["before", "after"],
"additionalProperties": false,
"properties": {
"before": { "type": "integer", "minimum": 0 },
"after": { "type": "integer", "minimum": 0 }
}
}
}
}
},
"stats": {
"type": "object",
"required": ["materializeMs", "diffMs", "nodeCount", "edgeCount"],
"additionalProperties": false,
"properties": {
"sameTick": { "type": "boolean" },
"materializeMs": {
"type": "object",
"required": ["a", "b"],
"additionalProperties": false,
"properties": {
"a": { "type": "integer", "minimum": 0 },
"b": { "type": "integer", "minimum": 0 }
}
},
"diffMs": { "type": "integer", "minimum": 0 },
"nodeCount": {
"type": "object",
"required": ["a", "b"],
"additionalProperties": false,
"properties": {
"a": { "type": "integer", "minimum": 0 },
"b": { "type": "integer", "minimum": 0 }
}
},
"edgeCount": {
"type": "object",
"required": ["a", "b"],
"additionalProperties": false,
"properties": {
"a": { "type": "integer", "minimum": 0 },
"b": { "type": "integer", "minimum": 0 }
}
}
}
}
},
"$defs": {
"diffEndpoint": {
"type": "object",
"required": ["ref", "sha", "tick", "nearest"],
"additionalProperties": false,
"properties": {
"ref": { "type": "string" },
"sha": { "type": "string", "minLength": 8, "maxLength": 8 },
"tick": { "type": "integer", "minimum": 0 },
"nearest": { "type": "boolean" }
}
},
"edgeDiffEntry": {
"type": "object",
"required": ["source", "target", "type"],
"additionalProperties": false,
"properties": {
"source": { "type": "string", "minLength": 1 },
"target": { "type": "string", "minLength": 1 },
"type": { "type": "string" }
}
}
}
}
Loading