-
Notifications
You must be signed in to change notification settings - Fork 0
feat: M9 Phase A — API stability surface + error taxonomy #297
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
5486152
7b9888c
c3add2f
a9322e8
8257424
7ff89fc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -83,6 +83,39 @@ test/ | |||||||||||||||||||||||||
| hooks.test.js — Directive parsing tests | ||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| ## Public API & Deprecation Protocol | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| git-mind's public API is everything exported from `src/index.js`. A stability | ||||||||||||||||||||||||||
| test (`test/api-surface.test.js`) snapshots every export name and type — CI | ||||||||||||||||||||||||||
| will fail if the surface changes without an intentional update. | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| ### Making changes to the public API | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| | Change | Semver | Process | | ||||||||||||||||||||||||||
| |--------|--------|---------| | ||||||||||||||||||||||||||
| | **Add** an export | minor | Add to `API_SNAPSHOT` in `test/api-surface.test.js` | | ||||||||||||||||||||||||||
| | **Remove** an export | **major** | Follow the deprecation protocol below | | ||||||||||||||||||||||||||
| | **Change** an export's type/signature | **major** | Follow the deprecation protocol below | | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| ### Deprecation protocol | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| 1. **Mark deprecated** — Add `@deprecated` JSDoc tag with a migration note and | ||||||||||||||||||||||||||
| the target removal version (at least one minor release away): | ||||||||||||||||||||||||||
| ```js | ||||||||||||||||||||||||||
| /** @deprecated Use newFunction() instead. Removal: v6.0.0 */ | ||||||||||||||||||||||||||
| export function oldFunction() { ... } | ||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||
|
Comment on lines
+100
to
+107
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Markdown lint warning: fenced code block at line 104 needs a blank line before it. Static analysis flags MD031 at lines 104 and 107. The deprecation example code block should be surrounded by blank lines: 📝 Proposed fix 1. **Mark deprecated** — Add `@deprecated` JSDoc tag with a migration note and
the target removal version (at least one minor release away):
+
```js
/** `@deprecated` Use newFunction() instead. Removal: v6.0.0 */
export function oldFunction() { ... }
```
+
2. **Runtime warning** — Emit a one-time `console.warn` on first call:📝 Committable suggestion
Suggested change
🧰 Tools🪛 markdownlint-cli2 (0.21.0)[warning] 104-104: Fenced code blocks should be surrounded by blank lines (MD031, blanks-around-fences) [warning] 107-107: Fenced code blocks should be surrounded by blank lines (MD031, blanks-around-fences) 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
| 2. **Runtime warning** — Emit a one-time `console.warn` on first call: | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| ```text | ||||||||||||||||||||||||||
| [git-mind] oldFunction() is deprecated — use newFunction(). Removal: v6.0.0 | ||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| 3. **Keep in snapshot** — The export stays in `test/api-surface.test.js` until | ||||||||||||||||||||||||||
| the major version that removes it. | ||||||||||||||||||||||||||
| 4. **Remove** — In the next major version, delete the export, remove it from | ||||||||||||||||||||||||||
| the snapshot, and document the removal in CHANGELOG.md. | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| ## License | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| By contributing, you agree that your contributions will be licensed under [Apache-2.0](LICENSE). | ||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,6 +9,7 @@ import { init, link, view, list, remove, nodes, status, at, importCmd, importMar | |
| import { parseDiffRefs, collectDiffPositionals } from '../src/diff.js'; | ||
| import { createContext } from '../src/context-envelope.js'; | ||
| import { registerBuiltinExtensions } from '../src/extension.js'; | ||
| import { GmindError } from '../src/errors.js'; | ||
|
|
||
| const args = process.argv.slice(2); | ||
| const command = args[0]; | ||
|
|
@@ -114,6 +115,28 @@ Edge types: implements, augments, relates-to, blocks, belongs-to, | |
|
|
||
| const BOOLEAN_FLAGS = new Set(['json', 'fix', 'dry-run', 'validate', 'raw']); | ||
|
|
||
| /** | ||
| * Handle a GmindError at the CLI boundary. | ||
| * Sets exit code and outputs structured JSON when --json is active. | ||
| * Falls back to plain stderr for non-GmindError exceptions. | ||
| * | ||
| * @param {Error} err | ||
| * @param {{ json?: boolean }} [opts] | ||
| */ | ||
| function handleError(err, opts = {}) { | ||
| if (err instanceof GmindError) { | ||
| if (opts.json) { | ||
| console.log(JSON.stringify({ ...err.toJSON(), schemaVersion: 1, command: 'error' }, null, 2)); | ||
| } else { | ||
| console.error(err.message); | ||
| } | ||
| process.exitCode = err.exitCode; | ||
| } else { | ||
| console.error(err.message ?? String(err)); | ||
| process.exitCode = 1; | ||
| } | ||
| } | ||
|
Comment on lines
+118
to
+138
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Hardcoded The If this is a deliberate schema decision (always emit Additionally, for non-GmindError exceptions (line 135), you're accessing 🤖 Prompt for AI Agents
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is intentional — error envelopes always use
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
✏️ Learnings added
Comment on lines
+126
to
+138
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial
When Consider logging the full stack when a debug flag is present: 🔧 Suggested enhancement } else {
- console.error(err.message ?? String(err));
+ console.error(err.message ?? String(err));
+ if (process.env.GITMIND_DEBUG) console.error(err.stack);
process.exitCode = 1;
}🤖 Prompt for AI Agents |
||
|
|
||
| /** | ||
| * Extract a ContextEnvelope from parsed flags. | ||
| * Builds one only when context flags are present; otherwise returns null. | ||
|
|
@@ -179,8 +202,7 @@ switch (command) { | |
| const source = args[1]; | ||
| const target = args[2]; | ||
| if (!source || !target) { | ||
| console.error('Usage: git mind link <source> <target> [--type <type>]'); | ||
| process.exitCode = 1; | ||
| handleError(new GmindError('GMIND_E_USAGE', 'Usage: git mind link <source> <target> [--type <type>]')); | ||
| break; | ||
| } | ||
| const flags = parseFlags(args.slice(3)); | ||
|
|
@@ -209,8 +231,7 @@ switch (command) { | |
| const rmSource = args[1]; | ||
| const rmTarget = args[2]; | ||
| if (!rmSource || !rmTarget) { | ||
| console.error('Usage: git mind remove <source> <target> [--type <type>]'); | ||
| process.exitCode = 1; | ||
| handleError(new GmindError('GMIND_E_USAGE', 'Usage: git mind remove <source> <target> [--type <type>]')); | ||
| break; | ||
| } | ||
| const rmFlags = parseFlags(args.slice(3)); | ||
|
|
@@ -246,9 +267,9 @@ switch (command) { | |
| const setKey = args[2]; | ||
| const setValue = args[3]; | ||
| if (!setNodeId || !setKey || setValue === undefined || setValue.startsWith('--')) { | ||
| console.error('Usage: git mind set <nodeId> <key> <value> [--json]'); | ||
| console.error(' <value> is positional and required (flags are not valid values)'); | ||
| process.exitCode = 1; | ||
| handleError(new GmindError('GMIND_E_USAGE', 'Usage: git mind set <nodeId> <key> <value> [--json]', { | ||
| hint: '<value> is positional and required (flags are not valid values)', | ||
| }), { json: args.includes('--json') }); | ||
| break; | ||
|
Comment on lines
+270
to
273
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Passing You parse flags with For consistency, consider using ♻️ Proposed refactor case 'set': {
const setNodeId = args[1];
const setKey = args[2];
const setValue = args[3];
+ const setFlags = parseFlags(args.slice(4));
+ const jsonMode = setFlags.json === true;
if (!setNodeId || !setKey || setValue === undefined || setValue.startsWith('--')) {
handleError(new GmindError('GMIND_E_USAGE', 'Usage: git mind set <nodeId> <key> <value> [--json]', {
hint: '<value> is positional and required (flags are not valid values)',
- }), { json: args.includes('--json') });
+ }), { json: jsonMode });
break;
}
- await set(cwd, setNodeId, setKey, setValue, { json: args.includes('--json') });
+ await set(cwd, setNodeId, setKey, setValue, { json: jsonMode });
break;
}🤖 Prompt for AI Agents |
||
| } | ||
| await set(cwd, setNodeId, setKey, setValue, { json: args.includes('--json') }); | ||
|
|
@@ -259,8 +280,7 @@ switch (command) { | |
| const unsetNodeId = args[1]; | ||
| const unsetKey = args[2]; | ||
| if (!unsetNodeId || !unsetKey) { | ||
| console.error('Usage: git mind unset <nodeId> <key>'); | ||
| process.exitCode = 1; | ||
| handleError(new GmindError('GMIND_E_USAGE', 'Usage: git mind unset <nodeId> <key>'), { json: args.includes('--json') }); | ||
| break; | ||
| } | ||
| await unsetCmd(cwd, unsetNodeId, unsetKey, { json: args.includes('--json') }); | ||
|
|
@@ -280,8 +300,7 @@ switch (command) { | |
| case 'at': { | ||
| const atRef = args[1]; | ||
| if (!atRef || atRef.startsWith('--')) { | ||
| console.error('Usage: git mind at <ref>'); | ||
| process.exitCode = 1; | ||
| handleError(new GmindError('GMIND_E_USAGE', 'Usage: git mind at <ref>'), { json: args.includes('--json') }); | ||
| break; | ||
| } | ||
| await at(cwd, atRef, { json: args.includes('--json') }); | ||
|
|
@@ -298,8 +317,10 @@ switch (command) { | |
| prefix: diffFlags.prefix, | ||
| }); | ||
| } catch (err) { | ||
| console.error(err.message); | ||
| process.exitCode = 1; | ||
| handleError( | ||
| err instanceof GmindError ? err : new GmindError('GMIND_E_USAGE', err.message, { cause: err }), | ||
| { json: diffFlags.json }, | ||
| ); | ||
| } | ||
| break; | ||
| } | ||
|
|
@@ -316,8 +337,7 @@ switch (command) { | |
|
|
||
| const importPath = args.slice(1).find(a => !a.startsWith('--')); | ||
| if (!importPath) { | ||
| console.error('Usage: git mind import <file> [--dry-run] [--json] [--from-markdown <glob>]'); | ||
| process.exitCode = 1; | ||
| handleError(new GmindError('GMIND_E_USAGE', 'Usage: git mind import <file> [--dry-run] [--json] [--from-markdown <glob>]'), { json: jsonMode }); | ||
| break; | ||
| } | ||
| await importCmd(cwd, importPath, { dryRun, json: jsonMode }); | ||
|
|
@@ -355,8 +375,7 @@ switch (command) { | |
|
|
||
| case 'process-commit': | ||
| if (!args[1]) { | ||
| console.error('Usage: git mind process-commit <sha>'); | ||
| process.exitCode = 1; | ||
| handleError(new GmindError('GMIND_E_USAGE', 'Usage: git mind process-commit <sha>')); | ||
| break; | ||
| } | ||
| await processCommitCmd(cwd, args[1]); | ||
|
|
@@ -403,8 +422,7 @@ switch (command) { | |
| const setNode = contentPositionals[0]; | ||
| const fromFile = contentFlags.from; | ||
| if (!setNode || !fromFile) { | ||
| console.error('Usage: git mind content set <node> --from <file> [--mime <type>] [--json]'); | ||
| process.exitCode = 1; | ||
| handleError(new GmindError('GMIND_E_USAGE', 'Usage: git mind content set <node> --from <file> [--mime <type>] [--json]'), { json: contentFlags.json }); | ||
| break; | ||
| } | ||
| await contentSet(cwd, setNode, fromFile, { | ||
|
|
@@ -416,8 +434,7 @@ switch (command) { | |
| case 'show': { | ||
| const showNode = contentPositionals[0]; | ||
| if (!showNode) { | ||
| console.error('Usage: git mind content show <node> [--raw] [--json]'); | ||
| process.exitCode = 1; | ||
| handleError(new GmindError('GMIND_E_USAGE', 'Usage: git mind content show <node> [--raw] [--json]'), { json: contentFlags.json }); | ||
| break; | ||
| } | ||
| await contentShow(cwd, showNode, { | ||
|
|
@@ -429,8 +446,7 @@ switch (command) { | |
| case 'meta': { | ||
| const metaNode = contentPositionals[0]; | ||
| if (!metaNode) { | ||
| console.error('Usage: git mind content meta <node> [--json]'); | ||
| process.exitCode = 1; | ||
| handleError(new GmindError('GMIND_E_USAGE', 'Usage: git mind content meta <node> [--json]'), { json: contentFlags.json }); | ||
| break; | ||
| } | ||
| await contentMeta(cwd, metaNode, { json: contentFlags.json ?? false }); | ||
|
|
@@ -439,17 +455,16 @@ switch (command) { | |
| case 'delete': { | ||
| const deleteNode = contentPositionals[0]; | ||
| if (!deleteNode) { | ||
| console.error('Usage: git mind content delete <node> [--json]'); | ||
| process.exitCode = 1; | ||
| handleError(new GmindError('GMIND_E_USAGE', 'Usage: git mind content delete <node> [--json]'), { json: contentFlags.json }); | ||
| break; | ||
| } | ||
| await contentDelete(cwd, deleteNode, { json: contentFlags.json ?? false }); | ||
| break; | ||
| } | ||
| default: | ||
| console.error(`Unknown content subcommand: ${contentSubCmd ?? '(none)'}`); | ||
| console.error('Usage: git mind content <set|show|meta|delete>'); | ||
| process.exitCode = 1; | ||
| handleError(new GmindError('GMIND_E_UNKNOWN_CMD', `Unknown content subcommand: ${contentSubCmd ?? '(none)'}`, { | ||
| hint: 'Usage: git mind content <set|show|meta|delete>', | ||
| }), { json: contentFlags.json ?? false }); | ||
| } | ||
| break; | ||
| } | ||
|
|
@@ -478,9 +493,9 @@ switch (command) { | |
| break; | ||
| } | ||
| default: | ||
| console.error(`Unknown extension subcommand: ${subCmd ?? '(none)'}`); | ||
| console.error('Usage: git mind extension <list|validate|add|remove>'); | ||
| process.exitCode = 1; | ||
| handleError(new GmindError('GMIND_E_UNKNOWN_CMD', `Unknown extension subcommand: ${subCmd ?? '(none)'}`, { | ||
| hint: 'Usage: git mind extension <list|validate|add|remove>', | ||
| }), { json: extFlags.json ?? false }); | ||
| } | ||
| break; | ||
| } | ||
|
|
@@ -491,11 +506,15 @@ switch (command) { | |
| printUsage(); | ||
| break; | ||
|
|
||
| default: | ||
| default: { | ||
| // No command: show usage (plain text) or exit silently (--json) with code 0. | ||
| const jsonMode = args.includes('--json'); | ||
| if (command) { | ||
| console.error(`Unknown command: ${command}\n`); | ||
| handleError(new GmindError('GMIND_E_UNKNOWN_CMD', `Unknown command: ${command}`), { json: jsonMode }); | ||
| if (!jsonMode) console.error(''); | ||
| } | ||
| printUsage(); | ||
| process.exitCode = command ? 1 : 0; | ||
| if (!jsonMode) printUsage(); | ||
| if (!command) process.exitCode = 0; | ||
| break; | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Uh oh!
There was an error while loading. Please reload this page.