@contentrain/mcp 1.3.0 → 1.4.0
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/dist/index.mjs +1 -1
- package/dist/providers/github/index.d.mts +60 -3
- package/dist/providers/github/index.d.mts.map +1 -1
- package/dist/providers/github/index.mjs +85 -5
- package/dist/providers/github/index.mjs.map +1 -1
- package/dist/server/http/index.d.mts +63 -21
- package/dist/server/http/index.d.mts.map +1 -1
- package/dist/server/http/index.mjs +156 -45
- package/dist/server/http/index.mjs.map +1 -1
- package/dist/{server-B1OJrUQ9.mjs → server-BkhhtEE2.mjs} +2 -2
- package/dist/{server-B1OJrUQ9.mjs.map → server-BkhhtEE2.mjs.map} +1 -1
- package/dist/server.mjs +1 -1
- package/dist/testing/conformance.d.mts +59 -0
- package/dist/testing/conformance.d.mts.map +1 -0
- package/dist/testing/conformance.mjs +59 -0
- package/dist/testing/conformance.mjs.map +1 -0
- package/package.json +10 -5
- package/testing/conformance/01-new-collection-entry/expected/.contentrain/config.json +15 -0
- package/testing/conformance/01-new-collection-entry/expected/.contentrain/content/blog/blog/en.json +6 -0
- package/testing/conformance/01-new-collection-entry/expected/.contentrain/context.json +22 -0
- package/testing/conformance/01-new-collection-entry/expected/.contentrain/meta/blog/en.json +7 -0
- package/testing/conformance/01-new-collection-entry/expected/.contentrain/models/blog.json +16 -0
- package/testing/conformance/01-new-collection-entry/scenario.json +19 -0
- package/testing/conformance/01-new-collection-entry/setup/.contentrain/config.json +15 -0
- package/testing/conformance/01-new-collection-entry/setup/.contentrain/models/blog.json +16 -0
- package/testing/conformance/02-new-singleton/expected/.contentrain/config.json +15 -0
- package/testing/conformance/02-new-singleton/expected/.contentrain/content/marketing/hero/en.json +4 -0
- package/testing/conformance/02-new-singleton/expected/.contentrain/context.json +22 -0
- package/testing/conformance/02-new-singleton/expected/.contentrain/meta/hero/en.json +5 -0
- package/testing/conformance/02-new-singleton/expected/.contentrain/models/hero.json +16 -0
- package/testing/conformance/02-new-singleton/scenario.json +18 -0
- package/testing/conformance/02-new-singleton/setup/.contentrain/config.json +15 -0
- package/testing/conformance/02-new-singleton/setup/.contentrain/models/hero.json +16 -0
- package/testing/conformance/03-new-dictionary/expected/.contentrain/config.json +15 -0
- package/testing/conformance/03-new-dictionary/expected/.contentrain/content/marketing/ui-strings/en.json +5 -0
- package/testing/conformance/03-new-dictionary/expected/.contentrain/context.json +22 -0
- package/testing/conformance/03-new-dictionary/expected/.contentrain/meta/ui-strings/en.json +5 -0
- package/testing/conformance/03-new-dictionary/expected/.contentrain/models/ui-strings.json +7 -0
- package/testing/conformance/03-new-dictionary/scenario.json +19 -0
- package/testing/conformance/03-new-dictionary/setup/.contentrain/config.json +15 -0
- package/testing/conformance/03-new-dictionary/setup/.contentrain/models/ui-strings.json +7 -0
- package/testing/conformance/README.md +77 -0
- package/testing/conformance/scenarios.json +36 -0
package/dist/server.mjs
CHANGED
|
@@ -12,7 +12,7 @@ import "./local-DvP4Y4T-.mjs";
|
|
|
12
12
|
import "./detect-DL9mFmaS.mjs";
|
|
13
13
|
import "./branch-lifecycle-CFVpTpNT.mjs";
|
|
14
14
|
import "./annotations-Clhbw2lh.mjs";
|
|
15
|
-
import { t as createServer } from "./server-
|
|
15
|
+
import { t as createServer } from "./server-BkhhtEE2.mjs";
|
|
16
16
|
import "./validator-B0nNRFeU.mjs";
|
|
17
17
|
import "./scan-config-jC6XmiqF.mjs";
|
|
18
18
|
import "./graph-builder-DXv2JEp-.mjs";
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
//#region src/testing/conformance.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Conformance fixture loader — published as `@contentrain/mcp/testing/conformance`
|
|
4
|
+
* so external tools (Studio, third-party reimplementations, alt-provider
|
|
5
|
+
* test harnesses) can assert byte-identical output against the same
|
|
6
|
+
* scenarios `@contentrain/mcp` tests itself against.
|
|
7
|
+
*
|
|
8
|
+
* Each scenario directory under `testing/conformance/` is:
|
|
9
|
+
*
|
|
10
|
+
* scenario.json — { id, description, operation, input, context_tool? }
|
|
11
|
+
* setup/ — files seeded into a temp projectRoot before the op
|
|
12
|
+
* expected/ — byte-identical file tree after the op runs
|
|
13
|
+
*
|
|
14
|
+
* Consumers use the `fixturesDir` export to locate the root and the
|
|
15
|
+
* helpers below to iterate scenarios without hardcoding directory names.
|
|
16
|
+
*
|
|
17
|
+
* The fixtures are published as-is — no transformation, no compilation.
|
|
18
|
+
* They ship next to the package so consumers can read them with
|
|
19
|
+
* `node:fs` without additional dependencies.
|
|
20
|
+
*/
|
|
21
|
+
interface ConformanceScenario {
|
|
22
|
+
/** Stable scenario id — e.g. `01-new-collection-entry`. */
|
|
23
|
+
id: string;
|
|
24
|
+
/** Short human-readable description pulled from `scenario.json`. */
|
|
25
|
+
description: string;
|
|
26
|
+
/** The write operation under test — `save_content`, `save_model`, `delete_content`, …. */
|
|
27
|
+
operation: string;
|
|
28
|
+
/** Tool input object — passed into the operation at runtime. */
|
|
29
|
+
input: Record<string, unknown>;
|
|
30
|
+
/** Optional MCP tool name recorded in the resulting `context.json`. */
|
|
31
|
+
context_tool?: string;
|
|
32
|
+
/** Absolute path to this scenario's directory on disk. */
|
|
33
|
+
dir: string;
|
|
34
|
+
/** Absolute path to this scenario's `setup/` tree. */
|
|
35
|
+
setupDir: string;
|
|
36
|
+
/** Absolute path to this scenario's `expected/` tree. */
|
|
37
|
+
expectedDir: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Resolve the conformance fixtures root. When called from the built
|
|
41
|
+
* package (`dist/testing/conformance.mjs`), the fixtures live at
|
|
42
|
+
* `../../testing/conformance`. When called from source tests
|
|
43
|
+
* (`src/testing/conformance.ts`), they live at
|
|
44
|
+
* `../../testing/conformance` as well — the layout is identical.
|
|
45
|
+
*/
|
|
46
|
+
declare const fixturesDir: string;
|
|
47
|
+
/**
|
|
48
|
+
* List every scenario directory under `fixturesDir`. Returns an array
|
|
49
|
+
* sorted by `id` for deterministic iteration.
|
|
50
|
+
*/
|
|
51
|
+
declare function listConformanceScenarios(): ConformanceScenario[];
|
|
52
|
+
/**
|
|
53
|
+
* Load a single scenario by id. Throws with a helpful message when the
|
|
54
|
+
* id does not resolve to a published fixture.
|
|
55
|
+
*/
|
|
56
|
+
declare function loadConformanceScenario(id: string): ConformanceScenario;
|
|
57
|
+
//#endregion
|
|
58
|
+
export { ConformanceScenario, fixturesDir, listConformanceScenarios, loadConformanceScenario };
|
|
59
|
+
//# sourceMappingURL=conformance.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conformance.d.mts","names":[],"sources":["../../src/testing/conformance.ts"],"mappings":";;AAwBA;;;;;;;;;;;;;;;AA4BA;;;UA5BiB,mBAAA;EA4B2E;EA1B1F,EAAA;EAgCsC;EA9BtC,WAAA;EA8B0C;EA5B1C,SAAA;EAqDc;EAnDd,KAAA,EAAO,MAAA;;EAEP,YAAA;EAiDsE;EA/CtE,GAAA;;EAEA,QAAA;;EAEA,WAAA;AAAA;;;;;;;;cAYW,WAAA;;;;;iBAMG,wBAAA,CAAA,GAA4B,mBAAA;;;;;iBAyB5B,uBAAA,CAAwB,EAAA,WAAa,mBAAA"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { dirname, join, resolve } from "node:path";
|
|
2
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
/**
|
|
5
|
+
* Resolve the conformance fixtures root. When called from the built
|
|
6
|
+
* package (`dist/testing/conformance.mjs`), the fixtures live at
|
|
7
|
+
* `../../testing/conformance`. When called from source tests
|
|
8
|
+
* (`src/testing/conformance.ts`), they live at
|
|
9
|
+
* `../../testing/conformance` as well — the layout is identical.
|
|
10
|
+
*/
|
|
11
|
+
const fixturesDir = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "testing", "conformance");
|
|
12
|
+
/**
|
|
13
|
+
* List every scenario directory under `fixturesDir`. Returns an array
|
|
14
|
+
* sorted by `id` for deterministic iteration.
|
|
15
|
+
*/
|
|
16
|
+
function listConformanceScenarios() {
|
|
17
|
+
if (!existsSync(fixturesDir)) throw new Error(`Conformance fixtures not found at ${fixturesDir}. Is @contentrain/mcp/testing/conformance installed as a dev dependency?`);
|
|
18
|
+
const entries = readdirSync(fixturesDir, { withFileTypes: true });
|
|
19
|
+
const scenarios = [];
|
|
20
|
+
for (const entry of entries) {
|
|
21
|
+
if (!entry.isDirectory()) continue;
|
|
22
|
+
if (entry.name.startsWith("_") || entry.name.startsWith(".")) continue;
|
|
23
|
+
const dir = join(fixturesDir, entry.name);
|
|
24
|
+
const scenarioPath = join(dir, "scenario.json");
|
|
25
|
+
if (!existsSync(scenarioPath)) continue;
|
|
26
|
+
scenarios.push({
|
|
27
|
+
id: entry.name,
|
|
28
|
+
...loadScenarioFile(scenarioPath),
|
|
29
|
+
dir,
|
|
30
|
+
setupDir: join(dir, "setup"),
|
|
31
|
+
expectedDir: join(dir, "expected")
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return scenarios.toSorted((a, b) => a.id.localeCompare(b.id));
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Load a single scenario by id. Throws with a helpful message when the
|
|
38
|
+
* id does not resolve to a published fixture.
|
|
39
|
+
*/
|
|
40
|
+
function loadConformanceScenario(id) {
|
|
41
|
+
const all = listConformanceScenarios();
|
|
42
|
+
const match = all.find((s) => s.id === id);
|
|
43
|
+
if (!match) throw new Error(`Unknown conformance scenario "${id}". Known ids: ${all.map((s) => s.id).join(", ")}`);
|
|
44
|
+
return match;
|
|
45
|
+
}
|
|
46
|
+
function loadScenarioFile(path) {
|
|
47
|
+
const raw = readFileSync(path, "utf-8");
|
|
48
|
+
const parsed = JSON.parse(raw);
|
|
49
|
+
return {
|
|
50
|
+
description: parsed.description,
|
|
51
|
+
operation: parsed.operation,
|
|
52
|
+
input: parsed.input,
|
|
53
|
+
...parsed.context_tool ? { context_tool: parsed.context_tool } : {}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
//#endregion
|
|
57
|
+
export { fixturesDir, listConformanceScenarios, loadConformanceScenario };
|
|
58
|
+
|
|
59
|
+
//# sourceMappingURL=conformance.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conformance.mjs","names":[],"sources":["../../src/testing/conformance.ts"],"sourcesContent":["import { existsSync, readdirSync, readFileSync } from 'node:fs'\nimport { dirname, join, resolve } from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\n/**\n * Conformance fixture loader — published as `@contentrain/mcp/testing/conformance`\n * so external tools (Studio, third-party reimplementations, alt-provider\n * test harnesses) can assert byte-identical output against the same\n * scenarios `@contentrain/mcp` tests itself against.\n *\n * Each scenario directory under `testing/conformance/` is:\n *\n * scenario.json — { id, description, operation, input, context_tool? }\n * setup/ — files seeded into a temp projectRoot before the op\n * expected/ — byte-identical file tree after the op runs\n *\n * Consumers use the `fixturesDir` export to locate the root and the\n * helpers below to iterate scenarios without hardcoding directory names.\n *\n * The fixtures are published as-is — no transformation, no compilation.\n * They ship next to the package so consumers can read them with\n * `node:fs` without additional dependencies.\n */\n\nexport interface ConformanceScenario {\n /** Stable scenario id — e.g. `01-new-collection-entry`. */\n id: string\n /** Short human-readable description pulled from `scenario.json`. */\n description: string\n /** The write operation under test — `save_content`, `save_model`, `delete_content`, …. */\n operation: string\n /** Tool input object — passed into the operation at runtime. */\n input: Record<string, unknown>\n /** Optional MCP tool name recorded in the resulting `context.json`. */\n context_tool?: string\n /** Absolute path to this scenario's directory on disk. */\n dir: string\n /** Absolute path to this scenario's `setup/` tree. */\n setupDir: string\n /** Absolute path to this scenario's `expected/` tree. */\n expectedDir: string\n}\n\nconst MODULE_DIR = dirname(fileURLToPath(import.meta.url))\n\n/**\n * Resolve the conformance fixtures root. When called from the built\n * package (`dist/testing/conformance.mjs`), the fixtures live at\n * `../../testing/conformance`. When called from source tests\n * (`src/testing/conformance.ts`), they live at\n * `../../testing/conformance` as well — the layout is identical.\n */\nexport const fixturesDir: string = resolve(MODULE_DIR, '..', '..', 'testing', 'conformance')\n\n/**\n * List every scenario directory under `fixturesDir`. Returns an array\n * sorted by `id` for deterministic iteration.\n */\nexport function listConformanceScenarios(): ConformanceScenario[] {\n if (!existsSync(fixturesDir)) {\n throw new Error(\n `Conformance fixtures not found at ${fixturesDir}. `\n + 'Is @contentrain/mcp/testing/conformance installed as a dev dependency?',\n )\n }\n\n const entries = readdirSync(fixturesDir, { withFileTypes: true })\n const scenarios: ConformanceScenario[] = []\n for (const entry of entries) {\n if (!entry.isDirectory()) continue\n if (entry.name.startsWith('_') || entry.name.startsWith('.')) continue\n const dir = join(fixturesDir, entry.name)\n const scenarioPath = join(dir, 'scenario.json')\n if (!existsSync(scenarioPath)) continue\n scenarios.push({ id: entry.name, ...loadScenarioFile(scenarioPath), dir, setupDir: join(dir, 'setup'), expectedDir: join(dir, 'expected') })\n }\n return scenarios.toSorted((a, b) => a.id.localeCompare(b.id))\n}\n\n/**\n * Load a single scenario by id. Throws with a helpful message when the\n * id does not resolve to a published fixture.\n */\nexport function loadConformanceScenario(id: string): ConformanceScenario {\n const all = listConformanceScenarios()\n const match = all.find(s => s.id === id)\n if (!match) {\n throw new Error(`Unknown conformance scenario \"${id}\". Known ids: ${all.map(s => s.id).join(', ')}`)\n }\n return match\n}\n\nfunction loadScenarioFile(path: string): Omit<ConformanceScenario, 'id' | 'dir' | 'setupDir' | 'expectedDir'> {\n const raw = readFileSync(path, 'utf-8')\n const parsed = JSON.parse(raw) as {\n description: string\n operation: string\n input: Record<string, unknown>\n context_tool?: string\n }\n return {\n description: parsed.description,\n operation: parsed.operation,\n input: parsed.input,\n ...(parsed.context_tool ? { context_tool: parsed.context_tool } : {}),\n }\n}\n"],"mappings":";;;;;;;;;;AAoDA,MAAa,cAAsB,QAThB,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC,EASH,MAAM,MAAM,WAAW,cAAc;;;;;AAM5F,SAAgB,2BAAkD;AAChE,KAAI,CAAC,WAAW,YAAY,CAC1B,OAAM,IAAI,MACR,qCAAqC,YAAY,0EAElD;CAGH,MAAM,UAAU,YAAY,aAAa,EAAE,eAAe,MAAM,CAAC;CACjE,MAAM,YAAmC,EAAE;AAC3C,MAAK,MAAM,SAAS,SAAS;AAC3B,MAAI,CAAC,MAAM,aAAa,CAAE;AAC1B,MAAI,MAAM,KAAK,WAAW,IAAI,IAAI,MAAM,KAAK,WAAW,IAAI,CAAE;EAC9D,MAAM,MAAM,KAAK,aAAa,MAAM,KAAK;EACzC,MAAM,eAAe,KAAK,KAAK,gBAAgB;AAC/C,MAAI,CAAC,WAAW,aAAa,CAAE;AAC/B,YAAU,KAAK;GAAE,IAAI,MAAM;GAAM,GAAG,iBAAiB,aAAa;GAAE;GAAK,UAAU,KAAK,KAAK,QAAQ;GAAE,aAAa,KAAK,KAAK,WAAW;GAAE,CAAC;;AAE9I,QAAO,UAAU,UAAU,GAAG,MAAM,EAAE,GAAG,cAAc,EAAE,GAAG,CAAC;;;;;;AAO/D,SAAgB,wBAAwB,IAAiC;CACvE,MAAM,MAAM,0BAA0B;CACtC,MAAM,QAAQ,IAAI,MAAK,MAAK,EAAE,OAAO,GAAG;AACxC,KAAI,CAAC,MACH,OAAM,IAAI,MAAM,iCAAiC,GAAG,gBAAgB,IAAI,KAAI,MAAK,EAAE,GAAG,CAAC,KAAK,KAAK,GAAG;AAEtG,QAAO;;AAGT,SAAS,iBAAiB,MAAoF;CAC5G,MAAM,MAAM,aAAa,MAAM,QAAQ;CACvC,MAAM,SAAS,KAAK,MAAM,IAAI;AAM9B,QAAO;EACL,aAAa,OAAO;EACpB,WAAW,OAAO;EAClB,OAAO,OAAO;EACd,GAAI,OAAO,eAAe,EAAE,cAAc,OAAO,cAAc,GAAG,EAAE;EACrE"}
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contentrain/mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"mcpName": "io.github.Contentrain/contentrain",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"description": "Local-first MCP server for AI-generated content governance —
|
|
6
|
+
"description": "Local-first MCP server for AI-generated content governance — 17 deterministic tools, stdio + HTTP transports, Local / GitHub / GitLab providers",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
@@ -120,6 +120,10 @@
|
|
|
120
120
|
"types": "./dist/tools/annotations.d.mts",
|
|
121
121
|
"import": "./dist/tools/annotations.mjs"
|
|
122
122
|
},
|
|
123
|
+
"./testing/conformance": {
|
|
124
|
+
"types": "./dist/testing/conformance.d.mts",
|
|
125
|
+
"import": "./dist/testing/conformance.mjs"
|
|
126
|
+
},
|
|
123
127
|
"./templates": {
|
|
124
128
|
"types": "./dist/templates/index.d.mts",
|
|
125
129
|
"import": "./dist/templates/index.mjs"
|
|
@@ -140,7 +144,8 @@
|
|
|
140
144
|
"main": "./dist/index.mjs",
|
|
141
145
|
"types": "./dist/index.d.mts",
|
|
142
146
|
"files": [
|
|
143
|
-
"dist"
|
|
147
|
+
"dist",
|
|
148
|
+
"testing"
|
|
144
149
|
],
|
|
145
150
|
"dependencies": {
|
|
146
151
|
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
@@ -177,8 +182,8 @@
|
|
|
177
182
|
"vitest": "^3.0.0"
|
|
178
183
|
},
|
|
179
184
|
"scripts": {
|
|
180
|
-
"build": "tsdown src/index.ts src/server.ts src/core/config.ts src/core/context.ts src/core/model-manager.ts src/core/content-manager.ts src/core/meta-manager.ts src/core/validator/index.ts src/core/scanner.ts src/core/scan-config.ts src/core/doctor.ts src/core/graph-builder.ts src/core/apply-manager.ts src/util/detect.ts src/util/fs.ts src/util/id.ts src/git/transaction.ts src/git/branch-lifecycle.ts src/tools/annotations.ts src/templates/index.ts src/core/contracts/index.ts src/core/ops/index.ts src/core/overlay-reader.ts src/providers/local/index.ts src/providers/github/index.ts src/providers/gitlab/index.ts src/server/http/index.ts --format esm --dts --external typescript",
|
|
181
|
-
"dev": "tsdown src/index.ts src/server.ts src/core/config.ts src/core/context.ts src/core/model-manager.ts src/core/content-manager.ts src/core/meta-manager.ts src/core/validator/index.ts src/core/scanner.ts src/core/scan-config.ts src/core/doctor.ts src/core/graph-builder.ts src/core/apply-manager.ts src/util/detect.ts src/util/fs.ts src/util/id.ts src/git/transaction.ts src/git/branch-lifecycle.ts src/tools/annotations.ts src/templates/index.ts src/core/contracts/index.ts src/core/ops/index.ts src/core/overlay-reader.ts src/providers/local/index.ts src/providers/github/index.ts src/providers/gitlab/index.ts src/server/http/index.ts --format esm --dts --external typescript --watch",
|
|
185
|
+
"build": "tsdown src/index.ts src/server.ts src/core/config.ts src/core/context.ts src/core/model-manager.ts src/core/content-manager.ts src/core/meta-manager.ts src/core/validator/index.ts src/core/scanner.ts src/core/scan-config.ts src/core/doctor.ts src/core/graph-builder.ts src/core/apply-manager.ts src/util/detect.ts src/util/fs.ts src/util/id.ts src/git/transaction.ts src/git/branch-lifecycle.ts src/tools/annotations.ts src/testing/conformance.ts src/templates/index.ts src/core/contracts/index.ts src/core/ops/index.ts src/core/overlay-reader.ts src/providers/local/index.ts src/providers/github/index.ts src/providers/gitlab/index.ts src/server/http/index.ts --format esm --dts --external typescript",
|
|
186
|
+
"dev": "tsdown src/index.ts src/server.ts src/core/config.ts src/core/context.ts src/core/model-manager.ts src/core/content-manager.ts src/core/meta-manager.ts src/core/validator/index.ts src/core/scanner.ts src/core/scan-config.ts src/core/doctor.ts src/core/graph-builder.ts src/core/apply-manager.ts src/util/detect.ts src/util/fs.ts src/util/id.ts src/git/transaction.ts src/git/branch-lifecycle.ts src/tools/annotations.ts src/testing/conformance.ts src/templates/index.ts src/core/contracts/index.ts src/core/ops/index.ts src/core/overlay-reader.ts src/providers/local/index.ts src/providers/github/index.ts src/providers/gitlab/index.ts src/server/http/index.ts --format esm --dts --external typescript --watch",
|
|
182
187
|
"test": "vitest run",
|
|
183
188
|
"typecheck": "tsc --noEmit",
|
|
184
189
|
"clean": "rm -rf dist"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lastOperation": {
|
|
3
|
+
"entries": [
|
|
4
|
+
"abc123def456"
|
|
5
|
+
],
|
|
6
|
+
"locale": "en",
|
|
7
|
+
"model": "blog",
|
|
8
|
+
"source": "mcp-local",
|
|
9
|
+
"timestamp": "2026-01-01T00:00:00.000Z",
|
|
10
|
+
"tool": "contentrain_content_save"
|
|
11
|
+
},
|
|
12
|
+
"stats": {
|
|
13
|
+
"entries": 1,
|
|
14
|
+
"lastSync": "2026-01-01T00:00:00.000Z",
|
|
15
|
+
"locales": [
|
|
16
|
+
"en",
|
|
17
|
+
"tr"
|
|
18
|
+
],
|
|
19
|
+
"models": 1
|
|
20
|
+
},
|
|
21
|
+
"version": "1"
|
|
22
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"context_tool": "contentrain_content_save",
|
|
3
|
+
"description": "Save a new collection entry for a model",
|
|
4
|
+
"id": "01-new-collection-entry",
|
|
5
|
+
"input": {
|
|
6
|
+
"entries": [
|
|
7
|
+
{
|
|
8
|
+
"data": {
|
|
9
|
+
"body": "First post content",
|
|
10
|
+
"title": "Hello World"
|
|
11
|
+
},
|
|
12
|
+
"id": "abc123def456",
|
|
13
|
+
"locale": "en"
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"model": "blog"
|
|
17
|
+
},
|
|
18
|
+
"operation": "save_content"
|
|
19
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lastOperation": {
|
|
3
|
+
"entries": [
|
|
4
|
+
"en"
|
|
5
|
+
],
|
|
6
|
+
"locale": "en",
|
|
7
|
+
"model": "hero",
|
|
8
|
+
"source": "mcp-local",
|
|
9
|
+
"timestamp": "2026-01-01T00:00:00.000Z",
|
|
10
|
+
"tool": "contentrain_content_save"
|
|
11
|
+
},
|
|
12
|
+
"stats": {
|
|
13
|
+
"entries": 1,
|
|
14
|
+
"lastSync": "2026-01-01T00:00:00.000Z",
|
|
15
|
+
"locales": [
|
|
16
|
+
"en",
|
|
17
|
+
"tr"
|
|
18
|
+
],
|
|
19
|
+
"models": 1
|
|
20
|
+
},
|
|
21
|
+
"version": "1"
|
|
22
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"context_tool": "contentrain_content_save",
|
|
3
|
+
"description": "Save a new singleton content block",
|
|
4
|
+
"id": "02-new-singleton",
|
|
5
|
+
"input": {
|
|
6
|
+
"entries": [
|
|
7
|
+
{
|
|
8
|
+
"data": {
|
|
9
|
+
"subtitle": "Build the web with AI agents",
|
|
10
|
+
"title": "Welcome to Contentrain"
|
|
11
|
+
},
|
|
12
|
+
"locale": "en"
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"model": "hero"
|
|
16
|
+
},
|
|
17
|
+
"operation": "save_content"
|
|
18
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lastOperation": {
|
|
3
|
+
"entries": [
|
|
4
|
+
"en"
|
|
5
|
+
],
|
|
6
|
+
"locale": "en",
|
|
7
|
+
"model": "ui-strings",
|
|
8
|
+
"source": "mcp-local",
|
|
9
|
+
"timestamp": "2026-01-01T00:00:00.000Z",
|
|
10
|
+
"tool": "contentrain_content_save"
|
|
11
|
+
},
|
|
12
|
+
"stats": {
|
|
13
|
+
"entries": 1,
|
|
14
|
+
"lastSync": "2026-01-01T00:00:00.000Z",
|
|
15
|
+
"locales": [
|
|
16
|
+
"en",
|
|
17
|
+
"tr"
|
|
18
|
+
],
|
|
19
|
+
"models": 1
|
|
20
|
+
},
|
|
21
|
+
"version": "1"
|
|
22
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"context_tool": "contentrain_content_save",
|
|
3
|
+
"description": "Create a dictionary with multiple string keys",
|
|
4
|
+
"id": "03-new-dictionary",
|
|
5
|
+
"input": {
|
|
6
|
+
"entries": [
|
|
7
|
+
{
|
|
8
|
+
"data": {
|
|
9
|
+
"hero.cta": "Get started",
|
|
10
|
+
"hero.subtitle": "Build the web with AI agents",
|
|
11
|
+
"hero.title": "Welcome to Contentrain"
|
|
12
|
+
},
|
|
13
|
+
"locale": "en"
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"model": "ui-strings"
|
|
17
|
+
},
|
|
18
|
+
"operation": "save_content"
|
|
19
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Conformance Fixtures
|
|
2
|
+
|
|
3
|
+
Byte-identical conformance test suite for content operations. Each fixture describes an operation and its expected on-disk file tree after execution. Used by `packages/mcp/tests/conformance.test.ts`.
|
|
4
|
+
|
|
5
|
+
**Purpose** — guarantee byte-level parity between providers (LocalProvider today; GitHubProvider + GitLabProvider in later phases) for the same operation. Any silent drift in serialization, meta generation, context.json, or path resolution shows up as a failed fixture.
|
|
6
|
+
|
|
7
|
+
## Directory layout
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
conformance/
|
|
11
|
+
├── README.md — this file
|
|
12
|
+
├── scenarios.json — human-readable index
|
|
13
|
+
├── 01-new-collection-entry/
|
|
14
|
+
│ ├── scenario.json — input spec
|
|
15
|
+
│ ├── setup/ — files copied into temp projectRoot before op runs (optional)
|
|
16
|
+
│ └── expected/ — byte-identical file tree after op (GENERATED, committed)
|
|
17
|
+
├── 02-new-singleton/
|
|
18
|
+
│ └── ...
|
|
19
|
+
└── 03-new-dictionary/
|
|
20
|
+
└── ...
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Scenario schema
|
|
24
|
+
|
|
25
|
+
```jsonc
|
|
26
|
+
{
|
|
27
|
+
"id": "01-new-collection-entry",
|
|
28
|
+
"description": "Save a new collection entry",
|
|
29
|
+
"operation": "save_content", // save_content | save_model | delete_content
|
|
30
|
+
"context_tool": "contentrain_content_save", // tool name recorded in context.json (optional)
|
|
31
|
+
"input": { // operation-specific params
|
|
32
|
+
"model": "blog",
|
|
33
|
+
"entries": [
|
|
34
|
+
{ "id": "...", "locale": "en", "data": { ... } }
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Determinism rules
|
|
41
|
+
|
|
42
|
+
The harness fixes non-deterministic sources so output is reproducible:
|
|
43
|
+
|
|
44
|
+
- `Date.now()` / `new Date()` returns `2026-01-01T00:00:00.000Z` (vitest fake timers)
|
|
45
|
+
- `CONTENTRAIN_SOURCE=mcp-local` env var is set
|
|
46
|
+
- `Math.random()` is NOT stubbed globally — scenarios that need determinism must pass explicit entry IDs instead of relying on `generateEntryId()`
|
|
47
|
+
- Git is not initialized — core write functions don't require it
|
|
48
|
+
|
|
49
|
+
## Regenerating `expected/`
|
|
50
|
+
|
|
51
|
+
When an intentional behavior change lands (e.g., schema bump, new default field), regenerate the expected fixtures and review the diff:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
GENERATE_FIXTURES=1 pnpm --filter @contentrain/mcp test -- conformance
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Then `git diff` the `expected/` directories. Each byte difference must be a conscious decision.
|
|
58
|
+
|
|
59
|
+
## Adding a new fixture
|
|
60
|
+
|
|
61
|
+
1. `mkdir NN-short-name/setup/.contentrain`
|
|
62
|
+
2. Write `scenario.json` at the fixture root
|
|
63
|
+
3. Optionally add setup files under `setup/`
|
|
64
|
+
4. `GENERATE_FIXTURES=1 pnpm --filter @contentrain/mcp test -- conformance` to populate `expected/`
|
|
65
|
+
5. Review generated `expected/` files manually — they become the contract
|
|
66
|
+
6. Commit both `setup/` and `expected/`
|
|
67
|
+
|
|
68
|
+
## Provider matrix (future)
|
|
69
|
+
|
|
70
|
+
Today the harness runs scenarios against the current MCP write path (direct fs + `writeContent` + `writeContext`). Post-refactor phases will add:
|
|
71
|
+
|
|
72
|
+
- Phase 2 — run through plan/apply API, assert `FileChange[]` → same `expected/`
|
|
73
|
+
- Phase 3 — LocalProvider.applyPlan → same `expected/`
|
|
74
|
+
- Phase 5 — GitHubProvider.applyPlan (mocked) → same `expected/`
|
|
75
|
+
- Phase 8 — GitLab/Bitbucket providers → same `expected/`
|
|
76
|
+
|
|
77
|
+
The fixtures themselves never change between providers. Only the runner wiring does.
|