Skip to content

Commit 52dd990

Browse files
committed
docs: redesign V2 user documentation and interactive examples
1 parent 71c774d commit 52dd990

61 files changed

Lines changed: 7341 additions & 387 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎README.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
| 独立MCP服务 | intent-runtime-mcp --config <绝对路径.mjs> |
1414
| 已有MCP tools | /mcp的createIntentMcpAdapter,SDK继续管理传输与原路由 |
1515

16+
先按你的任务选入口,无需先了解 host/executor。Node.js 与 executor MCP 使用你配置的模型;Codex host 复用当前模型。远程 HTTP 由你的已有 MCP 服务承载 Adapter,独立命令提供 stdio。
17+
1618
要求Node.js≥20.0.0、ESM、schema-dsl@3.0.4。本文对应V2;截至2026-10-15,npm latest为1.0.2,V2尚未发行。先按[安装与版本](https://devcodex-labs.github.io/intent-runtime/guide/installation.html)构建并安装V2的tgz。结果/baseline的schemaVersion为2.0-draft.6,与npm版本不同。
1719

1820
## 在应用中调用
@@ -74,6 +76,8 @@ intent-runtime clean --host codex
7476

7577
基础无Schema示例见[快速开始](https://devcodex-labs.github.io/intent-runtime/guide/quick-start.html),读取目标、约束与字段见[结果指南](https://devcodex-labs.github.io/intent-runtime/guide/results.html)。
7678

79+
响应中的正向要求在 constraints,交付物在 expectedOutput;没有独立 requirements 副本。根级 complete 仍可能伴随无效字段,使用参数前检查对应 fieldResults 与 issues。固定的完整输入、Schema、多轮材料和结果可在[响应场景目录](https://devcodex-labs.github.io/intent-runtime/api/response.html#完整场景与多轮材料)下载。
80+
7781
## 使用文档
7882

7983
- [快速开始](https://devcodex-labs.github.io/intent-runtime/guide/quick-start.html)与[配置参考](https://devcodex-labs.github.io/intent-runtime/guide/configuration.html)

‎examples/README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
| 文件 | 用途 | 前置条件 |
66
|---|---|---|
77
| api/basic.mjs | 无Schema的基础目标与约束识别 | 项目安装运行库,环境INTENT_API_KEY/INTENT_MODEL |
8+
| api/native-response.mjs | 默认完整 Native 响应,与响应参考使用同一调用 | 项目安装运行库,环境INTENT_API_KEY/INTENT_MODEL |
89
| api/multi-turn.mjs | 完整VIP Schema与三轮失效/修正 | 项目安装运行库,环境INTENT_API_KEY/INTENT_MODEL |
910
| api/parse.mjs | API识别与所属字段消费 | 项目安装运行库/schema-dsl,环境INTENT_API_KEY/INTENT_MODEL |
1011
| custom-executor.ts | 自有模型网关适配 | ESM TypeScript项目,网关符合示例请求/回复协议 |

‎examples/api/native-response.mjs‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import {Intent, IntentError} from '@devcodex/intent-runtime';
2+
import {createApiExecutor} from '@devcodex/intent-runtime/adapters/api';
3+
4+
const apiKey = process.env.INTENT_API_KEY, model = process.env.INTENT_MODEL;
5+
if (!apiKey || !model) throw new Error('Set INTENT_API_KEY and INTENT_MODEL in your environment.');
6+
const intent = new Intent({
7+
language: 'zh-CN',
8+
executor: createApiExecutor({provider: 'openai', apiKey, model}),
9+
schema: {type: 'object', properties: {orderId: {type: 'string', pattern: '^[0-9]+$'}}},
10+
});
11+
try {
12+
const result = await intent.parse({input: '查询订单000123,不要取消订单', fields: ['orderId']});
13+
console.log(JSON.stringify(result, null, 2));
14+
} catch (error) {
15+
if (!(error instanceof IntentError)) throw error;
16+
console.error(JSON.stringify(error.toJSON()));
17+
process.exitCode = 1;
18+
} finally {await intent.dispose();}

‎scripts/check-doc-examples.mjs‎

Lines changed: 10 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,16 @@ import {cp, mkdir, mkdtemp, readFile, readdir, writeFile} from 'node:fs/promises
33
import path from 'node:path';
44
import {spawnSync} from 'node:child_process';
55
import {fileURLToPath, pathToFileURL} from 'node:url';
6+
import {checkResultLinks} from './documentation/result-check.mjs';
67

78
const repo = fileURLToPath(new URL('../', import.meta.url));
89
const [consumer, tarball] = process.argv.slice(2);
910
if (!consumer || !path.isAbsolute(consumer) || consumer.startsWith(repo)) throw new Error('Provide an external installed consumer directory.');
1011
const work = await mkdtemp(path.join(consumer, 'documentation-'));
12+
// Generated fixtures must still be reproducible through the installed core.
13+
// A shape-only check cannot establish field ownership or withdrawal identity.
14+
const generated = spawnSync(process.execPath, [path.join(repo, 'scripts/generate-doc-scenarios.mjs'), consumer], {encoding: 'utf8'});
15+
if (generated.status !== 0) throw new Error(generated.stdout + generated.stderr);
1116
const examples = path.join(work, 'examples');
1217
await cp(path.join(repo, 'examples'), examples, {recursive: true});
1318
function run(command, args, overrides = {}) {
@@ -25,11 +30,6 @@ const {StdioClientTransport} = await import(pathToFileURL(path.join(consumer, 'n
2530
const {INTENT_RESULT_SCHEMA} = await import(pathToFileURL(path.join(consumer, 'node_modules/@devcodex/intent-runtime/dist/index.js')).href);
2631
const {Validator} = await import(pathToFileURL(path.join(consumer, 'node_modules/schema-dsl/dist/pure.cjs')).href);
2732
const validator = new Validator({logger: false, strict: false, useDefaults: false, coerceTypes: false, removeAdditional: false, allErrors: true});
28-
function pointer(root, text) {
29-
assert(text === '' || text.startsWith('/'));
30-
assert(!/~(?![01])/u.test(text));
31-
return text === '' ? root : text.slice(1).split('/').map(s => s.replace(/~1/g, '/').replace(/~0/g, '~')).reduce((value, key) => {assert(value !== null && typeof value === 'object' && Object.hasOwn(value, key), text); return value[key];}, root);
32-
}
3333
let resultExamples = 0;
3434
for (const stem of ['order', 'clarification']) {
3535
const input = JSON.parse(await readFile(path.join(repo, 'website/content/public/examples/' + stem + '-request.json'), 'utf8'));
@@ -41,24 +41,7 @@ for (const stem of ['order', 'clarification']) {
4141
const instanceConfig = input.config.instances[input.request.instance ?? names[0]];
4242
assert(instanceConfig);
4343
const materials = {...instanceConfig, ...input.request};
44-
function sources(value) {
45-
if (!value || typeof value !== 'object') return;
46-
if ('root' in value && 'pointer' in value) {
47-
assert(Object.hasOwn(materials, value.root));
48-
const source = pointer(materials[value.root], value.pointer);
49-
if (value.quote !== undefined) assert((typeof source === 'string' ? source : JSON.stringify(source)).includes(value.quote));
50-
}
51-
for (const child of Object.values(value)) sources(child);
52-
}
53-
sources(result);
54-
const ids = new Set();
55-
function add(value) {assert(!ids.has(value.id)); ids.add(value.id);}
56-
function intents(nodes) {for (const node of nodes) {add(node); node.constraints.forEach(add); const local = new Set(); for (const ref of node.references) {assert(!local.has(ref.id)); local.add(ref.id);} intents(node.children);}}
57-
intents(result.effective.intents);
58-
result.effective.constraints.forEach(add); result.effective.relations.forEach(add); result.issues.forEach(add);
59-
for (const change of result.turn.changes) {if (change.target.kind !== 'unresolved') assert(ids.has(change.target.id)); if (change.conditionId) assert(ids.has(change.conditionId));}
60-
for (const id of result.turn.intentIds) assert(ids.has(id));
61-
for (const entry of result.evidence ?? []) pointer(result, entry.target);
44+
checkResultLinks(result, materials);
6245
resultExamples++;
6346
}
6447
validator.clearCache();
@@ -139,7 +122,7 @@ globalThis.fetch=async function(url,options){
139122
Object.assign(c.nodes[0],{domain:'订单',action:'查询',target:'订单000123',goal:'查询订单000123'});
140123
c.observations=[];
141124
const observation=(field,value)=>({ownerKey:'order',container:'inputs',field,presence:'known',withdrawn:false,valueJson:JSON.stringify(value),rawText:null,sources:[src],semantic:null});
142-
if(request.input.includes('不取消订单')){
125+
if(/不(?:要)?取消订单/.test(request.input)){
143126
c.constraints=[{key:'no-cancel',priorRef:null,ownerKey:'order',scope:'intent',referenceKey:null,kind:'prohibition',text:'不取消订单',expressionJson:null,sources:[src]}];
144127
c.changes.push({operation:'add',effect:'applied',targetKind:'constraint',targetKey:'no-cancel',conditionKey:null,description:'保留禁止取消约束',sources:[src],deltaHints:[]});
145128
}
@@ -159,13 +142,13 @@ globalThis.fetch=async function(url,options){
159142
};
160143
`);
161144
const runnableExamples = [];
162-
for (const name of ['basic', 'parse', 'multi-turn']) {
145+
for (const name of ['basic', 'parse', 'multi-turn', 'native-response']) {
163146
const trace = path.join(work, name + '-results.json');
164147
const env = {INTENT_API_KEY: 'controlled-test-only', INTENT_MODEL: 'controlled-language-reply', INTENT_DOC_TRACE: trace, INTENT_DOC_FAILURE: ''};
165148
run(process.execPath, ['--import', pathToFileURL(preload).href, path.join(examples, 'api', name + '.mjs')], env);
166149
const results = JSON.parse(await readFile(trace, 'utf8')).map(record => record.result);
167150
if (name === 'basic') {assert.equal(results.length, 1); assert.equal(results[0].fieldResults.length, 0); assert.equal(results[0].effective.intents[0].constraints[0].kind, 'prohibition');}
168-
if (name === 'parse') assert.equal(results[0].effective.intents[0].inputs.orderId, '000123');
151+
if (name === 'parse' || name === 'native-response') assert.equal(results[0].effective.intents[0].inputs.orderId, '000123');
169152
if (name === 'multi-turn') {
170153
assert.equal(results.length, 3);
171154
const [first, second, third] = results;
@@ -182,4 +165,4 @@ for (const name of ['basic', 'parse', 'multi-turn']) {
182165
runnableExamples.push({name, actualCoreResults: results.length, authenticationFailureHandled: true});
183166
}
184167
await writeFile(path.join(work, 'verification.json'), JSON.stringify({node: process.version, syntaxFiles, customExecutorStrictTypes: true, resultExamples, protocolRecords, globalMaintenance, runnableExamples, realProviderCalls: 0, scope: 'User documentation installation/configuration/SDK examples and static result fixtures; not model semantic acceptance.'}, null, 2) + '\n');
185-
console.log('User examples verified: ' + syntaxFiles + ' ESM files, strict custom executor, ' + resultExamples + ' full responses, 3 actual SDK protocols, 3 runnable API examples and auth failure branches, ' + (tarball ? 'isolated global setup/doctor/clean' : 'no new global install') + '.');
168+
console.log('User examples verified: ' + syntaxFiles + ' ESM files, strict custom executor, ' + resultExamples + ' full responses, 3 actual SDK protocols, 4 runnable API examples and auth failure branches, ' + (tarball ? 'isolated global setup/doctor/clean' : 'no new global install') + '.');
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import assert from 'node:assert/strict';
2+
3+
export function pointer(root, text) {
4+
assert(typeof text === 'string' && (text === '' || text.startsWith('/')), 'Expected JSON Pointer: ' + text);
5+
assert(!/~(?![01])/u.test(text), 'Invalid pointer escape: ' + text);
6+
return text === '' ? root : text.slice(1).split('/').map(s => s.replace(/~1/g, '/').replace(/~0/g, '~')).reduce((value, key) => {
7+
assert(value !== null && typeof value === 'object' && Object.hasOwn(value, key), text);
8+
return value[key];
9+
}, root);
10+
}
11+
12+
function identities(snapshot, unique = false) {
13+
const sets = {intent: new Set(), constraint: new Set(), relation: new Set()};
14+
const all = new Set();
15+
const add = (kind, value) => {
16+
assert(typeof value.id === 'string');
17+
if (unique) assert(!all.has(value.id), 'Duplicate identity: ' + value.id);
18+
all.add(value.id); sets[kind].add(value.id);
19+
};
20+
function visit(nodes) {for (const node of nodes ?? []) {
21+
add('intent', node); node.constraints?.forEach(c => add('constraint', c));
22+
const refs = new Set();
23+
for (const ref of node.references ?? []) {assert(!refs.has(ref.id)); refs.add(ref.id);}
24+
visit(node.children);
25+
}}
26+
visit(snapshot?.intents);
27+
snapshot?.constraints?.forEach(c => add('constraint', c));
28+
snapshot?.relations?.forEach(r => add('relation', r));
29+
return sets;
30+
}
31+
32+
// Documentation fixture checks, not an alternative public business Validator.
33+
// Prior withdrawal examples carry an explicit baseline snapshot. Arbitrary
34+
// Context strings are not treated as permission to reference any old ID.
35+
export function checkResultLinks(result, materials) {
36+
const current = identities(result.effective, true), prior = identities(materials.baseline);
37+
const all = new Set(Object.values(current).flatMap(set => [...set]));
38+
for (const issue of result.issues ?? []) {assert(!all.has(issue.id), 'Duplicate issue identity'); all.add(issue.id);}
39+
for (const change of result.turn.changes) {
40+
if (change.target.kind !== 'unresolved') {
41+
const {kind, id} = change.target;
42+
assert(current[kind], 'Unknown target kind');
43+
if (change.operation === 'withdraw' && change.effect === 'applied') {
44+
assert(!current[kind].has(id) && prior[kind].has(id), 'Withdrawal must reference a supplied prior ' + kind);
45+
} else assert(current[kind].has(id), 'Target must be a current ' + kind);
46+
}
47+
if (change.conditionId !== undefined) assert(current.constraint.has(change.conditionId), 'conditionId must be a current constraint');
48+
}
49+
for (const id of result.turn.intentIds) assert(current.intent.has(id), 'turn.intentIds must reference current intents');
50+
for (const relation of result.effective.relations) assert(current.intent.has(relation.fromId) && current.intent.has(relation.toId));
51+
for (const entry of [...result.evidence ?? [], ...result.inferences]) pointer(result, entry.target);
52+
function sources(value) {
53+
if (!value || typeof value !== 'object') return;
54+
if ('root' in value && 'pointer' in value) {
55+
assert(Object.hasOwn(materials, value.root), 'Missing source root: ' + value.root);
56+
const source = pointer(materials[value.root], value.pointer);
57+
if (value.quote !== undefined) assert((typeof source === 'string' ? source : JSON.stringify(source)).includes(value.quote));
58+
}
59+
for (const child of Object.values(value)) sources(child);
60+
}
61+
sources(result);
62+
}
63+
64+
// Runtime IDs are opaque. Canonical names make fixed documentation readable;
65+
// the same replacement is applied to requests, baselines and results together.
66+
export function readableIds(value) {
67+
const mapping = new Map(), counts = new Map();
68+
return JSON.parse(JSON.stringify(value, (_key, item) => {
69+
if (typeof item !== 'string') return item;
70+
return item.replace(/(?:intent|constraint|relation|reference)-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, id => {
71+
if (!mapping.has(id)) {
72+
const kind = id.slice(0, id.indexOf('-')), index = (counts.get(kind) ?? 0) + 1;
73+
counts.set(kind, index); mapping.set(id, kind + '-' + index);
74+
}
75+
return mapping.get(id);
76+
});
77+
}));
78+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import test from 'node:test';
2+
import assert from 'node:assert/strict';
3+
import {checkResultLinks, pointer, readableIds} from './result-check.mjs';
4+
5+
const old = {id: 'old', constraints: [], references: [], children: []};
6+
const materials = {input: '撤回旧请求', baseline: {intents: [old], constraints: [], relations: []}};
7+
function withdrawn() {return {turn: {intentIds: [], changes: [{operation: 'withdraw', effect: 'applied', target: {kind: 'intent', id: 'old'}}]}, effective: {intents: [], constraints: [], relations: []}, inferences: []};}
8+
test('withdrawal may reference a baseline intent absent from effective', () => checkResultLinks(withdrawn(), materials));
9+
test('derived issue IDs and nested baseline references use the same readable ID', () => {
10+
const id = 'intent-c7135d1a-ad05-4894-a974-07b2fb6dac12';
11+
assert.deepEqual(readableIds({id, issue: 'understanding-' + id, baseline: {id}, orderId: '000123'}), {id: 'intent-1', issue: 'understanding-intent-1', baseline: {id: 'intent-1'}, orderId: '000123'});
12+
});
13+
test('invented prior IDs remain invalid', () => {const r = withdrawn(); r.turn.changes[0].target.id = 'invented'; assert.throws(() => checkResultLinks(r, materials), /supplied prior/);});
14+
test('a prior intent cannot authorize a constraint target', () => {const r = withdrawn(); r.turn.changes[0].target.kind = 'constraint'; assert.throws(() => checkResultLinks(r, materials), /supplied prior/);});
15+
test('arbitrary Context strings do not authorize prior IDs', () => assert.throws(() => checkResultLinks(withdrawn(), {input: '撤回', context: {note: 'old'}}), /supplied prior/));
16+
test('a condition must reference a current constraint, not an intent', () => {
17+
const r = withdrawn(); r.effective.intents = [old]; r.turn.intentIds = ['old'];
18+
r.turn.changes = [{operation: 'withdraw', effect: 'conditional', target: {kind: 'intent', id: 'old'}, conditionId: 'old'}];
19+
assert.throws(() => checkResultLinks(r, materials), /current constraint/);
20+
});
21+
test('pointer preserves empty names and escaped keys and rejects malformed paths', () => {
22+
assert.equal(pointer({meta: {'': 1}}, '/meta/'), 1);
23+
assert.equal(pointer({'a/b': {'~': 2}}, '/a~1b/~0'), 2);
24+
assert.throws(() => pointer({}, '/a~2b'), /escape/);
25+
assert.throws(() => pointer({}, 'orders/1/amount'), /JSON Pointer/);
26+
});

0 commit comments

Comments
 (0)