prompt-contract 0.2.0 → 0.2.1

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/README.md CHANGED
@@ -7,8 +7,7 @@
7
7
 
8
8
  English · [简体中文](README.zh-CN.md)
9
9
 
10
- ![PromptContract playground running the self-contained demo: a vague one-line request enhanced into a structured task spec, with six rule assertions passing](docs/assets/hero-playground.png)
11
- <sub>The playground running its self-contained demo (`?demo=1`) against the local mock upstream — actual pipeline output, six rule assertions passing.</sub>
10
+ ![PromptContract a vague one-line request becomes a structured task spec (goal, scope, acceptance criteria, non-goals), verified by six hard rules via `prompt-contract check`. Real-LLM latency P50 2.3s; engine adds 0.005ms. CLI · MCP · Playground — zero dependencies, BYOK, offline-capable.](docs/assets/hero-cover.png)
12
11
 
13
12
  **PromptContract is a deterministic prompt-contract layer for AI coding agents.** It compiles a vague one-line request into a structured task specification — goal, scope, constraints, acceptance criteria — and verifies the result against six hard rules before you ever see it.
14
13
 
@@ -17,7 +16,7 @@ It is *not* a smarter brain: your model does the thinking, PromptContract makes
17
16
  ## Why PromptContract
18
17
 
19
18
  - **Stable agent inputs** — a profile plus hard constraints turn "a website for my dog" into goal / scope / acceptance criteria / explicit non-goals. Reduced retries and scope drift are hypotheses measured by the task-level harness, not current product claims.
20
- - **Six deterministic guardrails** — language consistency, enhanced-text-only, length & completeness, expand-don't-answer, no hallucinated tech. Every enhancement can be asserted with `contract check`; the same spec drives templates and tests.
19
+ - **Six deterministic guardrails** — language consistency, enhanced-text-only, length & completeness, expand-don't-answer, no hallucinated tech. Every enhancement can be asserted with `prompt-contract check`; the same spec drives templates and tests.
21
20
  - **One engine, three surfaces** — a CLI, an MCP server (agent-invoked **tool** + user-invoked **slash prompts**), and a browser playground. All share one zero-dependency core.
22
21
  - **Private by architecture** — bring your own key, no server in the middle, no telemetry, offline-capable via Ollama.
23
22
 
@@ -37,10 +36,10 @@ Install: `npm i -g prompt-contract` — or zero-install: `npx prompt-contract "y
37
36
 
38
37
  ```bash
39
38
  export CONTRACT_API_KEY=sk-xxx CONTRACT_MODEL=gpt-4o-mini # any OpenAI-compatible endpoint (DeepSeek, Qwen, GLM, vLLM…)
40
- contract "A website for my dog"
39
+ prompt-contract "A website for my dog"
41
40
 
42
41
  export CONTRACT_PROVIDER=ollama CONTRACT_MODEL=qwen3:4b # fully local/offline; keep_alive pins the model in RAM
43
- contract "帮我写一封请假邮件"
42
+ prompt-contract "帮我写一封请假邮件"
44
43
  ```
45
44
 
46
45
  Or write `~/.prompt-contract/config.json` once: `{ "provider": "openai", "baseUrl": "…", "apiKey": "…", "model": "…" }`.
@@ -88,8 +87,8 @@ raw input → script/scenario detect → profile + hard constraints + strength (
88
87
 
89
88
  ## Status & roadmap — stated honestly
90
89
 
91
- - **Shipped:** engine, CLI (`contract` / `check` / `doctor` / `profiles` / `spike-0`), MCP server (tool + zero-key prompts), playground, 3 profiles, eval cases, CI matrix.
92
- - **Gated:** `contract watch` (global-hotkey resident mode) remains intentionally unavailable. `contract spike-0` measures macOS capture/clipboard safety and dry-run focus eligibility, but never pastes or unlocks watch by itself; see [docs/SPIKE-0.md](docs/SPIKE-0.md).
90
+ - **Shipped:** engine, CLI (`prompt-contract` / `check` / `doctor` / `profiles` / `spike-0`), MCP server (tool + zero-key prompts), playground, 3 profiles, eval cases, CI matrix.
91
+ - **Gated:** `prompt-contract watch` (global-hotkey resident mode) remains intentionally unavailable. `prompt-contract spike-0` measures macOS capture/clipboard safety and dry-run focus eligibility, but never pastes or unlocks watch by itself; see [docs/SPIKE-0.md](docs/SPIKE-0.md).
93
92
  - **Open validation:** task-level outcome evaluation remains an evidence-gathering task. The harness and curated fixture set exist, but PromptContract's downstream effectiveness is still a hypothesis until a declared runner produces reviewed results.
94
93
  - Deferred: animated demo asset, IDE plugins, LLM-as-judge as *one* scorer inside the task-level eval.
95
94
 
package/README.zh-CN.md CHANGED
@@ -6,16 +6,16 @@
6
6
 
7
7
  [English](README.md) · 简体中文
8
8
 
9
+ ![PromptContract——模糊的一句话被编译成结构化任务规范(目标 / 范围 / 验收标准 / 明确不做的事),六条硬规则经 `contract check` 校验。真实 LLM 端到端 P50 2.3s,引擎附加开销 0.005ms。CLI · MCP · Playground——零依赖、BYOK、可离线。](docs/assets/hero-cover.png)
10
+
9
11
  **PromptContract 是面向 AI 编码 agent 的确定性 prompt 契约层**:把模糊的一句话编译成结构化任务规范(目标 / 范围 / 约束 / 验收标准),并在你看到结果之前用六条硬规则完成校验。
10
12
 
11
13
  它不是「更聪明的大脑」:思考由你的模型完成,PromptContract 让 agent 的输入变得稳定、可审查、可跨模型与跨工具迁移。
12
14
 
13
- ![Playground 自运行演示:模糊一句话被增强为结构化任务规范,六条规则断言全部通过](docs/assets/hero-playground.png)
14
-
15
15
  ## 为什么
16
16
 
17
17
  - **稳定的 agent 输入**——profile + 硬约束把「帮我做个网站」编译成目标 / 范围 / 验收标准 / 明确不做的事;重试与范围漂移是否减少,仍须由任务级 harness 验证,当前不作产品结论
18
- - **六条确定性护栏**——语言一致性、只输出增强文本、长度与完整性、扩写而非回答、无幻觉技术栈;`contract check` 随时可断言,模板与测试共用同一份规格
18
+ - **六条确定性护栏**——语言一致性、只输出增强文本、长度与完整性、扩写而非回答、无幻觉技术栈;`prompt-contract check` 随时可断言,模板与测试共用同一份规格
19
19
  - **一个引擎、三个形态**——CLI、MCP server(agent 调用的 tool + 用户调用的斜杠 prompts)、浏览器 Playground,共享同一个零依赖内核
20
20
  - **隐私即架构**——BYOK、无中间服务、零遥测,Ollama 全本地可用
21
21
 
@@ -35,10 +35,10 @@ node packages/playground/serve.js # → http://127.0.0.1:8
35
35
 
36
36
  ```bash
37
37
  export CONTRACT_API_KEY=sk-xxx CONTRACT_MODEL=gpt-4o-mini # 任意 OpenAI 兼容端点(DeepSeek/Qwen/GLM/vLLM…)
38
- contract "A website for my dog"
38
+ prompt-contract "A website for my dog"
39
39
 
40
40
  export CONTRACT_PROVIDER=ollama CONTRACT_MODEL=qwen3:4b # 全本地;keep_alive 把模型钉在内存
41
- contract "帮我写一封请假邮件"
41
+ prompt-contract "帮我写一封请假邮件"
42
42
  ```
43
43
 
44
44
  或一次写入 `~/.prompt-contract/config.json`。
@@ -64,7 +64,7 @@ npm run bench # 引擎自身开销 P50 ≈ 0.005ms(预算 <5ms)
64
64
  ## 状态与路线图(诚实版)
65
65
 
66
66
  - **已交付**:引擎、CLI(含 `spike-0`)、MCP server(tool + 零 key prompts)、Playground、3 个 profiles、eval 用例、CI 矩阵
67
- - **被门控**:`contract watch`(全局热键常驻)仍不可用。`contract spike-0` 只测 macOS 取词、剪贴板恢复和焦点校验的 dry-run,不发送粘贴,也不会自行解锁 watch;见 [docs/SPIKE-0.md](docs/SPIKE-0.md)
67
+ - **被门控**:`prompt-contract watch`(全局热键常驻)仍不可用。`prompt-contract spike-0` 只测 macOS 取词、剪贴板恢复和焦点校验的 dry-run,不发送粘贴,也不会自行解锁 watch;见 [docs/SPIKE-0.md](docs/SPIKE-0.md)
68
68
  - **开放验证**:任务级效果评测的 harness 与任务 fixture 已交付,但还没有声明 runner 产生并复核结果;长期价值在此之前仍是假设
69
69
  - **推迟**:动画 demo 资产、IDE 插件、LLM-as-judge(作为任务级评测中的评分器之一)
70
70
 
package/package.json CHANGED
@@ -1,21 +1,22 @@
1
1
  {
2
2
  "name": "prompt-contract",
3
- "version": "0.2.0",
4
- "description": "pc — one-key prompt enhancement: turn vague ideas into structured task specs anywhere you type to an AI. CLI + MCP server. Zero dependencies, BYOK, offline-capable (Ollama).",
3
+ "version": "0.2.1",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "description": "One-key prompt enhancement — turn vague ideas into structured task specs anywhere you type to an AI. CLI + MCP server. Zero dependencies, BYOK, offline-capable (Ollama).",
5
8
  "license": "Apache-2.0",
6
9
  "type": "module",
7
10
  "engines": {
8
11
  "node": ">=20"
9
12
  },
10
13
  "bin": {
11
- "contract": "./packages/cli/bin/contract.js",
12
14
  "prompt-contract": "./packages/cli/bin/contract.js",
13
- "prompt-contract-mcp": "./packages/mcp-server/bin/prompt-contract-mcp.js",
14
- "pb": "./packages/cli/bin/contract.js"
15
+ "contract": "./packages/cli/bin/contract.js",
16
+ "prompt-contract-mcp": "./packages/mcp-server/bin/prompt-contract-mcp.js"
15
17
  },
16
18
  "keywords": [
17
19
  "prompt",
18
- "prompt-contract",
19
20
  "prompt-engineering",
20
21
  "llm",
21
22
  "cli",
@@ -36,7 +37,7 @@
36
37
  "packages/",
37
38
  "profiles/",
38
39
  "README.md",
39
- "README.en.md",
40
+ "README.zh-CN.md",
40
41
  "LICENSE"
41
42
  ]
42
43
  }
@@ -24,18 +24,18 @@ if (basename(process.argv[1] || '') === 'contract') {
24
24
  process.stderr.write('[deprecated] this CLI is now `contract` (prompt-contract); the `contract` command will be removed in a future release.\n');
25
25
  }
26
26
 
27
- const VERSION = '0.2.0';
28
- const USAGE = `contract — one-key prompt enhancement (PromptContract v${VERSION})
27
+ const VERSION = '0.2.1';
28
+ const USAGE = `prompt-contract — one-key prompt enhancement (PromptContract v${VERSION})
29
29
 
30
30
  Usage:
31
- contract "build me a website for my dog" enhance a prompt (prints enhanced text to stdout)
32
- cat prompt.txt | contract enhance from stdin
33
- contract profiles list built-in profiles
34
- contract check --original "..." --enhanced "..."
31
+ prompt-contract "build me a website for my dog" enhance a prompt (prints enhanced text to stdout)
32
+ cat prompt.txt | prompt-contract enhance from stdin
33
+ prompt-contract profiles list built-in profiles
34
+ prompt-contract check --original "..." --enhanced "..."
35
35
  run the six hard-constraint rule assertions
36
- contract doctor verify config, provider reachability, profiles
37
- contract spike-0 macOS-only capture/restore compatibility diagnostic (dry-run)
38
- contract watch NOT BUILT — gated by decision D7 (Spike-0 first); see docs/SPIKE-0.md
36
+ prompt-contract doctor verify config, provider reachability, profiles
37
+ prompt-contract spike-0 macOS-only capture/restore compatibility diagnostic (dry-run)
38
+ prompt-contract watch NOT BUILT — gated by decision D7 (Spike-0 first); see docs/SPIKE-0.md
39
39
 
40
40
  Options:
41
41
  -p, --profile <name> scenario profile (default: coding-agent)
@@ -1,89 +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, repoRoot;
9
- const PB = join(dirname(fileURLToPath(import.meta.url)), '..', 'bin', 'contract.js');
10
-
11
- before(async () => {
12
- mock = createMockServer({});
13
- const port = await mock.listen();
14
- base = `http://127.0.0.1:${port}`;
15
- repoRoot = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
16
- });
17
-
18
- after(async () => { await mock.close(); });
19
-
20
- function runPb(args, { env = {}, input } = {}) {
21
- return new Promise((resolveRun) => {
22
- const child = spawn(process.execPath, [PB, ...args], {
23
- cwd: repoRoot,
24
- env: { ...process.env, ...env }
25
- });
26
- let stdout = '', stderr = '';
27
- child.stdout.on('data', (d) => { stdout += d; });
28
- child.stderr.on('data', (d) => { stderr += d; });
29
- if (input !== undefined) child.stdin.end(input); else child.stdin.end();
30
- child.on('close', (code) => resolveRun({ code, stdout, stderr }));
31
- });
32
- }
33
-
34
- const OPENAI_ENV = { CONTRACT_PROVIDER: 'openai', CONTRACT_BASE_URL: `${base}/v1`, CONTRACT_API_KEY: 'test-key-123', CONTRACT_MODEL: 'mock-model' };
35
-
36
- test('e2e: contract "..." enhances via openai-compatible upstream and passes rule assertions', async () => {
37
- const { code, stdout, stderr } = await runPb(['--json', '--provider', 'openai', '--base-url', `${base}/v1`, '--api-key', 'test-key-123', '--model', 'mock-model', '帮我做一个展示我家狗的网站']);
38
- assert.equal(code, 0, stderr);
39
- const out = JSON.parse(stdout);
40
- assert.equal(out.original, '帮我做一个展示我家狗的网站');
41
- assert.equal(out.enhanced, ZH_RESULT);
42
- assert.equal(out.meta.profile, 'coding-agent');
43
- assert.equal(out.meta.model, 'mock-model');
44
- assert.equal(out.rules.pass, true, JSON.stringify(out.rules.results));
45
- });
46
-
47
- test('e2e: contract reads prompt from stdin (pipe mode)', async () => {
48
- const { code, stdout } = await runPb(
49
- ['--no-stream', '--provider', 'openai', '--base-url', `${base}/v1`, '--api-key', 'test-key-123', '--model', 'mock-model'],
50
- { input: '帮我做一个展示我家狗的网站' }
51
- );
52
- assert.equal(code, 0);
53
- assert.equal(stdout.trim(), ZH_RESULT);
54
- });
55
-
56
- test('contract profiles lists the three built-in profiles', async () => {
57
- const { code, stdout } = await runPb(['profiles']);
58
- assert.equal(code, 0);
59
- for (const name of ['coding-agent', 'writing', 'image-gen']) assert.match(stdout, new RegExp(name));
60
- });
61
-
62
- test('contract check exits 1 on a failing pair (gate mode) and 0 on a good pair', async () => {
63
- const bad = await runPb(['check', '--original', '做个博客', '--enhanced', '好的,以下是实现方案:先安装依赖。']);
64
- assert.equal(bad.code, 1);
65
- assert.match(bad.stderr, /FAIL/);
66
- const good = await runPb(['check', '--original', '帮我做一个展示我家狗的网站', '--enhanced', ZH_RESULT]);
67
- assert.equal(good.code, 0, good.stderr);
68
- });
69
-
70
- test('contract watch is gated by decision D7', async () => {
71
- const { code, stderr } = await runPb(['watch']);
72
- assert.equal(code, 2);
73
- assert.match(stderr, /D7/);
74
- });
75
-
76
- test('contract spike-0 exposes the macOS dry-run diagnostic', async () => {
77
- const { code, stdout } = await runPb(['spike-0', '--help']);
78
- assert.equal(code, 0);
79
- assert.match(stdout, /dry-run/);
80
- assert.match(stdout, /Chrome/);
81
- assert.match(stdout, /PyCharm/);
82
- assert.match(stdout, /iTerm/);
83
- });
84
-
85
- test('contract doctor reports provider problems honestly', async () => {
86
- const { code, stderr } = await runPb(['doctor', '--provider', 'openai', '--base-url', `${base}/v1`, '--api-key', 'wrong-key', '--model', 'mock-model']);
87
- assert.equal(code, 1);
88
- assert.match(stderr, /FAIL/);
89
- });
@@ -1,260 +0,0 @@
1
- import { test } from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import {
4
- captureSelectedText,
5
- validatePasteBackDryRun,
6
- buildCompatibilityReport,
7
- DEFAULT_SPIKE_THRESHOLDS,
8
- isPlainTextClipboardInfo,
9
- runSpike0,
10
- } from '../src/spike-0.js';
11
-
12
- function context(overrides = {}) {
13
- return {
14
- processName: 'Google Chrome',
15
- bundleId: 'com.google.Chrome',
16
- pid: 123,
17
- windowTitle: 'Prompt test',
18
- focus: {
19
- role: 'AXTextField',
20
- subrole: 'AXStandardWindow',
21
- identifier: 'prompt-input',
22
- title: '',
23
- description: 'Prompt input',
24
- },
25
- ...overrides,
26
- };
27
- }
28
-
29
- function fakeAdapter({
30
- clipboard = 'keep this clipboard',
31
- selectedText = 'selected prompt',
32
- contexts = [context(), context(), context()],
33
- copyError,
34
- clipboardCheckError,
35
- } = {}) {
36
- const state = { clipboard, events: [], contexts: [...contexts] };
37
- return {
38
- state,
39
- async readClipboard() {
40
- state.events.push('readClipboard');
41
- return state.clipboard;
42
- },
43
- async checkClipboardRestorable() {
44
- state.events.push('checkClipboardRestorable');
45
- if (clipboardCheckError) throw clipboardCheckError;
46
- },
47
- async writeClipboard(value) {
48
- state.events.push(['writeClipboard', value]);
49
- state.clipboard = value;
50
- },
51
- async copySelection() {
52
- state.events.push('copySelection');
53
- if (copyError) throw copyError;
54
- state.clipboard = selectedText;
55
- },
56
- async getFocusIdentity() {
57
- state.events.push('getFocusIdentity');
58
- return state.contexts.shift() || context();
59
- },
60
- async sleep() {
61
- state.events.push('sleep');
62
- },
63
- };
64
- }
65
-
66
- test('captureSelectedText restores the original clipboard after a successful copy', async () => {
67
- const adapter = fakeAdapter();
68
-
69
- const result = await captureSelectedText(adapter, { settleMs: 0 });
70
-
71
- assert.equal(result.selectedText, 'selected prompt');
72
- assert.equal(result.selectedTextLength, 15);
73
- assert.equal(result.clipboardRestored, true);
74
- assert.equal(result.userTextMutated, false);
75
- assert.equal(adapter.state.clipboard, 'keep this clipboard');
76
- assert.deepEqual(adapter.state.events, [
77
- 'readClipboard',
78
- 'checkClipboardRestorable',
79
- 'getFocusIdentity',
80
- 'copySelection',
81
- 'sleep',
82
- 'readClipboard',
83
- 'getFocusIdentity',
84
- ['writeClipboard', 'keep this clipboard'],
85
- 'readClipboard',
86
- ]);
87
- });
88
-
89
- test('captureSelectedText restores the clipboard when copy fails', async () => {
90
- const adapter = fakeAdapter({ copyError: new Error('Accessibility denied') });
91
-
92
- const result = await captureSelectedText(adapter, { settleMs: 0 });
93
-
94
- assert.equal(result.selectedText, null);
95
- assert.equal(result.clipboardRestored, true);
96
- assert.equal(result.userTextMutated, false);
97
- assert.match(result.error, /Accessibility denied/);
98
- assert.equal(adapter.state.clipboard, 'keep this clipboard');
99
- assert.equal(adapter.state.events.some((event) => Array.isArray(event) && event[0] === 'writeClipboard'), true);
100
- });
101
-
102
- test('captureSelectedText refuses an unsupported clipboard without rewriting it', async () => {
103
- const adapter = fakeAdapter({ clipboardCheckError: new Error('clipboard_not_plain_text') });
104
-
105
- const result = await captureSelectedText(adapter, { settleMs: 0 });
106
-
107
- assert.equal(result.selectedText, null);
108
- assert.match(result.error, /clipboard_not_plain_text/);
109
- assert.equal(result.clipboardRestored, false);
110
- assert.equal(result.clipboardUntouched, true);
111
- assert.equal(adapter.state.clipboard, 'keep this clipboard');
112
- assert.equal(adapter.state.events.includes('copySelection'), false);
113
- assert.equal(adapter.state.events.some((event) => Array.isArray(event) && event[0] === 'writeClipboard'), false);
114
- });
115
-
116
- test('isPlainTextClipboardInfo accepts text-only pasteboards and rejects rich types', () => {
117
- assert.equal(
118
- isPlainTextClipboardInfo('«class utf8», 0, «class ut16», 2, string, 0, Unicode text, 0'),
119
- true,
120
- );
121
- assert.equal(
122
- isPlainTextClipboardInfo('«class utf8», 4, «class HTML», 128'),
123
- false,
124
- );
125
- });
126
-
127
- test('runSpike0 supports a setup delay before each target without changing capture semantics', async () => {
128
- const adapter = fakeAdapter();
129
- const announcements = [];
130
-
131
- await runSpike0({
132
- adapter,
133
- targets: ['Chrome'],
134
- iterations: 1,
135
- setupDelayMs: 25,
136
- interactive: false,
137
- announce: (message) => announcements.push(message),
138
- now: () => new Date('2026-09-07T00:00:00.000Z'),
139
- });
140
-
141
- assert.deepEqual(announcements, ['Focus Chrome and select text now; capture starts after the setup delay.']);
142
- assert.equal(adapter.state.events[0], 'sleep');
143
- assert.equal(adapter.state.events.includes('copySelection'), true);
144
- });
145
-
146
- test('validatePasteBackDryRun checks focus stability without issuing paste', async () => {
147
- const adapter = fakeAdapter();
148
- const before = context();
149
-
150
- const result = await validatePasteBackDryRun(adapter, {
151
- capturedContext: before,
152
- selectedText: 'selected prompt',
153
- pauseMs: 0,
154
- });
155
-
156
- assert.equal(result.mode, 'dry-run');
157
- assert.equal(result.executed, false);
158
- assert.equal(result.wouldPasteBack, true);
159
- assert.equal(result.userTextMutated, false);
160
- assert.equal(adapter.state.events.includes('pasteSelection'), false);
161
- });
162
-
163
- test('validatePasteBackDryRun rejects focus drift without changing user text', async () => {
164
- const adapter = fakeAdapter({ contexts: [context({ pid: 456, windowTitle: 'Other app' })] });
165
-
166
- const result = await validatePasteBackDryRun(adapter, {
167
- capturedContext: context(),
168
- selectedText: 'selected prompt',
169
- pauseMs: 0,
170
- });
171
-
172
- assert.equal(result.wouldPasteBack, false);
173
- assert.equal(result.reason, 'focus_drift');
174
- assert.equal(result.executed, false);
175
- assert.equal(result.userTextMutated, false);
176
- });
177
-
178
- test('validatePasteBackDryRun tolerates a transiently unavailable window title', async () => {
179
- const adapter = fakeAdapter({ contexts: [context({ windowTitle: '' })] });
180
-
181
- const result = await validatePasteBackDryRun(adapter, {
182
- capturedContext: context(),
183
- selectedText: 'selected prompt',
184
- pauseMs: 0,
185
- });
186
-
187
- assert.equal(result.wouldPasteBack, true);
188
- assert.equal(result.focusStable, true);
189
- assert.equal(result.executed, false);
190
- });
191
-
192
- test('buildCompatibilityReport applies the Spike-0 cohort thresholds', () => {
193
- const runs = [];
194
- for (const target of ['Chrome', 'PyCharm', 'iTerm']) {
195
- for (let iteration = 1; iteration <= 20; iteration++) {
196
- runs.push({
197
- target,
198
- iteration,
199
- capture: {
200
- selectedTextCaptured: true,
201
- clipboardRestored: true,
202
- clipboardRestoreVerified: true,
203
- focusRecorded: true,
204
- },
205
- pasteBack: { mode: 'dry-run', wouldPasteBack: true, contextAtValidation: {} },
206
- safety: { userTextMutated: false, pasteCommandSent: false },
207
- });
208
- }
209
- }
210
-
211
- const report = buildCompatibilityReport({
212
- runs,
213
- targets: ['Chrome', 'PyCharm', 'iTerm'],
214
- thresholds: DEFAULT_SPIKE_THRESHOLDS,
215
- platformInfo: { os: 'darwin', arch: 'arm64' },
216
- startedAt: '2026-09-07T00:00:00.000Z',
217
- finishedAt: '2026-09-07T00:00:01.000Z',
218
- });
219
-
220
- assert.equal(report.schemaVersion, 'prompt-contract/spike-0.v1');
221
- assert.equal(report.mode, 'dry-run');
222
- assert.equal(report.summary.captureSuccessRate, 1);
223
- assert.equal(report.summary.clipboardRestoreSuccessRate, 1);
224
- assert.equal(report.decision.pass, true);
225
- assert.equal(report.decision.watchGate, 'closed');
226
- });
227
-
228
- test('buildCompatibilityReport fails below the combined capture threshold', () => {
229
- const runs = [];
230
- for (const target of ['Chrome', 'PyCharm', 'iTerm']) {
231
- for (let iteration = 1; iteration <= 20; iteration++) {
232
- const failed = target === 'iTerm' && iteration <= 7;
233
- runs.push({
234
- target,
235
- iteration,
236
- capture: {
237
- selectedTextCaptured: !failed,
238
- clipboardRestored: true,
239
- clipboardRestoreVerified: true,
240
- focusRecorded: true,
241
- },
242
- pasteBack: { mode: 'dry-run', wouldPasteBack: !failed, contextAtValidation: {} },
243
- safety: { userTextMutated: false, pasteCommandSent: false },
244
- });
245
- }
246
- }
247
-
248
- const report = buildCompatibilityReport({
249
- runs,
250
- targets: ['Chrome', 'PyCharm', 'iTerm'],
251
- thresholds: DEFAULT_SPIKE_THRESHOLDS,
252
- platformInfo: { os: 'darwin', arch: 'arm64' },
253
- startedAt: '2026-09-07T00:00:00.000Z',
254
- finishedAt: '2026-09-07T00:00:01.000Z',
255
- });
256
-
257
- assert.equal(report.summary.captureSuccessRate, 0.8833);
258
- assert.equal(report.decision.pass, false);
259
- assert.match(report.decision.reasons.join(' '), /dry-run paste-back/);
260
- });
@@ -1,48 +0,0 @@
1
- /**
2
- * Engine overhead benchmark — the part of the latency budget we own (PRD §5.3/§7.6).
3
- * The model TTFT is external and dominates end-to-end latency; this proves the engine adds
4
- * effectively nothing on top (budget: P50 < 5ms, in practice microseconds).
5
- */
6
- import { assembleMessages } from '../src/pipeline.js';
7
- import { postprocess } from '../src/clean.js';
8
- import { checkRules } from '../src/rules.js';
9
-
10
- const PROFILE = { name: 'coding-agent', maxChars: 800, body: 'You rewrite vague requests for a coding assistant. '.repeat(4) };
11
- const INPUTS = [
12
- '帮我做一个展示我家狗的网站',
13
- 'A website for my dog',
14
- 'explain this code',
15
- '把这份周报改正式一点',
16
- '一只在雪地里的柴犬'
17
- ];
18
- const OUTPUT = '做一个展示宠物的小型网站:包含照片画廊、简介页与动态页,导航保持单层,暂不需要评论功能。';
19
-
20
- const N = 20000;
21
- const times = [];
22
- // warmup
23
- for (let i = 0; i < 500; i++) {
24
- const { system, user } = assembleMessages(INPUTS[i % INPUTS.length], { profile: PROFILE, strength: 'standard' });
25
- postprocess(system + user, 800);
26
- }
27
- for (let i = 0; i < N; i++) {
28
- const t0 = performance.now();
29
- const { system, user } = assembleMessages(INPUTS[i % INPUTS.length], { profile: PROFILE, strength: 'standard' });
30
- const cleaned = postprocess(`“${OUTPUT}”`, 800);
31
- checkRules(INPUTS[i % INPUTS.length], cleaned, { maxChars: 800 });
32
- if (system.length === 0 || user.length === 0) throw new Error('assembly broke');
33
- times.push(performance.now() - t0);
34
- }
35
- times.sort((a, b) => a - b);
36
- const p50 = times[Math.floor(N * 0.5)];
37
- const p95 = times[Math.floor(N * 0.95)];
38
- const mean = times.reduce((a, b) => a + b, 0) / N;
39
-
40
- console.log(`engine overhead per enhancement (assemble + clean + rules), n=${N}`);
41
- console.log(` P50: ${p50.toFixed(3)}ms`);
42
- console.log(` P95: ${p95.toFixed(3)}ms`);
43
- console.log(` mean: ${mean.toFixed(3)}ms`);
44
- if (p50 >= 5) {
45
- console.error('BUDGET VIOLATION: P50 must stay under 5ms');
46
- process.exit(1);
47
- }
48
- console.log('budget check: PASS (P50 < 5ms)');
@@ -1,11 +0,0 @@
1
- {
2
- "name": "@prompt-contract/core",
3
- "version": "0.1.0",
4
- "description": "PromptContract engine — profile assembly, single-shot LLM call contract, deterministic cleaning and rule assertions. Zero runtime dependencies, browser-safe.",
5
- "type": "module",
6
- "license": "Apache-2.0",
7
- "exports": {
8
- ".": "./src/index.js",
9
- "./node": "./src/node.js"
10
- }
11
- }
@@ -1,44 +0,0 @@
1
- import { test } from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import { stripWrappingQuotes, stripFences, clampChars, postprocess } from '../src/clean.js';
4
-
5
- test('stripWrappingQuotes removes paired quotes repeatedly', () => {
6
- assert.equal(stripWrappingQuotes('"hello"'), 'hello');
7
- assert.equal(stripWrappingQuotes('“你好世界”'), '你好世界');
8
- assert.equal(stripWrappingQuotes('‘“嵌套”’'), '嵌套');
9
- assert.equal(stripWrappingQuotes('「block」'), 'block');
10
- });
11
-
12
- test('stripWrappingQuotes keeps inner apostrophes', () => {
13
- assert.equal(stripWrappingQuotes("it's ok"), "it's ok");
14
- });
15
-
16
- test('stripFences removes full and partial fences', () => {
17
- assert.equal(stripFences('```\ntext\n```'), 'text');
18
- assert.equal(stripFences('```md\n# title\n```'), '# title');
19
- assert.equal(stripFences('```\nno closing'), 'no closing');
20
- assert.equal(stripFences('plain'), 'plain');
21
- });
22
-
23
- test('clampChars cuts at sentence boundary under the limit', () => {
24
- const t = '第一句。第二句。' + '长'.repeat(900);
25
- const out = clampChars(t, 800);
26
- assert.ok([...out].length <= 800);
27
- assert.ok(out.endsWith('。') || out.endsWith('长'));
28
- });
29
-
30
- test('clampChars removes dangling colon and list markers', () => {
31
- const t = '要点如下:' + 'x'.repeat(798);
32
- const out = clampChars(t, 800);
33
- assert.ok(!/[::]\s*$/.test(out));
34
- assert.ok(!/[-*+]\s*$/.test(out));
35
- });
36
-
37
- test('clampChars leaves short text untouched', () => {
38
- assert.equal(clampChars('短文本', 800), '短文本');
39
- });
40
-
41
- test('postprocess: null for empty, strips combo of fences + quotes', () => {
42
- assert.equal(postprocess(' \n\t', 800), null);
43
- assert.equal(postprocess('```\n“最终文本”\n```', 800), '最终文本');
44
- });
@@ -1,21 +0,0 @@
1
- import { test } from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import { detectScriptName } from '../src/lang.js';
4
-
5
- test('dominant script detection', () => {
6
- assert.equal(detectScriptName('你好世界'), 'han');
7
- assert.equal(detectScriptName('hello world'), 'latin');
8
- assert.equal(detectScriptName('こんにちは世界'), 'japanese');
9
- assert.equal(detectScriptName('Привет мир'), 'cyrillic');
10
- assert.equal(detectScriptName('안녕하세요'), 'hangul');
11
- });
12
-
13
- test('mixed CJK/latin resolves to the CJK script', () => {
14
- assert.equal(detectScriptName('用 React 重构这个 module'), 'han');
15
- assert.equal(detectScriptName('refactor this module 用例'), 'han');
16
- });
17
-
18
- test('japanese beats han when kana present (enables zh→ja detection)', () => {
19
- assert.equal(detectScriptName('世界'), 'han');
20
- assert.equal(detectScriptName('世界です'), 'japanese');
21
- });
@@ -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
- });