From fdea2843959197f9a25275fd208d6d0975ef1858 Mon Sep 17 00:00:00 2001 From: James Ross Date: Mon, 1 Jun 2026 04:28:15 -0700 Subject: [PATCH] feat: add bootstrap command contract (#305) Refs #310. --- README.md | 2 + TECHNICAL-TEARDOWN.md | 12 +- bin/git-mind.js | 14 +- docs/contracts/CLI_CONTRACTS.md | 1 + docs/contracts/cli/bootstrap.schema.json | 151 ++++++++++++++++++ .../feature-profiles/bootstrap-command.md | 6 +- docs/design/h1-semantic-bootstrap.md | 3 +- src/bootstrap.js | 68 ++++++++ src/cli/commands.js | 49 +++++- src/cli/format.js | 40 +++++ test/contracts.integration.test.js | 36 +++++ test/contracts.test.js | 12 ++ 12 files changed, 383 insertions(+), 11 deletions(-) create mode 100644 docs/contracts/cli/bootstrap.schema.json create mode 100644 src/bootstrap.js diff --git a/README.md b/README.md index 342f7202..2bc0266b 100644 --- a/README.md +++ b/README.md @@ -161,6 +161,8 @@ The repository is in a stabilize-and-clarify phase: - the inward-facing cognition thesis has moved to `think` - `git-mind` is being narrowed around semantic repository intelligence - existing graph capabilities remain real and useful +- the first `git mind bootstrap` command contract exists, with scanner and + inference behavior planned in follow-up slices - future work should be judged against the low-input semantic bootstrap hill ## Documentation diff --git a/TECHNICAL-TEARDOWN.md b/TECHNICAL-TEARDOWN.md index 6fbc93c4..6adb21c8 100644 --- a/TECHNICAL-TEARDOWN.md +++ b/TECHNICAL-TEARDOWN.md @@ -121,8 +121,8 @@ The distinction matters. The code can already store, inspect, filter, and diff semantic knowledge. The planned product move is to make the first useful graph appear from repository artifacts themselves: code files, docs, ADRs, commit history, issue references, PR references, and eventually review artifacts. The -new feature profile documents and graph data model describe that future contract, -but the CLI does not yet expose a `bootstrap` command. +CLI now exposes the first `git mind bootstrap` command contract, but the +repository scanner and inference pipeline are still planned follow-up slices. ## The Exact Entry Point @@ -201,10 +201,10 @@ Program bootstrapping is what `bin/git-mind.js` does every time it starts. It imports modules, parses arguments, constructs context objects when needed, and dispatches one command. -Product bootstrapping is the planned Hill 1 feature: `git mind bootstrap`. That -future flow should scan an unfamiliar repository and infer a first semantic map. -The design documents specify this direction, but the current executable command -set does not include that command yet. +Product bootstrapping is the Hill 1 feature surfaced as `git mind bootstrap`. +The current executable command establishes the JSON and dry-run contract. Later +slices will fill that contract by scanning an unfamiliar repository and inferring +a first semantic map. Runtime begins after command dispatch. At runtime, each command decides whether it needs a graph, opens that graph with `initGraph(cwd)`, performs reads or diff --git a/bin/git-mind.js b/bin/git-mind.js index 35f2de7e..aec3233b 100755 --- a/bin/git-mind.js +++ b/bin/git-mind.js @@ -5,7 +5,7 @@ * Usage: git mind [options] */ -import { init, link, view, list, remove, nodes, status, at, importCmd, importMarkdownCmd, exportCmd, mergeCmd, installHooks, processCommitCmd, doctor, suggest, review, diff, set, unsetCmd, contentSet, contentShow, contentMeta, contentDelete, extensionList, extensionValidate, extensionAdd, extensionRemove } from '../src/cli/commands.js'; +import { init, bootstrap, link, view, list, remove, nodes, status, at, importCmd, importMarkdownCmd, exportCmd, mergeCmd, installHooks, processCommitCmd, doctor, suggest, review, diff, set, unsetCmd, contentSet, contentShow, contentMeta, contentDelete, extensionList, extensionValidate, extensionAdd, extensionRemove } from '../src/cli/commands.js'; import { parseDiffRefs, collectDiffPositionals } from '../src/diff.js'; import { createContext } from '../src/context-envelope.js'; import { registerBuiltinExtensions } from '../src/extension.js'; @@ -24,6 +24,9 @@ Context flags (read commands: view, nodes, status, export, doctor): Commands: init Initialize git-mind in this repo + bootstrap Build the first semantic map skeleton + --dry-run Preview without writing graph state + --json Output as JSON link Create a semantic edge --type Edge type (default: relates-to) --confidence Confidence 0.0-1.0 (default: 1.0) @@ -175,6 +178,15 @@ switch (command) { await init(cwd); break; + case 'bootstrap': { + const bootstrapFlags = parseFlags(args.slice(1)); + await bootstrap(cwd, { + dryRun: bootstrapFlags['dry-run'] === true, + json: bootstrapFlags.json === true, + }); + break; + } + case 'link': { const source = args[1]; const target = args[2]; diff --git a/docs/contracts/CLI_CONTRACTS.md b/docs/contracts/CLI_CONTRACTS.md index 78b6f737..78f3bebf 100644 --- a/docs/contracts/CLI_CONTRACTS.md +++ b/docs/contracts/CLI_CONTRACTS.md @@ -27,6 +27,7 @@ Every `--json` output from the git-mind CLI includes a standard envelope: | Command | Schema File | |---------|-------------| +| `bootstrap --json` | [`bootstrap.schema.json`](cli/bootstrap.schema.json) | | `nodes --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) | diff --git a/docs/contracts/cli/bootstrap.schema.json b/docs/contracts/cli/bootstrap.schema.json new file mode 100644 index 00000000..6196245c --- /dev/null +++ b/docs/contracts/cli/bootstrap.schema.json @@ -0,0 +1,151 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/neuroglyph/git-mind/docs/contracts/cli/bootstrap.schema.json", + "title": "git-mind bootstrap --json", + "description": "Bootstrap summary output from `git mind bootstrap --json`", + "type": "object", + "required": [ + "schemaVersion", + "command", + "dryRun", + "artifacts", + "entities", + "relationships", + "confidence", + "provenance", + "warnings", + "next" + ], + "additionalProperties": false, + "properties": { + "schemaVersion": { + "type": "integer", + "const": 1 + }, + "command": { + "type": "string", + "const": "bootstrap" + }, + "dryRun": { + "type": "boolean" + }, + "artifacts": { + "type": "object", + "required": ["scanned", "byKind", "skipped", "warnings"], + "additionalProperties": false, + "properties": { + "scanned": { + "type": "integer", + "minimum": 0 + }, + "byKind": { + "type": "object", + "additionalProperties": { + "type": "integer", + "minimum": 0 + } + }, + "skipped": { + "type": "integer", + "minimum": 0 + }, + "warnings": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "entities": { + "type": "object", + "required": ["created", "unchanged", "byPrefix"], + "additionalProperties": false, + "properties": { + "created": { + "type": "integer", + "minimum": 0 + }, + "unchanged": { + "type": "integer", + "minimum": 0 + }, + "byPrefix": { + "type": "object", + "additionalProperties": { + "type": "integer", + "minimum": 0 + } + } + } + }, + "relationships": { + "type": "object", + "required": ["created", "unchanged", "byType"], + "additionalProperties": false, + "properties": { + "created": { + "type": "integer", + "minimum": 0 + }, + "unchanged": { + "type": "integer", + "minimum": 0 + }, + "byType": { + "type": "object", + "additionalProperties": { + "type": "integer", + "minimum": 0 + } + } + } + }, + "confidence": { + "type": "object", + "required": ["high", "medium", "low"], + "additionalProperties": false, + "properties": { + "high": { + "type": "integer", + "minimum": 0 + }, + "medium": { + "type": "integer", + "minimum": 0 + }, + "low": { + "type": "integer", + "minimum": 0 + } + } + }, + "provenance": { + "type": "object", + "required": ["inferred", "missing"], + "additionalProperties": false, + "properties": { + "inferred": { + "type": "integer", + "minimum": 0 + }, + "missing": { + "type": "integer", + "minimum": 0 + } + } + }, + "warnings": { + "type": "array", + "items": { + "type": "string" + } + }, + "next": { + "type": "array", + "items": { + "type": "string" + } + } + } +} diff --git a/docs/design/feature-profiles/bootstrap-command.md b/docs/design/feature-profiles/bootstrap-command.md index bac0ea53..dc65dae6 100644 --- a/docs/design/feature-profiles/bootstrap-command.md +++ b/docs/design/feature-profiles/bootstrap-command.md @@ -84,13 +84,15 @@ Initial JSON shape: ```json { "schemaVersion": 1, + "command": "bootstrap", "dryRun": true, - "artifacts": { "scanned": 0, "byKind": {} }, + "artifacts": { "scanned": 0, "byKind": {}, "skipped": 0, "warnings": [] }, "entities": { "created": 0, "unchanged": 0, "byPrefix": {} }, "relationships": { "created": 0, "unchanged": 0, "byType": {} }, "confidence": { "high": 0, "medium": 0, "low": 0 }, + "provenance": { "inferred": 0, "missing": 0 }, "warnings": [], - "followUp": [] + "next": ["git mind status", "git mind nodes", "git mind review"] } ``` diff --git a/docs/design/h1-semantic-bootstrap.md b/docs/design/h1-semantic-bootstrap.md index 2d12e79c..85cc443c 100644 --- a/docs/design/h1-semantic-bootstrap.md +++ b/docs/design/h1-semantic-bootstrap.md @@ -188,7 +188,8 @@ Suggested command shape: git mind bootstrap ``` -> Status: planned contract for Hill 1. This command is not implemented in the current CLI yet. +> Status: initial executable contract. The current CLI exposes the command and +> dry-run JSON shape; repository scanning and inference land in later slices. Possible compatible aliases later: diff --git a/src/bootstrap.js b/src/bootstrap.js new file mode 100644 index 00000000..3c1d18f4 --- /dev/null +++ b/src/bootstrap.js @@ -0,0 +1,68 @@ +/** + * @module bootstrap + * Hill 1 semantic bootstrap contract helpers. + */ + +export const BOOTSTRAP_NEXT_COMMANDS = [ + 'git mind status', + 'git mind nodes', + 'git mind review', +]; + +/** + * @typedef {object} BootstrapSummary + * @property {boolean} dryRun + * @property {{ scanned: number, byKind: Record, skipped: number, warnings: string[] }} artifacts + * @property {{ created: number, unchanged: number, byPrefix: Record }} entities + * @property {{ created: number, unchanged: number, byType: Record }} relationships + * @property {{ high: number, medium: number, low: number }} confidence + * @property {{ inferred: number, missing: number }} provenance + * @property {string[]} warnings + * @property {string[]} next + */ + +/** + * Create the current bootstrap summary payload. + * + * This first slice intentionally exposes the command contract before the + * scanner/inference pipeline exists. Keeping the payload construction pure + * makes the dry-run no-write guarantee easy to test and preserves a stable + * shape for later slices to fill with real counts. + * + * @param {{ dryRun?: boolean }} [opts] + * @returns {BootstrapSummary} + */ +export function createBootstrapSummary(opts = {}) { + const dryRun = opts.dryRun === true; + + return { + dryRun, + artifacts: { + scanned: 0, + byKind: {}, + skipped: 0, + warnings: [], + }, + entities: { + created: 0, + unchanged: 0, + byPrefix: {}, + }, + relationships: { + created: 0, + unchanged: 0, + byType: {}, + }, + confidence: { + high: 0, + medium: 0, + low: 0, + }, + provenance: { + inferred: 0, + missing: 0, + }, + warnings: [], + next: [...BOOTSTRAP_NEXT_COMMANDS], + }; +} diff --git a/src/cli/commands.js b/src/cli/commands.js index 58ab0087..392fbd55 100644 --- a/src/cli/commands.js +++ b/src/cli/commands.js @@ -26,8 +26,9 @@ import { computeDiff } from '../diff.js'; import { DEFAULT_CONTEXT } from '../context-envelope.js'; import { loadExtension, registerExtension, removeExtension, listExtensions, validateExtension } from '../extension.js'; import { writeContent, readContent, getContentMeta, deleteContent } from '../content.js'; +import { createBootstrapSummary } from '../bootstrap.js'; import { getProp } from '../prop-bag.js'; -import { success, error, info, formatEdge, formatView, formatNode, formatNodeList, formatStatus, formatExportResult, formatImportResult, formatDoctorResult, formatSuggestions, formatReviewItem, formatDecisionSummary, formatAtStatus, formatDiff, formatExtensionList, formatContentMeta } from './format.js'; +import { success, error, info, formatEdge, formatView, formatNode, formatNodeList, formatStatus, formatBootstrapResult, formatExportResult, formatImportResult, formatDoctorResult, formatSuggestions, formatReviewItem, formatDecisionSummary, formatAtStatus, formatDiff, formatExtensionList, formatContentMeta } from './format.js'; /** * Write structured JSON to stdout with schemaVersion and command fields. @@ -41,6 +42,26 @@ function outputJson(command, data) { console.log(JSON.stringify(out, null, 2)); } +/** + * Ensure a command is running from inside a Git worktree without mutating repo + * state. + * @param {string} cwd + */ +function assertGitWorktree(cwd) { + try { + const result = execFileSync('git', ['rev-parse', '--is-inside-work-tree'], { + cwd, + encoding: 'utf-8', + stdio: ['ignore', 'pipe', 'ignore'], + }).trim(); + if (result !== 'true') { + throw new Error('not a worktree'); + } + } catch { + throw new Error('Not inside a Git worktree. Run this command from a Git repository.'); + } +} + /** * Resolve a ContextEnvelope to a live graph-like object. * @@ -131,6 +152,32 @@ export async function init(cwd) { } } +/** + * Run the Hill 1 bootstrap command contract. + * @param {string} cwd + * @param {{ dryRun?: boolean, json?: boolean }} opts + */ +export async function bootstrap(cwd, opts = {}) { + try { + assertGitWorktree(cwd); + + if (!opts.dryRun) { + await initGraph(cwd); + } + + const result = createBootstrapSummary({ dryRun: opts.dryRun }); + + if (opts.json) { + outputJson('bootstrap', result); + } else { + console.log(formatBootstrapResult(result)); + } + } catch (err) { + console.error(error(err.message)); + process.exitCode = 1; + } +} + /** * Create a link (edge) between two nodes. * @param {string} cwd diff --git a/src/cli/format.js b/src/cli/format.js index bcfba9c0..a08f191c 100644 --- a/src/cli/format.js +++ b/src/cli/format.js @@ -191,6 +191,46 @@ export function formatStatus(status) { return lines.join('\n'); } +/** + * Format a bootstrap summary for terminal display. + * @param {import('../bootstrap.js').BootstrapSummary} result + * @returns {string} + */ +export function formatBootstrapResult(result) { + const lines = []; + + lines.push(chalk.bold(result.dryRun ? 'Bootstrap dry run' : 'Bootstrap')); + lines.push(chalk.dim('═'.repeat(32))); + lines.push(''); + lines.push(`${chalk.bold('Artifacts:')} ${result.artifacts.scanned} scanned, ${result.artifacts.skipped} skipped`); + renderCountTable(result.artifacts.byKind, lines); + lines.push(`${chalk.bold('Entities:')} ${result.entities.created} created, ${result.entities.unchanged} unchanged`); + renderCountTable(result.entities.byPrefix, lines); + lines.push(`${chalk.bold('Relationships:')} ${result.relationships.created} created, ${result.relationships.unchanged} unchanged`); + renderCountTable(result.relationships.byType, lines); + lines.push(`${chalk.bold('Confidence:')} high ${result.confidence.high}, medium ${result.confidence.medium}, low ${result.confidence.low}`); + lines.push(`${chalk.bold('Provenance:')} inferred ${result.provenance.inferred}, missing ${result.provenance.missing}`); + + const warnings = [...result.artifacts.warnings, ...result.warnings]; + if (warnings.length > 0) { + lines.push(''); + lines.push(chalk.bold('Warnings')); + for (const item of warnings) { + lines.push(` ${chalk.yellow(figures.warning)} ${item}`); + } + } + + if (result.next.length > 0) { + lines.push(''); + lines.push(chalk.bold('Next')); + for (const command of result.next) { + lines.push(` ${chalk.cyan(command)}`); + } + } + + return lines.join('\n'); +} + /** * Format a doctor result for terminal display. * @param {import('../doctor.js').DoctorResult} result diff --git a/test/contracts.integration.test.js b/test/contracts.integration.test.js index 1fd87931..20935460 100644 --- a/test/contracts.integration.test.js +++ b/test/contracts.integration.test.js @@ -185,6 +185,42 @@ describe('CLI schema contract canaries', () => { expect(validate(output), JSON.stringify(validate.errors)).toBe(true); }); + cliIt('bootstrap --dry-run --json validates against bootstrap.schema.json without writes', async () => { + const schema = await loadSchema('bootstrap.schema.json'); + const before = runCli(['status', '--json'], tempDir); + const output = runCli(['bootstrap', '--dry-run', '--json'], tempDir); + const after = runCli(['status', '--json'], tempDir); + + expect(output.schemaVersion).toBe(1); + expect(output.command).toBe('bootstrap'); + expect(output.dryRun).toBe(true); + expect(output.artifacts.scanned).toBe(0); + expect(output.entities.created).toBe(0); + expect(output.relationships.created).toBe(0); + expect(output.next).toContain('git mind status'); + expect(after).toEqual(before); + + const validate = ajv.compile(schema); + expect(validate(output), JSON.stringify(validate.errors)).toBe(true); + }); + + cliIt('bootstrap --dry-run --json fails outside a Git worktree without writes', async () => { + const outsideGitDir = await mkdtemp(join(tmpdir(), 'gitmind-bootstrap-outside-')); + + try { + expect(() => execFileSync(process.execPath, [BIN, 'bootstrap', '--dry-run', '--json'], { + cwd: outsideGitDir, + encoding: 'utf-8', + stdio: ['ignore', 'pipe', 'pipe'], + timeout: CLI_EXEC_TIMEOUT, + env: { ...process.env, NO_COLOR: '1' }, + })).toThrow(/Not inside a Git worktree/); + expect(existsSync(join(outsideGitDir, '.git'))).toBe(false); + } finally { + await rm(outsideGitDir, { recursive: true, force: true }); + } + }); + cliIt('review --json validates against review-list.schema.json', async () => { const schema = await loadSchema('review-list.schema.json'); const output = runCli(['review', '--json'], tempDir); diff --git a/test/contracts.test.js b/test/contracts.test.js index 2a89dbb1..3fe3b328 100644 --- a/test/contracts.test.js +++ b/test/contracts.test.js @@ -29,6 +29,18 @@ async function loadSchemas(dir = SCHEMA_DIR) { /** Sample valid payloads for each schema, keyed by filename. */ const VALID_SAMPLES = { + 'bootstrap.schema.json': { + schemaVersion: 1, + command: 'bootstrap', + dryRun: true, + artifacts: { scanned: 0, byKind: {}, skipped: 0, warnings: [] }, + entities: { created: 0, unchanged: 0, byPrefix: {} }, + relationships: { created: 0, unchanged: 0, byType: {} }, + confidence: { high: 0, medium: 0, low: 0 }, + provenance: { inferred: 0, missing: 0 }, + warnings: [], + next: ['git mind status', 'git mind nodes', 'git mind review'], + }, 'node-detail.schema.json': { schemaVersion: 1, command: 'nodes',