@smartmemory/compose 0.1.5-beta → 0.1.7-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/compose.js +410 -0
- package/bin/git-hooks/post-commit.template +61 -0
- package/bin/git-hooks/pre-push.template +26 -0
- package/contracts/feature-json.schema.json +115 -0
- package/contracts/roadmap-row.schema.json +23 -0
- package/contracts/vision-state.schema.json +64 -0
- package/lib/changelog-writer.js +647 -0
- package/lib/completion-writer.js +464 -0
- package/lib/feature-code.js +29 -0
- package/lib/feature-validator.js +629 -0
- package/lib/feature-writer.js +325 -5
- package/lib/journal-writer.js +928 -0
- package/package.json +5 -1
- package/server/compose-mcp-tools.js +80 -0
- package/server/compose-mcp.js +244 -1
- package/server/schema-validator.js +50 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smartmemory/compose",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7-beta",
|
|
4
4
|
"description": "Structured AI dev pipeline — goal-to-product orchestration with gates, iteration loops, and feature lifecycle management.",
|
|
5
5
|
"author": "SmartMemory",
|
|
6
6
|
"license": "MIT",
|
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
"bin": {
|
|
9
9
|
"compose": "./bin/compose.js"
|
|
10
10
|
},
|
|
11
|
+
"exports": {
|
|
12
|
+
"./mcp": "./server/compose-mcp.js",
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
11
15
|
"scripts": {
|
|
12
16
|
"dev": "node server/supervisor.js 2>&1 | tee /tmp/compose-server.log",
|
|
13
17
|
"dev:server": "node server/supervisor.js",
|
|
@@ -216,6 +216,86 @@ export async function toolRoadmapDiff(args) {
|
|
|
216
216
|
return roadmapDiff(getTargetRoot(), args);
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
+
export async function toolLinkArtifact(args) {
|
|
220
|
+
const { linkArtifact } = await import('../lib/feature-writer.js');
|
|
221
|
+
return linkArtifact(getTargetRoot(), args);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export async function toolLinkFeatures(args) {
|
|
225
|
+
const { linkFeatures } = await import('../lib/feature-writer.js');
|
|
226
|
+
return linkFeatures(getTargetRoot(), args);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export async function toolGetFeatureArtifacts(args) {
|
|
230
|
+
const { getFeatureArtifacts } = await import('../lib/feature-writer.js');
|
|
231
|
+
return getFeatureArtifacts(getTargetRoot(), args);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export async function toolGetFeatureLinks(args) {
|
|
235
|
+
const { getFeatureLinks } = await import('../lib/feature-writer.js');
|
|
236
|
+
return getFeatureLinks(getTargetRoot(), args);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
// Changelog writer — COMP-MCP-CHANGELOG-WRITER
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
|
|
243
|
+
export async function toolAddChangelogEntry(args) {
|
|
244
|
+
const { addChangelogEntry } = await import('../lib/changelog-writer.js');
|
|
245
|
+
return addChangelogEntry(getTargetRoot(), args);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export async function toolGetChangelogEntries(args) {
|
|
249
|
+
const { getChangelogEntries } = await import('../lib/changelog-writer.js');
|
|
250
|
+
return getChangelogEntries(getTargetRoot(), args);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
254
|
+
// Journal writer — COMP-MCP-JOURNAL-WRITER
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
|
|
257
|
+
export async function toolWriteJournalEntry(args) {
|
|
258
|
+
const { writeJournalEntry } = await import('../lib/journal-writer.js');
|
|
259
|
+
return writeJournalEntry(getTargetRoot(), args);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export async function toolGetJournalEntries(args) {
|
|
263
|
+
const { getJournalEntries } = await import('../lib/journal-writer.js');
|
|
264
|
+
return getJournalEntries(getTargetRoot(), args);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ---------------------------------------------------------------------------
|
|
268
|
+
// Completion writer — COMP-MCP-COMPLETION
|
|
269
|
+
// ---------------------------------------------------------------------------
|
|
270
|
+
|
|
271
|
+
export async function toolRecordCompletion(args) {
|
|
272
|
+
const { recordCompletion } = await import('../lib/completion-writer.js');
|
|
273
|
+
return recordCompletion(getTargetRoot(), args);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export async function toolGetCompletions(args) {
|
|
277
|
+
const { getCompletions } = await import('../lib/completion-writer.js');
|
|
278
|
+
return getCompletions(getTargetRoot(), args);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export async function toolValidateFeature(args = {}) {
|
|
282
|
+
const { validateFeature } = await import('../lib/feature-validator.js');
|
|
283
|
+
const { feature_code, external_prefixes, feature_json_mode } = args;
|
|
284
|
+
return validateFeature(getTargetRoot(), feature_code, {
|
|
285
|
+
externalPrefixes: external_prefixes,
|
|
286
|
+
featureJsonMode: feature_json_mode,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export async function toolValidateProject(args = {}) {
|
|
291
|
+
const { validateProject } = await import('../lib/feature-validator.js');
|
|
292
|
+
const { external_prefixes, feature_json_mode } = args;
|
|
293
|
+
return validateProject(getTargetRoot(), {
|
|
294
|
+
externalPrefixes: external_prefixes,
|
|
295
|
+
featureJsonMode: feature_json_mode,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
219
299
|
export async function toolBindSession({ featureCode }) {
|
|
220
300
|
const postData = JSON.stringify({ featureCode });
|
|
221
301
|
return new Promise((resolve, reject) => {
|
package/server/compose-mcp.js
CHANGED
|
@@ -46,6 +46,18 @@ import {
|
|
|
46
46
|
toolAddRoadmapEntry,
|
|
47
47
|
toolSetFeatureStatus,
|
|
48
48
|
toolRoadmapDiff,
|
|
49
|
+
toolLinkArtifact,
|
|
50
|
+
toolLinkFeatures,
|
|
51
|
+
toolGetFeatureArtifacts,
|
|
52
|
+
toolGetFeatureLinks,
|
|
53
|
+
toolAddChangelogEntry,
|
|
54
|
+
toolGetChangelogEntries,
|
|
55
|
+
toolWriteJournalEntry,
|
|
56
|
+
toolGetJournalEntries,
|
|
57
|
+
toolRecordCompletion,
|
|
58
|
+
toolGetCompletions,
|
|
59
|
+
toolValidateFeature,
|
|
60
|
+
toolValidateProject,
|
|
49
61
|
} from './compose-mcp-tools.js';
|
|
50
62
|
|
|
51
63
|
// ---------------------------------------------------------------------------
|
|
@@ -312,6 +324,212 @@ const TOOLS = [
|
|
|
312
324
|
},
|
|
313
325
|
},
|
|
314
326
|
},
|
|
327
|
+
{
|
|
328
|
+
name: 'validate_feature',
|
|
329
|
+
description: 'Cross-check a single feature against ROADMAP, vision-state, feature.json, folder contents, linked artifacts, and cross-references. Returns structured findings with severity (error/warning/info). FEATURE_NOT_FOUND emitted as a finding (not thrown) when the code matches strict regex but exists in no source.',
|
|
330
|
+
inputSchema: {
|
|
331
|
+
type: 'object',
|
|
332
|
+
required: ['feature_code'],
|
|
333
|
+
properties: {
|
|
334
|
+
feature_code: { type: 'string', description: 'Strict feature code, e.g. "COMP-MCP-VALIDATE"' },
|
|
335
|
+
external_prefixes: { type: 'array', items: { type: 'string' }, description: 'Code prefixes (e.g. ["STRAT-"]) treated as external; downgrades ORPHAN_FOLDER to info' },
|
|
336
|
+
feature_json_mode: { type: 'boolean', description: 'Default true. Set false to skip feature.json comparisons in legacy projects.' },
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
name: 'validate_project',
|
|
342
|
+
description: 'Run validate_feature for every code in vision-state, ROADMAP, and folders, plus cross-cutting checks (orphan folders, dangling cross-refs, CHANGELOG references, journal index drift). Returns the union of all findings.',
|
|
343
|
+
inputSchema: {
|
|
344
|
+
type: 'object',
|
|
345
|
+
properties: {
|
|
346
|
+
external_prefixes: { type: 'array', items: { type: 'string' } },
|
|
347
|
+
feature_json_mode: { type: 'boolean' },
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
|
|
352
|
+
// -------------------------------------------------------------------------
|
|
353
|
+
// Linker — COMP-MCP-ARTIFACT-LINKER
|
|
354
|
+
// -------------------------------------------------------------------------
|
|
355
|
+
{
|
|
356
|
+
name: 'link_artifact',
|
|
357
|
+
description: 'Register a non-canonical artifact (snapshot, journal entry, finding, etc.) on a feature. Canonical artifacts (design.md, plan.md, …) inside the feature folder are auto-discovered and rejected here. Stores in feature.json artifacts[]; dedups on (type, path); appends an audit event (best-effort).',
|
|
358
|
+
inputSchema: {
|
|
359
|
+
type: 'object',
|
|
360
|
+
required: ['feature_code', 'artifact_type', 'path'],
|
|
361
|
+
properties: {
|
|
362
|
+
feature_code: { type: 'string' },
|
|
363
|
+
artifact_type: { type: 'string', description: 'e.g. "journal", "snapshot", "finding", "report-supplement", "link", "external"' },
|
|
364
|
+
path: { type: 'string', description: 'Repo-relative path. Must exist; cannot contain ".." after normalization.' },
|
|
365
|
+
status: { type: 'string', enum: ['current', 'superseded', 'historical'] },
|
|
366
|
+
force: { type: 'boolean', description: 'Overwrite an existing entry with the same (type, path)' },
|
|
367
|
+
idempotency_key: { type: 'string' },
|
|
368
|
+
},
|
|
369
|
+
},
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
name: 'link_features',
|
|
373
|
+
description: 'Register a typed cross-feature relationship. Stores on the source feature; query the inverse via get_feature_links(direction:"incoming"). Closed enum on kind; self-links rejected; dedups on (kind, to_code).',
|
|
374
|
+
inputSchema: {
|
|
375
|
+
type: 'object',
|
|
376
|
+
required: ['from_code', 'to_code', 'kind'],
|
|
377
|
+
properties: {
|
|
378
|
+
from_code: { type: 'string' },
|
|
379
|
+
to_code: { type: 'string', description: 'Target feature code. Need not exist yet (you can link to a code you are about to create).' },
|
|
380
|
+
kind: { type: 'string', enum: ['surfaced_by', 'blocks', 'depends_on', 'follow_up', 'supersedes', 'related'] },
|
|
381
|
+
note: { type: 'string' },
|
|
382
|
+
force: { type: 'boolean' },
|
|
383
|
+
idempotency_key: { type: 'string' },
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
name: 'get_feature_artifacts',
|
|
389
|
+
description: 'Read both canonical (auto-discovered: design.md, plan.md, …) and linked (snapshots, journals, findings) artifacts for a feature in one call. Each linked entry includes a current existence check.',
|
|
390
|
+
inputSchema: {
|
|
391
|
+
type: 'object',
|
|
392
|
+
required: ['feature_code'],
|
|
393
|
+
properties: {
|
|
394
|
+
feature_code: { type: 'string' },
|
|
395
|
+
},
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
name: 'get_feature_links',
|
|
400
|
+
description: 'Read outgoing and/or incoming feature links. Default returns both directions; filter by kind if needed.',
|
|
401
|
+
inputSchema: {
|
|
402
|
+
type: 'object',
|
|
403
|
+
required: ['feature_code'],
|
|
404
|
+
properties: {
|
|
405
|
+
feature_code: { type: 'string' },
|
|
406
|
+
direction: { type: 'string', enum: ['outgoing', 'incoming', 'both'] },
|
|
407
|
+
kind: { type: 'string' },
|
|
408
|
+
},
|
|
409
|
+
},
|
|
410
|
+
},
|
|
411
|
+
|
|
412
|
+
// -------------------------------------------------------------------------
|
|
413
|
+
// Changelog writer — COMP-MCP-CHANGELOG-WRITER
|
|
414
|
+
// -------------------------------------------------------------------------
|
|
415
|
+
{
|
|
416
|
+
name: 'add_changelog_entry',
|
|
417
|
+
description: 'Insert (or replace, with force: true) a typed entry in compose/CHANGELOG.md. Idempotent on (date_or_version, code) at storage level; optional caller-supplied idempotency_key for retry safety. Audit-log append is best-effort. Use this instead of editing CHANGELOG.md by hand.',
|
|
418
|
+
inputSchema: {
|
|
419
|
+
type: 'object',
|
|
420
|
+
required: ['date_or_version', 'code', 'summary'],
|
|
421
|
+
properties: {
|
|
422
|
+
date_or_version: { type: 'string', description: 'ISO date "YYYY-MM-DD" or semver "vX.Y.Z"' },
|
|
423
|
+
code: { type: 'string', description: 'Feature code (e.g. "COMP-FOO-1"). Uppercase A-Z, digits, dashes; cannot start or end with a dash.' },
|
|
424
|
+
summary: { type: 'string', description: 'One-line summary; renders as the "— summary" tail of the entry header.' },
|
|
425
|
+
body: { type: 'string', description: 'Free paragraphs between header and labeled subsections.' },
|
|
426
|
+
sections: {
|
|
427
|
+
type: 'object',
|
|
428
|
+
description: 'Optional labeled subsections; emitted in fixed order Added → Changed → Fixed → Snapshot.',
|
|
429
|
+
properties: {
|
|
430
|
+
added: { type: 'array', items: { type: 'string' } },
|
|
431
|
+
changed: { type: 'array', items: { type: 'string' } },
|
|
432
|
+
fixed: { type: 'array', items: { type: 'string' } },
|
|
433
|
+
snapshot: { type: 'array', items: { type: 'string' } },
|
|
434
|
+
},
|
|
435
|
+
additionalProperties: false,
|
|
436
|
+
},
|
|
437
|
+
force: { type: 'boolean', description: 'If true and an entry with the same (date_or_version, code) exists, replace it in place.' },
|
|
438
|
+
idempotency_key: { type: 'string', description: 'Optional caller-supplied key. Same key replays return the cached result without re-mutating.' },
|
|
439
|
+
},
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
name: 'get_changelog_entries',
|
|
444
|
+
description: 'Read parsed entries from compose/CHANGELOG.md. Filter by code (exact) or since (shorthand "24h"/"7d"/"30m" or ISO date — date-only; version surfaces always pass through).',
|
|
445
|
+
inputSchema: {
|
|
446
|
+
type: 'object',
|
|
447
|
+
properties: {
|
|
448
|
+
since: { type: 'string', description: 'Window: shorthand like "24h"/"7d"/"30m" or ISO date. Date-only filter; version surfaces are always returned.' },
|
|
449
|
+
code: { type: 'string' },
|
|
450
|
+
limit: { type: 'number', description: 'Default 50; capped at 500.' },
|
|
451
|
+
},
|
|
452
|
+
},
|
|
453
|
+
},
|
|
454
|
+
|
|
455
|
+
// -------------------------------------------------------------------------
|
|
456
|
+
// Journal writer — COMP-MCP-JOURNAL-WRITER
|
|
457
|
+
// -------------------------------------------------------------------------
|
|
458
|
+
{
|
|
459
|
+
name: 'write_journal_entry',
|
|
460
|
+
description: 'Write a typed entry to compose/docs/journal/ with auto-numbered global session and inserted index row. Idempotent on (date, slug) at storage level; optional caller idempotency_key for retry safety. Audit-log append is best-effort.',
|
|
461
|
+
inputSchema: {
|
|
462
|
+
type: 'object',
|
|
463
|
+
required: ['date', 'slug', 'sections', 'summary_for_index'],
|
|
464
|
+
properties: {
|
|
465
|
+
date: { type: 'string', description: 'ISO date "YYYY-MM-DD".' },
|
|
466
|
+
slug: { type: 'string', description: 'Kebab-case slug for the filename, e.g. "mcp-journal-writer".' },
|
|
467
|
+
sections: {
|
|
468
|
+
type: 'object',
|
|
469
|
+
required: ['what_happened', 'what_we_built', 'what_we_learned', 'open_threads'],
|
|
470
|
+
properties: {
|
|
471
|
+
what_happened: { type: 'string' },
|
|
472
|
+
what_we_built: { type: 'string' },
|
|
473
|
+
what_we_learned: { type: 'string' },
|
|
474
|
+
open_threads: { type: 'string' },
|
|
475
|
+
},
|
|
476
|
+
additionalProperties: false,
|
|
477
|
+
},
|
|
478
|
+
summary_for_index: { type: 'string', description: 'Single-line summary for the README index row. No newlines, no "|".' },
|
|
479
|
+
feature_code: { type: 'string', description: 'Optional feature code stamped in entry frontmatter.' },
|
|
480
|
+
closing_line: { type: 'string', description: 'Optional final italicized one-liner.' },
|
|
481
|
+
force: { type: 'boolean', description: 'If true and an entry with the same (date, slug) exists, overwrite in place.' },
|
|
482
|
+
idempotency_key: { type: 'string', description: 'Optional caller-supplied key. Same key replays return the cached result without re-mutating.' },
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
name: 'get_journal_entries',
|
|
488
|
+
description: 'Read parsed entries from compose/docs/journal/. Filter by feature_code (exact), session (exact), or since (shorthand "24h"/"7d"/"30m" or ISO date).',
|
|
489
|
+
inputSchema: {
|
|
490
|
+
type: 'object',
|
|
491
|
+
properties: {
|
|
492
|
+
since: { type: 'string' },
|
|
493
|
+
feature_code: { type: 'string' },
|
|
494
|
+
session: { type: 'number' },
|
|
495
|
+
limit: { type: 'number', description: 'Default 50; capped at 500.' },
|
|
496
|
+
},
|
|
497
|
+
},
|
|
498
|
+
},
|
|
499
|
+
// -------------------------------------------------------------------------
|
|
500
|
+
// Completion writer — COMP-MCP-COMPLETION
|
|
501
|
+
// -------------------------------------------------------------------------
|
|
502
|
+
{
|
|
503
|
+
name: 'record_completion',
|
|
504
|
+
description: 'Record a completion bound to a commit SHA. Stores in feature.json completions[]; idempotent on (feature_code, commit_sha); when set_status:true (default) also flips status to COMPLETE via set_feature_status. Audit append best-effort. Status-flip failure rethrows as STATUS_FLIP_AFTER_COMPLETION_RECORDED with err.cause; the completion record is still persisted.',
|
|
505
|
+
inputSchema: {
|
|
506
|
+
type: 'object',
|
|
507
|
+
required: ['feature_code', 'commit_sha', 'tests_pass', 'files_changed'],
|
|
508
|
+
properties: {
|
|
509
|
+
feature_code: { type: 'string' },
|
|
510
|
+
commit_sha: { type: 'string', description: 'Full 40-char hex SHA (Decision 9). Short prefixes are rejected on write. Stored verbatim; commit_sha_short is derived for display only.' },
|
|
511
|
+
tests_pass: { type: 'boolean' },
|
|
512
|
+
files_changed: { type: 'array', items: { type: 'string' } },
|
|
513
|
+
notes: { type: 'string' },
|
|
514
|
+
set_status: { type: 'boolean', description: 'Default true. When true, flips status to COMPLETE via set_feature_status.' },
|
|
515
|
+
force: { type: 'boolean', description: 'If true and a record with the same (feature_code, commit_sha) exists, replace it in place.' },
|
|
516
|
+
idempotency_key: { type: 'string' },
|
|
517
|
+
},
|
|
518
|
+
},
|
|
519
|
+
},
|
|
520
|
+
{
|
|
521
|
+
name: 'get_completions',
|
|
522
|
+
description: 'Read completion records from feature.json files. Filter by feature_code (exact), commit_sha (short or full prefix), or since (shorthand or ISO date).',
|
|
523
|
+
inputSchema: {
|
|
524
|
+
type: 'object',
|
|
525
|
+
properties: {
|
|
526
|
+
feature_code: { type: 'string' },
|
|
527
|
+
commit_sha: { type: 'string' },
|
|
528
|
+
since: { type: 'string' },
|
|
529
|
+
limit: { type: 'number', description: 'Default 50; capped at 500.' },
|
|
530
|
+
},
|
|
531
|
+
},
|
|
532
|
+
},
|
|
315
533
|
];
|
|
316
534
|
|
|
317
535
|
// ---------------------------------------------------------------------------
|
|
@@ -352,6 +570,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
352
570
|
case 'add_roadmap_entry': result = await toolAddRoadmapEntry(args); break;
|
|
353
571
|
case 'set_feature_status': result = await toolSetFeatureStatus(args); break;
|
|
354
572
|
case 'roadmap_diff': result = await toolRoadmapDiff(args); break;
|
|
573
|
+
case 'link_artifact': result = await toolLinkArtifact(args); break;
|
|
574
|
+
case 'link_features': result = await toolLinkFeatures(args); break;
|
|
575
|
+
case 'get_feature_artifacts': result = await toolGetFeatureArtifacts(args); break;
|
|
576
|
+
case 'get_feature_links': result = await toolGetFeatureLinks(args); break;
|
|
577
|
+
case 'add_changelog_entry': result = await toolAddChangelogEntry(args); break;
|
|
578
|
+
case 'get_changelog_entries': result = await toolGetChangelogEntries(args); break;
|
|
579
|
+
case 'write_journal_entry': result = await toolWriteJournalEntry(args); break;
|
|
580
|
+
case 'get_journal_entries': result = await toolGetJournalEntries(args); break;
|
|
581
|
+
case 'record_completion': result = await toolRecordCompletion(args); break;
|
|
582
|
+
case 'get_completions': result = await toolGetCompletions(args); break;
|
|
583
|
+
case 'validate_feature': result = await toolValidateFeature(args); break;
|
|
584
|
+
case 'validate_project': result = await toolValidateProject(args); break;
|
|
355
585
|
// agent_run removed — STRAT-DEDUP-AGENTRUN v1. Use mcp__stratum__stratum_agent_run.
|
|
356
586
|
default:
|
|
357
587
|
return {
|
|
@@ -363,8 +593,21 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
363
593
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
364
594
|
};
|
|
365
595
|
} catch (err) {
|
|
596
|
+
// Surface typed error codes (e.g. INVALID_INPUT, CHANGELOG_FORMAT) when
|
|
597
|
+
// tools attach them, so MCP callers can branch deterministically. Plain
|
|
598
|
+
// errors fall back to the original "Error: <message>" shape.
|
|
599
|
+
// When err.cause is an Error-shaped object, append it so callers can
|
|
600
|
+
// distinguish partial-write sub-errors (e.g. rollback succeeded vs failed).
|
|
601
|
+
let text = err && err.code
|
|
602
|
+
? `Error [${err.code}]: ${err.message}`
|
|
603
|
+
: `Error: ${err.message}`;
|
|
604
|
+
if (err && err.cause && typeof err.cause.message === 'string') {
|
|
605
|
+
text += err.cause.code
|
|
606
|
+
? `\n Caused by [${err.cause.code}]: ${err.cause.message}`
|
|
607
|
+
: `\n Caused by: ${err.cause.message}`;
|
|
608
|
+
}
|
|
366
609
|
return {
|
|
367
|
-
content: [{ type: 'text', text
|
|
610
|
+
content: [{ type: 'text', text }],
|
|
368
611
|
isError: true,
|
|
369
612
|
};
|
|
370
613
|
}
|
|
@@ -5,26 +5,43 @@ import Ajv from 'ajv';
|
|
|
5
5
|
import addFormats from 'ajv-formats';
|
|
6
6
|
|
|
7
7
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
-
const
|
|
8
|
+
const DEFAULT_SCHEMA_PATH = resolve(__dirname, '../contracts/comp-obs-contract.schema.json');
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
// Per-path cache. Each entry: { schema, ajv }.
|
|
11
|
+
const cache = new Map();
|
|
11
12
|
|
|
12
|
-
function load() {
|
|
13
|
-
if (
|
|
14
|
-
const schema = JSON.parse(readFileSync(
|
|
13
|
+
function load(schemaPath = DEFAULT_SCHEMA_PATH) {
|
|
14
|
+
if (cache.has(schemaPath)) return cache.get(schemaPath);
|
|
15
|
+
const schema = JSON.parse(readFileSync(schemaPath, 'utf8'));
|
|
15
16
|
const ajv = new Ajv({ strict: false, allErrors: true });
|
|
16
17
|
addFormats(ajv);
|
|
17
18
|
ajv.addSchema(schema);
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
const entry = { schema, ajv };
|
|
20
|
+
cache.set(schemaPath, entry);
|
|
21
|
+
return entry;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Load (and cache) a schema by absolute path. Returns `{ schema, ajv }`.
|
|
26
|
+
* Used by code that wants to compile arbitrary `$ref`s against the schema
|
|
27
|
+
* without going through the SchemaValidator class.
|
|
28
|
+
*/
|
|
29
|
+
export function loadSchema(schemaPath) {
|
|
30
|
+
return load(schemaPath);
|
|
20
31
|
}
|
|
21
32
|
|
|
22
33
|
export class SchemaValidator {
|
|
23
|
-
|
|
24
|
-
|
|
34
|
+
/**
|
|
35
|
+
* @param {string} [schemaPath] Absolute path to a JSON Schema file.
|
|
36
|
+
* Default is the comp-obs-contract schema (back-compat for existing
|
|
37
|
+
* callers that pass no args).
|
|
38
|
+
*/
|
|
39
|
+
constructor(schemaPath = DEFAULT_SCHEMA_PATH) {
|
|
40
|
+
const { schema, ajv } = load(schemaPath);
|
|
25
41
|
this.schema = schema;
|
|
26
42
|
this.ajv = ajv;
|
|
27
43
|
this._validators = new Map();
|
|
44
|
+
this._rootValidator = null;
|
|
28
45
|
}
|
|
29
46
|
|
|
30
47
|
_getValidator(defName) {
|
|
@@ -39,11 +56,35 @@ export class SchemaValidator {
|
|
|
39
56
|
return v;
|
|
40
57
|
}
|
|
41
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Validate `obj` against `schema.definitions[defName]`. Used by the
|
|
61
|
+
* comp-obs-contract code paths.
|
|
62
|
+
*/
|
|
42
63
|
validate(defName, obj) {
|
|
43
64
|
const v = this._getValidator(defName);
|
|
44
65
|
const valid = v(obj);
|
|
45
66
|
return { valid: !!valid, errors: valid ? [] : (v.errors || []) };
|
|
46
67
|
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Validate `obj` against the schema's root (no $defs/$ref indirection).
|
|
71
|
+
* Used by feature-json / vision-state / roadmap-row schemas, which are
|
|
72
|
+
* top-level shapes without nested definitions.
|
|
73
|
+
*/
|
|
74
|
+
validateRoot(obj) {
|
|
75
|
+
if (!this._rootValidator) {
|
|
76
|
+
// ajv.getSchema by $id returns the root validator if the schema was
|
|
77
|
+
// added via addSchema (which load() does).
|
|
78
|
+
let v = this.schema.$id ? this.ajv.getSchema(this.schema.$id) : null;
|
|
79
|
+
if (!v) v = this.ajv.compile(this.schema);
|
|
80
|
+
this._rootValidator = v;
|
|
81
|
+
}
|
|
82
|
+
const v = this._rootValidator;
|
|
83
|
+
const valid = v(obj);
|
|
84
|
+
return { valid: !!valid, errors: valid ? [] : (v.errors || []) };
|
|
85
|
+
}
|
|
47
86
|
}
|
|
48
87
|
|
|
88
|
+
// Back-compat export — points at the comp-obs schema version. Existing
|
|
89
|
+
// callers consuming SCHEMA_VERSION continue to work unchanged.
|
|
49
90
|
export const SCHEMA_VERSION = load().schema.version;
|