prompt-contract 0.2.0 → 0.3.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.
@@ -1,85 +0,0 @@
1
- import { test } from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import { enhance, assembleMessages, hardConstraints } from '../src/pipeline.js';
4
- import { PromptContractError } from '../src/errors.js';
5
-
6
- const PROFILE = { name: 'coding-agent', maxChars: 800, body: 'You rewrite vague requests for a coding assistant.' };
7
-
8
- function fakeProvider(reply, { fail } = {}) {
9
- const calls = [];
10
- return {
11
- calls,
12
- async complete(opts) {
13
- calls.push(opts);
14
- if (fail === 'abort') { const e = new Error('aborted'); e.name = 'AbortError'; throw e; }
15
- if (fail === 'boom') throw new Error('socket hang up');
16
- return { text: reply };
17
- }
18
- };
19
- }
20
-
21
- test('assembleMessages embeds profile body, constraints, strength and context', () => {
22
- const { system, user } = assembleMessages('做个网站', { profile: PROFILE, strength: 'expand', context: 'repo: pet-project' });
23
- assert.match(system, /You rewrite vague requests/);
24
- assert.match(system, /HARD CONSTRAINTS:/);
25
- assert.match(system, /under 800 characters/);
26
- assert.match(system, /STRENGTH MODE: EXPAND/);
27
- assert.match(user, /USER INPUT:\n做个网站/);
28
- assert.match(user, /CONTEXT \(background[^\n]*\):\nrepo: pet-project/);
29
- });
30
-
31
- test('all three strengths render distinct modes', () => {
32
- for (const s of ['polish', 'standard', 'expand']) {
33
- const { system } = assembleMessages('x', { profile: PROFILE, strength: s });
34
- assert.match(system, new RegExp(`STRENGTH MODE: ${s.toUpperCase()}`));
35
- }
36
- });
37
-
38
- test('hardConstraints mirror the six eval rules', () => {
39
- const hc = hardConstraints({ maxChars: 800, strength: 'standard' });
40
- assert.match(hc, /same language as USER INPUT/);
41
- assert.match(hc, /no markdown fences/);
42
- assert.match(hc, /under 800 characters/);
43
- assert.match(hc, /EXPAND, DO NOT ANSWER/);
44
- assert.match(hc, /lightly polish/);
45
- assert.match(hc, /do not add requirements, features, or technologies/);
46
- });
47
-
48
- test('enhance: happy path returns cleaned text + original + meta', async () => {
49
- const provider = fakeProvider(' “请结构化地说明……” ');
50
- const res = await enhance('帮我解释', { profile: PROFILE, provider, model: 'm1', strength: 'standard' });
51
- assert.equal(res.text, '请结构化地说明……');
52
- assert.equal(res.original, '帮我解释');
53
- assert.equal(res.meta.profile, 'coding-agent');
54
- assert.equal(res.meta.model, 'm1');
55
- assert.equal(typeof res.meta.ms, 'number');
56
- assert.equal(provider.calls[0].maxTokens, Math.ceil(800 * 1.2));
57
- });
58
-
59
- test('enhance: empty input → empty_input', async () => {
60
- await assert.rejects(
61
- () => enhance(' ', { profile: PROFILE, provider: fakeProvider('x') }),
62
- (e) => e instanceof PromptContractError && e.code === 'empty_input'
63
- );
64
- });
65
-
66
- test('enhance: empty provider result → llm_error', async () => {
67
- await assert.rejects(
68
- () => enhance('hello', { profile: PROFILE, provider: fakeProvider(' ') }),
69
- (e) => e.code === 'llm_error'
70
- );
71
- });
72
-
73
- test('enhance: caller abort → aborted (ADR-017 semantics)', async () => {
74
- await assert.rejects(
75
- () => enhance('hello', { profile: PROFILE, provider: fakeProvider(null, { fail: 'abort' }) }),
76
- (e) => e.code === 'aborted'
77
- );
78
- });
79
-
80
- test('enhance: unexpected provider crash → provider_unavailable', async () => {
81
- await assert.rejects(
82
- () => enhance('hello', { profile: PROFILE, provider: fakeProvider(null, { fail: 'boom' }) }),
83
- (e) => e.code === 'provider_unavailable'
84
- );
85
- });
@@ -1,40 +0,0 @@
1
- import { test } from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import { parseProfile, parseYamlLite } from '../src/profile.js';
4
- import { PromptContractError } from '../src/errors.js';
5
-
6
- const SAMPLE = `---
7
- name: test-profile
8
- domain: 测试场景
9
- maxChars: 600
10
- noUnmentionedTech: true
11
- ---
12
- You rewrite vague requests. Preserve intent and language.`;
13
-
14
- test('parseProfile reads frontmatter scalars and body', () => {
15
- const p = parseProfile(SAMPLE, { path: 'test.md' });
16
- assert.equal(p.name, 'test-profile');
17
- assert.equal(p.domain, '测试场景');
18
- assert.equal(p.maxChars, 600);
19
- assert.equal(p.noUnmentionedTech, true);
20
- assert.match(p.body, /Preserve intent and language\.$/);
21
- });
22
-
23
- test('parseProfile applies defaults', () => {
24
- const p = parseProfile('---\nname: minimal\n---\nBody here.');
25
- assert.equal(p.maxChars, 800);
26
- assert.equal(p.noUnmentionedTech, true);
27
- assert.equal(p.domain, '');
28
- });
29
-
30
- test('parseProfile rejects malformed input with config_error', () => {
31
- assert.throws(() => parseProfile('no frontmatter here'), (e) => e instanceof PromptContractError && e.code === 'config_error');
32
- assert.throws(() => parseProfile('---\ndomain: x\n---\nbody'), (e) => e.code === 'config_error');
33
- });
34
-
35
- test('parseYamlLite handles string lists', () => {
36
- const y = parseYamlLite('items:\n - a\n - b\nflag: false\ncount: 3');
37
- assert.deepEqual(y.items, ['a', 'b']);
38
- assert.equal(y.flag, false);
39
- assert.equal(y.count, 3);
40
- });
@@ -1,53 +0,0 @@
1
- import { test } from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import { checkRules } from '../src/rules.js';
4
-
5
- const GOOD_EN = 'Build a small responsive website showcasing one pet: a photo gallery, a biography page, and an update feed. Keep navigation simple and skip comments.';
6
- const GOOD_ZH = '做一个展示宠物的小型网站:包含照片画廊、简介页与动态页,导航保持单层,暂不需要评论功能。';
7
-
8
- test('good outputs pass all six assertions', () => {
9
- for (const [orig, out] of [['A website for my dog', GOOD_EN], ['帮我做一个宠物网站', GOOD_ZH]]) {
10
- const { pass, results } = checkRules(orig, out);
11
- assert.equal(pass, true, JSON.stringify(results.filter((r) => !r.pass)));
12
- }
13
- });
14
-
15
- test('lang-consistency fails on script switch', () => {
16
- const { results } = checkRules('帮我做一个网站', GOOD_EN);
17
- const r = results.find((r) => r.id === 'lang-consistency');
18
- assert.equal(r.pass, false);
19
- });
20
-
21
- test('only-enhanced-text fails on fences, wrapping quotes and meta labels', () => {
22
- for (const bad of ['```text\nx\n```', '“结果”', '增强后的提示词:写一封邮件', 'Enhanced prompt: do something']) {
23
- const { results } = checkRules('写一封邮件', bad, { maxChars: 800 });
24
- assert.equal(results.find((r) => r.id === 'only-enhanced-text').pass, false, bad);
25
- }
26
- });
27
-
28
- test('length-limit fails on overflow and dangling colon', () => {
29
- const long = 'x'.repeat(900);
30
- const { results } = checkRules('x', long);
31
- assert.equal(results.find((r) => r.id === 'length-limit').pass, false);
32
- const { results: r2 } = checkRules('x', '要点如下:');
33
- assert.equal(r2.find((r) => r.id === 'length-limit').pass, false);
34
- });
35
-
36
- test('expand-not-answer fails on answer openers and clarifying questions', () => {
37
- for (const bad of ['好的,以下是实现方案:先装依赖再写组件。', "Here's how you can do it: install the deps first.", '你想用哪种框架?']) {
38
- const { results } = checkRules('做个网站', bad);
39
- assert.equal(results.find((r) => r.id === 'expand-not-answer').pass, false, bad);
40
- }
41
- });
42
-
43
- test('no-hallucinated-tech flags tech names absent from input', () => {
44
- const { results } = checkRules('做一个读书笔记应用', '用 React 和 PostgreSQL 构建笔记应用');
45
- assert.equal(results.find((r) => r.id === 'no-hallucinated-tech').pass, false);
46
- const ok = checkRules('用 React 做个笔记应用', '用 React 实现笔记的增删改查,数据存本地文件,支持全文检索');
47
- assert.equal(ok.results.find((r) => r.id === 'no-hallucinated-tech').pass, true);
48
- });
49
-
50
- test('japanese output fails for chinese input (kana detector)', () => {
51
- const { results } = checkRules('サイトを作って', '做一个展示宠物的网站:包含照片画廊。');
52
- assert.equal(results.find((r) => r.id === 'lang-consistency').pass, false);
53
- });
@@ -1,10 +0,0 @@
1
- {
2
- "name": "@prompt-contract/mcp-server",
3
- "version": "0.1.0",
4
- "description": "PromptContract MCP server — enhance_prompt tool + boost-* prompts, stdio JSON-RPC, zero dependencies",
5
- "type": "module",
6
- "license": "Apache-2.0",
7
- "bin": {
8
- "prompt-contract-mcp": "./bin/prompt-contract-mcp.js"
9
- }
10
- }
@@ -1,167 +0,0 @@
1
- import { test, before, after } from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import { spawn } from 'node:child_process';
4
- import { fileURLToPath } from 'node:url';
5
- import { dirname, join } from 'node:path';
6
- import { createMockServer, ZH_RESULT } from '../../../mock/server.js';
7
-
8
- let mock, base;
9
- const SERVER = join(dirname(fileURLToPath(import.meta.url)), '..', 'src', 'server.js');
10
-
11
- before(async () => {
12
- mock = createMockServer({});
13
- const port = await mock.listen();
14
- base = `http://127.0.0.1:${port}`;
15
- });
16
-
17
- after(async () => { await mock.close(); });
18
-
19
- function startServer() {
20
- const child = spawn(process.execPath, [SERVER], {
21
- env: { ...process.env, CONTRACT_PROVIDER: 'openai', CONTRACT_BASE_URL: `${base}/v1`, CONTRACT_API_KEY: 'test-key-123', CONTRACT_MODEL: 'mock-model' }
22
- });
23
- const pending = [];
24
- let buffer = '';
25
- const waiters = [];
26
- child.stdout.setEncoding('utf8');
27
- child.stdout.on('data', (d) => {
28
- buffer += d;
29
- let nl;
30
- while ((nl = buffer.indexOf('\n')) >= 0) {
31
- const line = buffer.slice(0, nl).trim();
32
- buffer = buffer.slice(nl + 1);
33
- if (!line) continue;
34
- const msg = JSON.parse(line);
35
- const w = waiters.shift();
36
- if (w) w(msg);
37
- else pending.push(msg);
38
- }
39
- });
40
- let stderr = '';
41
- child.stderr.on('data', (d) => { stderr += d; });
42
- const request = (obj) => new Promise((resolveReq) => {
43
- const waiter = (msg) => resolveReq(msg);
44
- if (pending.length) waiter(pending.shift()); else waiters.push(waiter);
45
- child.stdin.write(JSON.stringify(obj) + '\n');
46
- });
47
- const done = () => new Promise((r) => child.on('close', r));
48
- const kill = () => { child.stdin.end(); };
49
- return { child, request, done, kill, getStderr: () => stderr };
50
- }
51
-
52
- test('MCP: initialize → tools → tool call → prompts, full handshake', async () => {
53
- const s = startServer();
54
- try {
55
- const init = await s.request({ jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2025-06-18' } });
56
- assert.equal(init.result.serverInfo.name, 'prompt-contract');
57
- assert.ok(init.result.capabilities.tools);
58
- assert.ok(init.result.capabilities.prompts);
59
-
60
- const tools = await s.request({ jsonrpc: '2.0', id: 2, method: 'tools/list' });
61
- assert.equal(tools.result.tools.length, 1);
62
- assert.equal(tools.result.tools[0].name, 'enhance_prompt');
63
- assert.deepEqual(tools.result.tools[0].inputSchema.required, ['text']);
64
-
65
- const call = await s.request({
66
- jsonrpc: '2.0', id: 3, method: 'tools/call',
67
- params: { name: 'enhance_prompt', arguments: { text: '帮我做一个展示我家狗的网站' } }
68
- });
69
- assert.equal(call.result.isError, undefined);
70
- assert.equal(call.result.content[0].text, ZH_RESULT);
71
-
72
- const callProfiled = await s.request({
73
- jsonrpc: '2.0', id: 4, method: 'tools/call',
74
- params: { name: 'enhance_prompt', arguments: { text: 'A website for my dog', profile: 'writing', strength: 'polish' } }
75
- });
76
- assert.equal(callProfiled.result.isError, undefined);
77
- assert.ok(callProfiled.result.content[0].text.length > 0);
78
-
79
- const prompts = await s.request({ jsonrpc: '2.0', id: 5, method: 'prompts/list' });
80
- assert.deepEqual(prompts.result.prompts.map((p) => p.name), ['contract-coding-agent', 'contract-image-gen', 'contract-writing']);
81
-
82
- const get = await s.request({
83
- jsonrpc: '2.0', id: 6, method: 'prompts/get',
84
- params: { name: 'contract-writing', arguments: { text: '帮我写一封请假邮件' } }
85
- });
86
- const promptText = get.result.messages[0].content.text;
87
- assert.match(promptText, /USER INPUT:\s*\n+帮我写一封请假邮件/);
88
- assert.match(promptText, /HARD CONSTRAINTS:/);
89
-
90
- const unknown = await s.request({ jsonrpc: '2.0', id: 7, method: 'bogus/method' });
91
- assert.equal(unknown.error.code, -32601);
92
-
93
- const badTool = await s.request({
94
- jsonrpc: '2.0', id: 8, method: 'tools/call',
95
- params: { name: 'enhance_prompt', arguments: { text: '' } }
96
- });
97
- assert.equal(badTool.result.isError, true);
98
- assert.match(badTool.result.content[0].text, /empty_input/);
99
- } finally {
100
- s.kill();
101
- await s.done();
102
- }
103
- });
104
-
105
- test('MCP: notification messages get no response frame', async () => {
106
- const s = startServer();
107
- try {
108
- child_notify(s);
109
- const res = await s.request({ jsonrpc: '2.0', id: 100, method: 'ping' });
110
- assert.deepEqual(res.result, {});
111
- } finally {
112
- s.kill();
113
- await s.done();
114
- }
115
- });
116
-
117
- test('MCP: zero-key startup — prompts work, tool calls report config_error honestly (R12 caveat fixed)', async () => {
118
- // strip every config source so nothing can satisfy the tool mode
119
- const cleanEnv = { ...process.env };
120
- for (const k of Object.keys(cleanEnv)) if (k.startsWith('PB_')) delete cleanEnv[k];
121
- cleanEnv.CONTRACT_CONFIG = '/tmp/definitely-missing-contract-config.json';
122
- const child = spawn(process.execPath, [SERVER], { env: cleanEnv });
123
- const pending = [];
124
- let buffer = '';
125
- child.stdout.setEncoding('utf8');
126
- child.stdout.on('data', (d) => {
127
- buffer += d;
128
- let nl;
129
- while ((nl = buffer.indexOf('\n')) >= 0) {
130
- const line = buffer.slice(0, nl).trim();
131
- buffer = buffer.slice(nl + 1);
132
- if (line) pending.push(JSON.parse(line));
133
- }
134
- });
135
- const request = (obj) => new Promise((resolveReq) => {
136
- const check = () => {
137
- if (pending.length) resolveReq(pending.shift());
138
- else setTimeout(check, 20);
139
- };
140
- child.stdin.write(JSON.stringify(obj) + '\n');
141
- check();
142
- });
143
- try {
144
- const init = await request({ jsonrpc: '2.0', id: 1, method: 'initialize', params: {} });
145
- assert.equal(init.result.serverInfo.name, 'prompt-contract');
146
-
147
- const get = await request({
148
- jsonrpc: '2.0', id: 2, method: 'prompts/get',
149
- params: { name: 'contract-coding-agent', arguments: { text: '做个网站' } }
150
- });
151
- assert.match(get.result.messages[0].content.text, /USER INPUT:\s*\n+做个网站/);
152
-
153
- const call = await request({
154
- jsonrpc: '2.0', id: 3, method: 'tools/call',
155
- params: { name: 'enhance_prompt', arguments: { text: '做个网站' } }
156
- });
157
- assert.equal(call.result.isError, true);
158
- assert.match(call.result.content[0].text, /config_error/);
159
- } finally {
160
- child.stdin.end();
161
- await new Promise((r) => child.on('close', r));
162
- }
163
- });
164
-
165
- function child_notify(s) {
166
- s.child.stdin.write(JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' }) + '\n');
167
- }
@@ -1,7 +0,0 @@
1
- {
2
- "name": "@prompt-contract/providers",
3
- "version": "0.1.0",
4
- "description": "Provider adapters — OpenAI-compatible SSE and Ollama (with keep_alive model pinning). Fetch-based, browser-safe.",
5
- "type": "module",
6
- "license": "Apache-2.0"
7
- }
@@ -1,64 +0,0 @@
1
- import { test, before, after } from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import { createMockServer, EN_RESULT, ZH_RESULT } from '../../../mock/server.js';
4
- import { createOpenAIProvider } from '../src/openai.js';
5
- import { createOllamaProvider } from '../src/ollama.js';
6
- import { PromptContractError } from '../../core/src/errors.js';
7
-
8
- let mock, base;
9
- before(async () => {
10
- mock = createMockServer({});
11
- const port = await mock.listen();
12
- base = `http://127.0.0.1:${port}`;
13
- });
14
- after(async () => { await mock.close(); });
15
-
16
- test('openai provider reassembles SSE chunks in order', async () => {
17
- const p = createOpenAIProvider({ baseUrl: `${base}/v1`, apiKey: 'test-key-123' });
18
- let deltas = 0;
19
- const { text } = await p.complete({ system: 's', user: 'USER INPUT:\n帮我做一个展示我家狗的网站', model: 'mock-model', onDelta: () => deltas++ });
20
- assert.equal(text, ZH_RESULT);
21
- assert.ok(deltas > 3, `expected multiple deltas, got ${deltas}`);
22
- });
23
-
24
- test('openai provider handles english input', async () => {
25
- const p = createOpenAIProvider({ baseUrl: `${base}/v1`, apiKey: 'test-key-123' });
26
- const { text } = await p.complete({ system: 's', user: 'USER INPUT:\nA website for my dog', model: 'mock-model' });
27
- assert.equal(text, EN_RESULT);
28
- });
29
-
30
- test('openai provider maps non-200 to provider_unavailable', async () => {
31
- const p = createOpenAIProvider({ baseUrl: base, apiKey: 'test-key-123' }); // /chat/completions → 404
32
- await assert.rejects(
33
- () => p.complete({ system: 's', user: 'u', model: 'm' }),
34
- (e) => e instanceof PromptContractError && e.code === 'provider_unavailable'
35
- );
36
- });
37
-
38
- test('openai provider: caller abort cancels the stream (ADR-017)', async () => {
39
- const p = createOpenAIProvider({ baseUrl: `${base}/v1`, apiKey: 'test-key-123' });
40
- const ctrl = new AbortController();
41
- const promise = p.complete({
42
- system: 's', user: 'USER INPUT:\nA website for my dog', model: 'm',
43
- signal: ctrl.signal,
44
- onDelta: () => ctrl.abort()
45
- });
46
- await assert.rejects(promise, (e) => e.name === 'AbortError');
47
- await new Promise((r) => setTimeout(r, 50));
48
- assert.ok(mock.state.aborts >= 1, 'mock should have observed the aborted connection');
49
- });
50
-
51
- test('ollama provider sends keep_alive so the model stays warm (R10)', async () => {
52
- const p = createOllamaProvider({ baseUrl: base, keepAlive: '60m' });
53
- const { text } = await p.complete({ system: 's', user: 'USER INPUT:\n帮我做一个展示我家狗的网站', model: 'mock-model', maxTokens: 960 });
54
- assert.equal(text, ZH_RESULT);
55
- assert.equal(mock.state.lastChatBody.keep_alive, '60m');
56
- assert.equal(mock.state.lastChatBody.options.num_predict, 960);
57
- });
58
-
59
- test('ollama warmup preloads the model via /api/generate (R10)', async () => {
60
- const p = createOllamaProvider({ baseUrl: base, keepAlive: '30m' });
61
- await p.warmup({ model: 'mock-model' });
62
- assert.equal(mock.state.lastGenerateBody.model, 'mock-model');
63
- assert.equal(mock.state.lastGenerateBody.keep_alive, '30m');
64
- });