cognitive-modules-cli 1.1.0 → 1.2.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/README.md CHANGED
@@ -1,162 +1,86 @@
1
- # Cognitive Runtime
1
+ # Cognitive Modules CLI (Node.js)
2
2
 
3
- **Structured AI Task Execution**
3
+ [![npm version](https://badge.fury.io/js/cognitive-modules-cli.svg)](https://www.npmjs.com/package/cognitive-modules-cli)
4
4
 
5
- Cognitive Runtime is the next-generation execution engine for Cognitive Modules. It provides a clean, provider-agnostic runtime that treats LLMs as interchangeable backends.
5
+ Node.js/TypeScript 版本的 Cognitive Modules CLI。
6
6
 
7
- ## Philosophy
7
+ > 这是 [cognitive-modules](../../README.md) monorepo 的一部分。
8
8
 
9
- Following the **Cognitive Runtime + Provider** architecture:
10
-
11
- ```
12
- ┌─────────────────────────────────────┐
13
- │ Cognitive Runtime │
14
- │ ┌────────────────────────────────┐ │
15
- │ │ Module System │ │
16
- │ │ (load, parse, validate) │ │
17
- │ └────────────────────────────────┘ │
18
- │ ┌────────────────────────────────┐ │
19
- │ │ Execution Engine │ │
20
- │ │ (prompt, schema, contract) │ │
21
- │ └────────────────────────────────┘ │
22
- │ ┌────────────────────────────────┐ │
23
- │ │ Provider Abstraction │ │
24
- │ │ (gemini, openai, anthropic, │ │
25
- │ │ deepseek, minimax, qwen...) │ │
26
- │ └────────────────────────────────┘ │
27
- └─────────────────────────────────────┘
28
- ```
29
-
30
- ## Installation
9
+ ## 安装
31
10
 
32
11
  ```bash
33
- npm install -g cognitive-runtime
34
- ```
12
+ # 全局安装(推荐)
13
+ npm install -g cogn
35
14
 
36
- Or run directly:
37
-
38
- ```bash
39
- npx cognitive-runtime --help
15
+ # 或使用 npx 零安装
16
+ npx cogn --help
40
17
  ```
41
18
 
42
- ## Usage
43
-
44
- ### Run a Module
19
+ ## 快速开始
45
20
 
46
21
  ```bash
47
- cog run code-reviewer --args "def foo(): pass"
48
- ```
22
+ # 配置 LLM
23
+ export LLM_PROVIDER=openai
24
+ export OPENAI_API_KEY=sk-xxx
49
25
 
50
- ### List Modules
26
+ # 运行模块
27
+ cog run code-reviewer --args "def login(u,p): return db.query(f'SELECT * FROM users WHERE name={u}')"
51
28
 
52
- ```bash
29
+ # 列出模块
53
30
  cog list
54
- ```
55
31
 
56
- ### Pipe Mode (stdin/stdout)
57
-
58
- ```bash
32
+ # 管道模式
59
33
  echo "review this code" | cog pipe --module code-reviewer
60
34
  ```
61
35
 
62
- ### Check Configuration
36
+ ## Python 版的区别
63
37
 
64
- ```bash
65
- cog doctor
66
- ```
38
+ | 功能 | Python (`cogn`) | Node.js (`cog`) |
39
+ |------|----------------|-----------------|
40
+ | 包名 | `cognitive-modules` | `cognitive-modules-cli` |
41
+ | 安装 | `pip install` | `npm install -g` |
42
+ | 子代理 | ✅ `@call:module` | ❌ 暂不支持 |
43
+ | MCP Server | ✅ | ❌ 暂不支持 |
44
+ | HTTP Server | ✅ | ❌ 暂不支持 |
67
45
 
68
- ## Module Formats
46
+ 两个版本共享相同的模块格式和 v2.2 规范。
69
47
 
70
- ### v2 (Recommended)
48
+ ## 支持的 Provider
71
49
 
72
- ```
73
- my-module/
74
- ├── module.yaml # Machine-readable manifest
75
- ├── prompt.md # Human-readable prompt
76
- ├── schema.json # IO contract
77
- └── tests/
78
- ├── case1.input.json
79
- └── case1.expected.json
80
- ```
50
+ | Provider | 环境变量 | 别名 |
51
+ |----------|----------|------|
52
+ | OpenAI | `OPENAI_API_KEY` | - |
53
+ | Anthropic | `ANTHROPIC_API_KEY` | - |
54
+ | Gemini | `GEMINI_API_KEY` | - |
55
+ | DeepSeek | `DEEPSEEK_API_KEY` | - |
56
+ | MiniMax | `MINIMAX_API_KEY` | - |
57
+ | Moonshot | `MOONSHOT_API_KEY` | `kimi` |
58
+ | Qwen | `DASHSCOPE_API_KEY` | `tongyi` |
59
+ | Ollama | `OLLAMA_HOST` | `local` |
81
60
 
82
- **module.yaml**:
83
- ```yaml
84
- name: my-module
85
- version: 2.0.0
86
- responsibility: What this module does
87
- constraints:
88
- no_network: true
89
- no_side_effects: true
90
- output:
91
- mode: json_strict
92
- require_confidence: true
93
- require_rationale: true
94
- require_behavior_equivalence: true
95
- tools:
96
- allowed: []
97
- ```
98
-
99
- ### v1 (Legacy, still supported)
100
-
101
- ```
102
- my-module/
103
- ├── MODULE.md # Frontmatter + prompt combined
104
- └── schema.json
105
- ```
106
-
107
- ## Providers
108
-
109
- | Provider | Environment Variable | Default Model |
110
- |------------|------------------------|----------------------|
111
- | Gemini | `GEMINI_API_KEY` | `gemini-3-flash` |
112
- | OpenAI | `OPENAI_API_KEY` | `gpt-5.2` |
113
- | Anthropic | `ANTHROPIC_API_KEY` | `claude-sonnet-4.5` |
114
- | DeepSeek | `DEEPSEEK_API_KEY` | `deepseek-v3.2` |
115
- | MiniMax | `MINIMAX_API_KEY` | `MiniMax-M2.1` |
116
- | Moonshot | `MOONSHOT_API_KEY` | `kimi-k2.5` |
117
- | Qwen | `DASHSCOPE_API_KEY` | `qwen3-max` |
118
- | Ollama | `OLLAMA_HOST` | `llama4` (local) |
119
-
120
- ### Provider Aliases
61
+ ## 命令
121
62
 
122
- - `kimi` → Moonshot
123
- - `tongyi` / `dashscope` → Qwen
124
- - `local` Ollama
125
-
126
- ## Module Search Paths
127
-
128
- Modules are searched in order:
129
-
130
- 1. `./cognitive/modules/` (project-local)
131
- 2. `./.cognitive/modules/` (project-local, hidden)
132
- 3. `~/.cognitive/modules/` (user-global)
133
-
134
- ## Programmatic API
135
-
136
- ```typescript
137
- import { getProvider, findModule, runModule } from 'cognitive-runtime';
138
-
139
- const provider = getProvider('gemini');
140
- const module = await findModule('code-reviewer', ['./cognitive/modules']);
141
-
142
- if (module) {
143
- const result = await runModule(module, provider, {
144
- args: 'def foo(): pass',
145
- });
146
- console.log(result.output);
147
- }
63
+ ```bash
64
+ cog list # 列出模块
65
+ cog run <module> --args "..." # 运行模块
66
+ cog add <url> -m <module> # 从 GitHub 添加模块
67
+ cog update <module> # 更新模块
68
+ cog remove <module> # 删除模块
69
+ cog versions <url> # 查看可用版本
70
+ cog init <name> # 创建新模块
71
+ cog pipe --module <name> # 管道模式
148
72
  ```
149
73
 
150
- ## Development
74
+ ## 开发
151
75
 
152
76
  ```bash
153
- # Install dependencies
77
+ # 安装依赖
154
78
  npm install
155
79
 
156
- # Build
80
+ # 构建
157
81
  npm run build
158
82
 
159
- # Run in development
83
+ # 开发模式运行
160
84
  npm run dev -- run code-reviewer --args "..."
161
85
  ```
162
86
 
@@ -19,6 +19,18 @@ async function detectFormat(modulePath) {
19
19
  return 'v1';
20
20
  }
21
21
  }
22
+ /**
23
+ * Detect v2.x sub-version from manifest
24
+ */
25
+ function detectV2Version(manifest) {
26
+ if (manifest.tier || manifest.overflow || manifest.enums) {
27
+ return 'v2.2';
28
+ }
29
+ if (manifest.policies || manifest.failure) {
30
+ return 'v2.1';
31
+ }
32
+ return 'v2.0';
33
+ }
22
34
  /**
23
35
  * Load v2 format module (module.yaml + prompt.md)
24
36
  */
@@ -29,6 +41,8 @@ async function loadModuleV2(modulePath) {
29
41
  // Read module.yaml
30
42
  const manifestContent = await fs.readFile(manifestFile, 'utf-8');
31
43
  const manifest = yaml.load(manifestContent);
44
+ // Detect v2.x version
45
+ const formatVersion = detectV2Version(manifest);
32
46
  // Read prompt.md
33
47
  let prompt = '';
34
48
  try {
@@ -40,17 +54,61 @@ async function loadModuleV2(modulePath) {
40
54
  // Read schema.json
41
55
  let inputSchema;
42
56
  let outputSchema;
57
+ let dataSchema;
58
+ let metaSchema;
43
59
  let errorSchema;
44
60
  try {
45
61
  const schemaContent = await fs.readFile(schemaFile, 'utf-8');
46
62
  const schema = JSON.parse(schemaContent);
47
63
  inputSchema = schema.input;
48
- outputSchema = schema.output;
64
+ // Support both "data" (v2.2) and "output" (v2.1) aliases
65
+ dataSchema = schema.data || schema.output;
66
+ outputSchema = dataSchema; // Backward compat
67
+ metaSchema = schema.meta;
49
68
  errorSchema = schema.error;
50
69
  }
51
70
  catch {
52
71
  // Schema file is optional but recommended
53
72
  }
73
+ // Extract v2.2 fields
74
+ const tier = manifest.tier;
75
+ const schemaStrictness = manifest.schema_strictness || 'medium';
76
+ // Determine default max_items based on strictness (SPEC-v2.2)
77
+ const strictnessMaxItems = {
78
+ high: 0,
79
+ medium: 5,
80
+ low: 20
81
+ };
82
+ const defaultMaxItems = strictnessMaxItems[schemaStrictness] ?? 5;
83
+ const defaultEnabled = schemaStrictness !== 'high';
84
+ // Parse overflow config with strictness-based defaults
85
+ const overflowRaw = manifest.overflow || {};
86
+ const overflow = {
87
+ enabled: overflowRaw.enabled ?? defaultEnabled,
88
+ recoverable: overflowRaw.recoverable ?? true,
89
+ max_items: overflowRaw.max_items ?? defaultMaxItems,
90
+ require_suggested_mapping: overflowRaw.require_suggested_mapping ?? true
91
+ };
92
+ // Parse enums config
93
+ const enumsRaw = manifest.enums || {};
94
+ const enums = {
95
+ strategy: enumsRaw.strategy ??
96
+ (tier === 'exec' ? 'strict' : 'extensible'),
97
+ unknown_tag: enumsRaw.unknown_tag ?? 'custom'
98
+ };
99
+ // Parse compat config
100
+ const compatRaw = manifest.compat || {};
101
+ const compat = {
102
+ accepts_v21_payload: compatRaw.accepts_v21_payload ?? true,
103
+ runtime_auto_wrap: compatRaw.runtime_auto_wrap ?? true,
104
+ schema_output_alias: compatRaw.schema_output_alias ?? 'data'
105
+ };
106
+ // Parse meta config (including risk_rule)
107
+ const metaRaw = manifest.meta || {};
108
+ const metaConfig = {
109
+ required: metaRaw.required,
110
+ risk_rule: metaRaw.risk_rule,
111
+ };
54
112
  return {
55
113
  name: manifest.name || path.basename(modulePath),
56
114
  version: manifest.version || '1.0.0',
@@ -62,13 +120,26 @@ async function loadModuleV2(modulePath) {
62
120
  output: manifest.output,
63
121
  failure: manifest.failure,
64
122
  runtimeRequirements: manifest.runtime_requirements,
123
+ // v2.2 fields
124
+ tier,
125
+ schemaStrictness,
126
+ overflow,
127
+ enums,
128
+ compat,
129
+ metaConfig,
130
+ // Context and prompt
65
131
  context: manifest.context,
66
132
  prompt,
133
+ // Schemas
67
134
  inputSchema,
68
135
  outputSchema,
136
+ dataSchema,
137
+ metaSchema,
69
138
  errorSchema,
139
+ // Metadata
70
140
  location: modulePath,
71
141
  format: 'v2',
142
+ formatVersion,
72
143
  };
73
144
  }
74
145
  /**
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Module Runner - Execute Cognitive Modules
3
- * v2.1: Envelope format support, clean input mapping
3
+ * v2.2: Envelope format with meta/data separation, risk_rule, repair pass
4
4
  */
5
5
  import type { Provider, CognitiveModule, ModuleResult, ModuleInput } from '../types.js';
6
6
  export interface RunOptions {
@@ -8,5 +8,7 @@ export interface RunOptions {
8
8
  args?: string;
9
9
  verbose?: boolean;
10
10
  useEnvelope?: boolean;
11
+ useV22?: boolean;
12
+ enableRepair?: boolean;
11
13
  }
12
14
  export declare function runModule(module: CognitiveModule, provider: Provider, options?: RunOptions): Promise<ModuleResult>;
@@ -1,11 +1,116 @@
1
1
  /**
2
2
  * Module Runner - Execute Cognitive Modules
3
- * v2.1: Envelope format support, clean input mapping
3
+ * v2.2: Envelope format with meta/data separation, risk_rule, repair pass
4
4
  */
5
+ import { aggregateRisk, isV22Envelope } from '../types.js';
6
+ // =============================================================================
7
+ // Repair Pass (v2.2)
8
+ // =============================================================================
9
+ /**
10
+ * Attempt to repair envelope format issues without changing semantics.
11
+ *
12
+ * Repairs (lossless only):
13
+ * - Missing meta fields (fill with conservative defaults)
14
+ * - Truncate explain if too long
15
+ * - Trim whitespace from string fields
16
+ *
17
+ * Does NOT repair:
18
+ * - Invalid enum values (treated as validation failure)
19
+ */
20
+ function repairEnvelope(response, riskRule = 'max_changes_risk', maxExplainLength = 280) {
21
+ const repaired = { ...response };
22
+ // Ensure meta exists
23
+ if (!repaired.meta || typeof repaired.meta !== 'object') {
24
+ repaired.meta = {};
25
+ }
26
+ const meta = repaired.meta;
27
+ const data = (repaired.data ?? {});
28
+ // Repair confidence
29
+ if (typeof meta.confidence !== 'number') {
30
+ meta.confidence = data.confidence ?? 0.5;
31
+ }
32
+ meta.confidence = Math.max(0, Math.min(1, meta.confidence));
33
+ // Repair risk using configurable aggregation rule
34
+ if (!meta.risk) {
35
+ meta.risk = aggregateRisk(data, riskRule);
36
+ }
37
+ // Trim whitespace only (lossless), do NOT invent new values
38
+ if (typeof meta.risk === 'string') {
39
+ meta.risk = meta.risk.trim().toLowerCase();
40
+ }
41
+ // Repair explain
42
+ if (typeof meta.explain !== 'string') {
43
+ const rationale = data.rationale;
44
+ meta.explain = rationale ? String(rationale).slice(0, maxExplainLength) : 'No explanation provided';
45
+ }
46
+ // Trim whitespace (lossless)
47
+ const explainStr = meta.explain;
48
+ meta.explain = explainStr.trim();
49
+ if (meta.explain.length > maxExplainLength) {
50
+ meta.explain = meta.explain.slice(0, maxExplainLength - 3) + '...';
51
+ }
52
+ // Build proper v2.2 response
53
+ const builtMeta = {
54
+ confidence: meta.confidence,
55
+ risk: meta.risk,
56
+ explain: meta.explain
57
+ };
58
+ const result = repaired.ok === false ? {
59
+ ok: false,
60
+ meta: builtMeta,
61
+ error: repaired.error ?? { code: 'UNKNOWN', message: 'Unknown error' },
62
+ partial_data: repaired.partial_data
63
+ } : {
64
+ ok: true,
65
+ meta: builtMeta,
66
+ data: repaired.data
67
+ };
68
+ return result;
69
+ }
70
+ /**
71
+ * Wrap v2.1 response to v2.2 format
72
+ */
73
+ function wrapV21ToV22(response, riskRule = 'max_changes_risk') {
74
+ if (isV22Envelope(response)) {
75
+ return response;
76
+ }
77
+ if (response.ok) {
78
+ const data = (response.data ?? {});
79
+ const confidence = data.confidence ?? 0.5;
80
+ const rationale = data.rationale ?? '';
81
+ return {
82
+ ok: true,
83
+ meta: {
84
+ confidence,
85
+ risk: aggregateRisk(data, riskRule),
86
+ explain: rationale.slice(0, 280) || 'No explanation provided'
87
+ },
88
+ data: data
89
+ };
90
+ }
91
+ else {
92
+ const errorMsg = response.error?.message ?? 'Unknown error';
93
+ return {
94
+ ok: false,
95
+ meta: {
96
+ confidence: 0,
97
+ risk: 'high',
98
+ explain: errorMsg.slice(0, 280)
99
+ },
100
+ error: response.error ?? { code: 'UNKNOWN', message: errorMsg },
101
+ partial_data: response.partial_data
102
+ };
103
+ }
104
+ }
5
105
  export async function runModule(module, provider, options = {}) {
6
- const { args, input, verbose = false, useEnvelope } = options;
106
+ const { args, input, verbose = false, useEnvelope, useV22, enableRepair = true } = options;
7
107
  // Determine if we should use envelope format
8
108
  const shouldUseEnvelope = useEnvelope ?? (module.output?.envelope === true || module.format === 'v2');
109
+ // Determine if we should use v2.2 format
110
+ const isV22Module = module.tier !== undefined || module.formatVersion === 'v2.2';
111
+ const shouldUseV22 = useV22 ?? (isV22Module || module.compat?.runtime_auto_wrap === true);
112
+ // Get risk_rule from module config
113
+ const riskRule = module.metaConfig?.risk_rule ?? 'max_changes_risk';
9
114
  // Build clean input data (v2 style: no $ARGUMENTS pollution)
10
115
  const inputData = input || {};
11
116
  // Map legacy --args to clean input
@@ -61,11 +166,21 @@ export async function runModule(module, provider, options = {}) {
61
166
  }
62
167
  // Add envelope format instructions
63
168
  if (shouldUseEnvelope) {
64
- systemParts.push('', 'RESPONSE FORMAT (Envelope):');
65
- systemParts.push('- Wrap your response in the envelope format');
66
- systemParts.push('- Success: { "ok": true, "data": { ...your output... } }');
67
- systemParts.push('- Error: { "ok": false, "error": { "code": "ERROR_CODE", "message": "..." } }');
68
- systemParts.push('- Include "confidence" (0-1) and "rationale" in data');
169
+ if (shouldUseV22) {
170
+ systemParts.push('', 'RESPONSE FORMAT (Envelope v2.2):');
171
+ systemParts.push('- Wrap your response in the v2.2 envelope format with separate meta and data');
172
+ systemParts.push('- Success: { "ok": true, "meta": { "confidence": 0.9, "risk": "low", "explain": "short summary" }, "data": { ...payload... } }');
173
+ systemParts.push('- Error: { "ok": false, "meta": { "confidence": 0.0, "risk": "high", "explain": "error summary" }, "error": { "code": "ERROR_CODE", "message": "..." } }');
174
+ systemParts.push('- meta.explain must be ≤280 characters. data.rationale can be longer for detailed reasoning.');
175
+ systemParts.push('- meta.risk must be one of: "none", "low", "medium", "high"');
176
+ }
177
+ else {
178
+ systemParts.push('', 'RESPONSE FORMAT (Envelope):');
179
+ systemParts.push('- Wrap your response in the envelope format');
180
+ systemParts.push('- Success: { "ok": true, "data": { ...your output... } }');
181
+ systemParts.push('- Error: { "ok": false, "error": { "code": "ERROR_CODE", "message": "..." } }');
182
+ systemParts.push('- Include "confidence" (0-1) and "rationale" in data');
183
+ }
69
184
  if (module.output?.require_behavior_equivalence) {
70
185
  systemParts.push('- Include "behavior_equivalence" (boolean) in data');
71
186
  }
@@ -105,10 +220,46 @@ export async function runModule(module, provider, options = {}) {
105
220
  }
106
221
  // Handle envelope format
107
222
  if (shouldUseEnvelope && isEnvelopeResponse(parsed)) {
108
- return parseEnvelopeResponse(parsed, result.content);
223
+ let response = parseEnvelopeResponse(parsed, result.content);
224
+ // Upgrade to v2.2 if needed
225
+ if (shouldUseV22 && response.ok && !('meta' in response && response.meta)) {
226
+ const upgraded = wrapV21ToV22(parsed, riskRule);
227
+ response = {
228
+ ok: true,
229
+ meta: upgraded.meta,
230
+ data: upgraded.data,
231
+ raw: result.content
232
+ };
233
+ }
234
+ // Apply repair pass if enabled and response needs it
235
+ if (enableRepair && response.ok && shouldUseV22) {
236
+ const repaired = repairEnvelope(response, riskRule);
237
+ response = {
238
+ ok: true,
239
+ meta: repaired.meta,
240
+ data: repaired.data,
241
+ raw: result.content
242
+ };
243
+ }
244
+ return response;
109
245
  }
110
246
  // Handle legacy format (non-envelope)
111
- return parseLegacyResponse(parsed, result.content);
247
+ const legacyResult = parseLegacyResponse(parsed, result.content);
248
+ // Upgrade to v2.2 if requested
249
+ if (shouldUseV22 && legacyResult.ok) {
250
+ const data = (legacyResult.data ?? {});
251
+ return {
252
+ ok: true,
253
+ meta: {
254
+ confidence: data.confidence ?? 0.5,
255
+ risk: aggregateRisk(data, riskRule),
256
+ explain: (data.rationale ?? '').slice(0, 280) || 'No explanation provided'
257
+ },
258
+ data: legacyResult.data,
259
+ raw: result.content
260
+ };
261
+ }
262
+ return legacyResult;
112
263
  }
113
264
  /**
114
265
  * Check if response is in envelope format
@@ -120,11 +271,32 @@ function isEnvelopeResponse(obj) {
120
271
  return typeof o.ok === 'boolean';
121
272
  }
122
273
  /**
123
- * Parse envelope format response
274
+ * Parse envelope format response (supports both v2.1 and v2.2)
124
275
  */
125
276
  function parseEnvelopeResponse(response, raw) {
277
+ // Check if v2.2 format (has meta)
278
+ if (isV22Envelope(response)) {
279
+ if (response.ok) {
280
+ return {
281
+ ok: true,
282
+ meta: response.meta,
283
+ data: response.data,
284
+ raw,
285
+ };
286
+ }
287
+ else {
288
+ return {
289
+ ok: false,
290
+ meta: response.meta,
291
+ error: response.error,
292
+ partial_data: response.partial_data,
293
+ raw,
294
+ };
295
+ }
296
+ }
297
+ // v2.1 format
126
298
  if (response.ok) {
127
- const data = response.data;
299
+ const data = (response.data ?? {});
128
300
  return {
129
301
  ok: true,
130
302
  data: {
package/dist/types.d.ts CHANGED
@@ -33,6 +33,8 @@ export type SchemaStrictness = 'high' | 'medium' | 'low';
33
33
  export type RiskLevel = 'none' | 'low' | 'medium' | 'high';
34
34
  /** Enum extension strategy */
35
35
  export type EnumStrategy = 'strict' | 'extensible';
36
+ /** Risk aggregation rule */
37
+ export type RiskRule = 'max_changes_risk' | 'max_issues_risk' | 'explicit';
36
38
  export interface CognitiveModule {
37
39
  name: string;
38
40
  version: string;
@@ -49,6 +51,7 @@ export interface CognitiveModule {
49
51
  overflow?: OverflowConfig;
50
52
  enums?: EnumConfig;
51
53
  compat?: CompatConfig;
54
+ metaConfig?: MetaConfig;
52
55
  context?: 'fork' | 'main';
53
56
  prompt: string;
54
57
  inputSchema?: object;
@@ -115,6 +118,18 @@ export interface CompatConfig {
115
118
  runtime_auto_wrap?: boolean;
116
119
  schema_output_alias?: 'data' | 'output';
117
120
  }
121
+ /** Meta field configuration (v2.2) */
122
+ export interface MetaConfig {
123
+ required?: string[];
124
+ risk_rule?: RiskRule;
125
+ confidence?: {
126
+ min?: number;
127
+ max?: number;
128
+ };
129
+ explain?: {
130
+ max_chars?: number;
131
+ };
132
+ }
118
133
  /**
119
134
  * Control plane metadata - unified across all modules.
120
135
  * Used for routing, logging, UI cards, and middleware decisions.
@@ -255,10 +270,15 @@ export declare function isV22Envelope<T>(response: EnvelopeResponse<T>): respons
255
270
  /** Check if response is successful */
256
271
  export declare function isEnvelopeSuccess<T>(response: EnvelopeResponse<T>): response is EnvelopeSuccessV22<T> | EnvelopeSuccessV21<T>;
257
272
  /** Extract meta from any envelope response */
258
- export declare function extractMeta<T>(response: EnvelopeResponse<T>): EnvelopeMeta;
259
- /** Aggregate risk from list of changes */
260
- export declare function aggregateRisk(changes: Array<{
261
- risk?: RiskLevel;
262
- }>): RiskLevel;
273
+ export declare function extractMeta<T>(response: EnvelopeResponse<T>, riskRule?: RiskRule): EnvelopeMeta;
274
+ /**
275
+ * Aggregate risk based on configured rule.
276
+ *
277
+ * Rules:
278
+ * - max_changes_risk: max(data.changes[*].risk) - default
279
+ * - max_issues_risk: max(data.issues[*].risk) - for review modules
280
+ * - explicit: return "medium", module should set risk explicitly
281
+ */
282
+ export declare function aggregateRisk(data: Record<string, unknown>, riskRule?: RiskRule): RiskLevel;
263
283
  /** Check if result should be escalated to human review */
264
284
  export declare function shouldEscalate<T>(response: EnvelopeResponse<T>, confidenceThreshold?: number): boolean;
package/dist/types.js CHANGED
@@ -14,17 +14,17 @@ export function isEnvelopeSuccess(response) {
14
14
  return response.ok === true;
15
15
  }
16
16
  /** Extract meta from any envelope response */
17
- export function extractMeta(response) {
17
+ export function extractMeta(response, riskRule = 'max_changes_risk') {
18
18
  if (isV22Envelope(response)) {
19
19
  return response.meta;
20
20
  }
21
21
  // Synthesize meta from v2.1 response
22
22
  if (response.ok) {
23
- const data = response.data;
23
+ const data = (response.data ?? {});
24
24
  return {
25
- confidence: data?.confidence ?? 0.5,
26
- risk: 'medium',
27
- explain: (data?.rationale ?? '').slice(0, 280) || 'No explanation',
25
+ confidence: data.confidence ?? 0.5,
26
+ risk: aggregateRisk(data, riskRule),
27
+ explain: (data.rationale ?? '').slice(0, 280) || 'No explanation',
28
28
  };
29
29
  }
30
30
  else {
@@ -35,20 +35,44 @@ export function extractMeta(response) {
35
35
  };
36
36
  }
37
37
  }
38
- /** Aggregate risk from list of changes */
39
- export function aggregateRisk(changes) {
38
+ /** Aggregate risk from list of items */
39
+ function aggregateRiskFromList(items) {
40
40
  const riskLevels = { none: 0, low: 1, medium: 2, high: 3 };
41
41
  const riskNames = ['none', 'low', 'medium', 'high'];
42
- if (!changes || changes.length === 0) {
42
+ if (!items || items.length === 0) {
43
43
  return 'medium';
44
44
  }
45
45
  let maxLevel = 0;
46
- for (const change of changes) {
47
- const level = riskLevels[change.risk ?? 'medium'];
46
+ for (const item of items) {
47
+ const level = riskLevels[item.risk ?? 'medium'];
48
48
  maxLevel = Math.max(maxLevel, level);
49
49
  }
50
50
  return riskNames[maxLevel];
51
51
  }
52
+ /**
53
+ * Aggregate risk based on configured rule.
54
+ *
55
+ * Rules:
56
+ * - max_changes_risk: max(data.changes[*].risk) - default
57
+ * - max_issues_risk: max(data.issues[*].risk) - for review modules
58
+ * - explicit: return "medium", module should set risk explicitly
59
+ */
60
+ export function aggregateRisk(data, riskRule = 'max_changes_risk') {
61
+ if (riskRule === 'max_changes_risk') {
62
+ const changes = data.changes ?? [];
63
+ return aggregateRiskFromList(changes);
64
+ }
65
+ else if (riskRule === 'max_issues_risk') {
66
+ const issues = data.issues ?? [];
67
+ return aggregateRiskFromList(issues);
68
+ }
69
+ else if (riskRule === 'explicit') {
70
+ return 'medium'; // Module should override
71
+ }
72
+ // Fallback to changes
73
+ const changes = data.changes ?? [];
74
+ return aggregateRiskFromList(changes);
75
+ }
52
76
  /** Check if result should be escalated to human review */
53
77
  export function shouldEscalate(response, confidenceThreshold = 0.7) {
54
78
  const meta = extractMeta(response);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cognitive-modules-cli",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Cognitive Modules - Structured AI Task Execution with version management",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -6,7 +6,21 @@
6
6
  import * as fs from 'node:fs/promises';
7
7
  import * as path from 'node:path';
8
8
  import yaml from 'js-yaml';
9
- import type { CognitiveModule, ModuleConstraints, ModulePolicies, ToolsPolicy, OutputContract, FailureContract, RuntimeRequirements } from '../types.js';
9
+ import type {
10
+ CognitiveModule,
11
+ ModuleConstraints,
12
+ ModulePolicies,
13
+ ToolsPolicy,
14
+ OutputContract,
15
+ FailureContract,
16
+ RuntimeRequirements,
17
+ OverflowConfig,
18
+ EnumConfig,
19
+ CompatConfig,
20
+ MetaConfig,
21
+ ModuleTier,
22
+ SchemaStrictness
23
+ } from '../types.js';
10
24
 
11
25
  const FRONTMATTER_REGEX = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n([\s\S]*))?/;
12
26
 
@@ -23,6 +37,19 @@ async function detectFormat(modulePath: string): Promise<'v1' | 'v2'> {
23
37
  }
24
38
  }
25
39
 
40
+ /**
41
+ * Detect v2.x sub-version from manifest
42
+ */
43
+ function detectV2Version(manifest: Record<string, unknown>): string {
44
+ if (manifest.tier || manifest.overflow || manifest.enums) {
45
+ return 'v2.2';
46
+ }
47
+ if (manifest.policies || manifest.failure) {
48
+ return 'v2.1';
49
+ }
50
+ return 'v2.0';
51
+ }
52
+
26
53
  /**
27
54
  * Load v2 format module (module.yaml + prompt.md)
28
55
  */
@@ -35,6 +62,9 @@ async function loadModuleV2(modulePath: string): Promise<CognitiveModule> {
35
62
  const manifestContent = await fs.readFile(manifestFile, 'utf-8');
36
63
  const manifest = yaml.load(manifestContent) as Record<string, unknown>;
37
64
 
65
+ // Detect v2.x version
66
+ const formatVersion = detectV2Version(manifest);
67
+
38
68
  // Read prompt.md
39
69
  let prompt = '';
40
70
  try {
@@ -46,18 +76,68 @@ async function loadModuleV2(modulePath: string): Promise<CognitiveModule> {
46
76
  // Read schema.json
47
77
  let inputSchema: object | undefined;
48
78
  let outputSchema: object | undefined;
79
+ let dataSchema: object | undefined;
80
+ let metaSchema: object | undefined;
49
81
  let errorSchema: object | undefined;
50
82
 
51
83
  try {
52
84
  const schemaContent = await fs.readFile(schemaFile, 'utf-8');
53
85
  const schema = JSON.parse(schemaContent);
54
86
  inputSchema = schema.input;
55
- outputSchema = schema.output;
87
+ // Support both "data" (v2.2) and "output" (v2.1) aliases
88
+ dataSchema = schema.data || schema.output;
89
+ outputSchema = dataSchema; // Backward compat
90
+ metaSchema = schema.meta;
56
91
  errorSchema = schema.error;
57
92
  } catch {
58
93
  // Schema file is optional but recommended
59
94
  }
60
95
 
96
+ // Extract v2.2 fields
97
+ const tier = manifest.tier as ModuleTier | undefined;
98
+ const schemaStrictness = (manifest.schema_strictness as SchemaStrictness) || 'medium';
99
+
100
+ // Determine default max_items based on strictness (SPEC-v2.2)
101
+ const strictnessMaxItems: Record<SchemaStrictness, number> = {
102
+ high: 0,
103
+ medium: 5,
104
+ low: 20
105
+ };
106
+ const defaultMaxItems = strictnessMaxItems[schemaStrictness] ?? 5;
107
+ const defaultEnabled = schemaStrictness !== 'high';
108
+
109
+ // Parse overflow config with strictness-based defaults
110
+ const overflowRaw = (manifest.overflow as Record<string, unknown>) || {};
111
+ const overflow: OverflowConfig = {
112
+ enabled: (overflowRaw.enabled as boolean) ?? defaultEnabled,
113
+ recoverable: (overflowRaw.recoverable as boolean) ?? true,
114
+ max_items: (overflowRaw.max_items as number) ?? defaultMaxItems,
115
+ require_suggested_mapping: (overflowRaw.require_suggested_mapping as boolean) ?? true
116
+ };
117
+
118
+ // Parse enums config
119
+ const enumsRaw = (manifest.enums as Record<string, unknown>) || {};
120
+ const enums: EnumConfig = {
121
+ strategy: (enumsRaw.strategy as 'strict' | 'extensible') ??
122
+ (tier === 'exec' ? 'strict' : 'extensible'),
123
+ unknown_tag: (enumsRaw.unknown_tag as string) ?? 'custom'
124
+ };
125
+
126
+ // Parse compat config
127
+ const compatRaw = (manifest.compat as Record<string, unknown>) || {};
128
+ const compat: CompatConfig = {
129
+ accepts_v21_payload: (compatRaw.accepts_v21_payload as boolean) ?? true,
130
+ runtime_auto_wrap: (compatRaw.runtime_auto_wrap as boolean) ?? true,
131
+ schema_output_alias: (compatRaw.schema_output_alias as 'data' | 'output') ?? 'data'
132
+ };
133
+
134
+ // Parse meta config (including risk_rule)
135
+ const metaRaw = (manifest.meta as Record<string, unknown>) || {};
136
+ const metaConfig: MetaConfig = {
137
+ required: metaRaw.required as string[] | undefined,
138
+ risk_rule: metaRaw.risk_rule as 'max_changes_risk' | 'max_issues_risk' | 'explicit' | undefined,
139
+ };
140
+
61
141
  return {
62
142
  name: manifest.name as string || path.basename(modulePath),
63
143
  version: manifest.version as string || '1.0.0',
@@ -69,13 +149,26 @@ async function loadModuleV2(modulePath: string): Promise<CognitiveModule> {
69
149
  output: manifest.output as OutputContract | undefined,
70
150
  failure: manifest.failure as FailureContract | undefined,
71
151
  runtimeRequirements: manifest.runtime_requirements as RuntimeRequirements | undefined,
152
+ // v2.2 fields
153
+ tier,
154
+ schemaStrictness,
155
+ overflow,
156
+ enums,
157
+ compat,
158
+ metaConfig,
159
+ // Context and prompt
72
160
  context: manifest.context as 'fork' | 'main' | undefined,
73
161
  prompt,
162
+ // Schemas
74
163
  inputSchema,
75
164
  outputSchema,
165
+ dataSchema,
166
+ metaSchema,
76
167
  errorSchema,
168
+ // Metadata
77
169
  location: modulePath,
78
170
  format: 'v2',
171
+ formatVersion,
79
172
  };
80
173
  }
81
174
 
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Module Runner - Execute Cognitive Modules
3
- * v2.1: Envelope format support, clean input mapping
3
+ * v2.2: Envelope format with meta/data separation, risk_rule, repair pass
4
4
  */
5
5
 
6
6
  import type {
@@ -8,11 +8,17 @@ import type {
8
8
  CognitiveModule,
9
9
  ModuleResult,
10
10
  ModuleResultV21,
11
+ ModuleResultV22,
11
12
  Message,
12
13
  ModuleInput,
13
14
  EnvelopeResponse,
14
- ModuleResultData
15
+ EnvelopeResponseV22,
16
+ EnvelopeMeta,
17
+ ModuleResultData,
18
+ RiskLevel,
19
+ RiskRule
15
20
  } from '../types.js';
21
+ import { aggregateRisk, isV22Envelope } from '../types.js';
16
22
 
17
23
  export interface RunOptions {
18
24
  // Clean input (v2 style)
@@ -26,6 +32,130 @@ export interface RunOptions {
26
32
 
27
33
  // Force envelope format (default: auto-detect from module.output.envelope)
28
34
  useEnvelope?: boolean;
35
+
36
+ // Force v2.2 format (default: auto-detect from module.tier)
37
+ useV22?: boolean;
38
+
39
+ // Enable repair pass for validation failures (default: true)
40
+ enableRepair?: boolean;
41
+ }
42
+
43
+ // =============================================================================
44
+ // Repair Pass (v2.2)
45
+ // =============================================================================
46
+
47
+ /**
48
+ * Attempt to repair envelope format issues without changing semantics.
49
+ *
50
+ * Repairs (lossless only):
51
+ * - Missing meta fields (fill with conservative defaults)
52
+ * - Truncate explain if too long
53
+ * - Trim whitespace from string fields
54
+ *
55
+ * Does NOT repair:
56
+ * - Invalid enum values (treated as validation failure)
57
+ */
58
+ function repairEnvelope(
59
+ response: Record<string, unknown>,
60
+ riskRule: RiskRule = 'max_changes_risk',
61
+ maxExplainLength: number = 280
62
+ ): EnvelopeResponseV22<unknown> {
63
+ const repaired = { ...response };
64
+
65
+ // Ensure meta exists
66
+ if (!repaired.meta || typeof repaired.meta !== 'object') {
67
+ repaired.meta = {};
68
+ }
69
+
70
+ const meta = repaired.meta as Record<string, unknown>;
71
+ const data = (repaired.data ?? {}) as Record<string, unknown>;
72
+
73
+ // Repair confidence
74
+ if (typeof meta.confidence !== 'number') {
75
+ meta.confidence = (data.confidence as number) ?? 0.5;
76
+ }
77
+ meta.confidence = Math.max(0, Math.min(1, meta.confidence as number));
78
+
79
+ // Repair risk using configurable aggregation rule
80
+ if (!meta.risk) {
81
+ meta.risk = aggregateRisk(data, riskRule);
82
+ }
83
+ // Trim whitespace only (lossless), do NOT invent new values
84
+ if (typeof meta.risk === 'string') {
85
+ meta.risk = meta.risk.trim().toLowerCase();
86
+ }
87
+
88
+ // Repair explain
89
+ if (typeof meta.explain !== 'string') {
90
+ const rationale = data.rationale as string | undefined;
91
+ meta.explain = rationale ? String(rationale).slice(0, maxExplainLength) : 'No explanation provided';
92
+ }
93
+ // Trim whitespace (lossless)
94
+ const explainStr = meta.explain as string;
95
+ meta.explain = explainStr.trim();
96
+ if ((meta.explain as string).length > maxExplainLength) {
97
+ meta.explain = (meta.explain as string).slice(0, maxExplainLength - 3) + '...';
98
+ }
99
+
100
+ // Build proper v2.2 response
101
+ const builtMeta: EnvelopeMeta = {
102
+ confidence: meta.confidence as number,
103
+ risk: meta.risk as RiskLevel,
104
+ explain: meta.explain as string
105
+ };
106
+
107
+ const result: EnvelopeResponseV22<unknown> = repaired.ok === false ? {
108
+ ok: false,
109
+ meta: builtMeta,
110
+ error: (repaired.error as { code: string; message: string }) ?? { code: 'UNKNOWN', message: 'Unknown error' },
111
+ partial_data: repaired.partial_data
112
+ } : {
113
+ ok: true,
114
+ meta: builtMeta,
115
+ data: repaired.data
116
+ };
117
+
118
+ return result;
119
+ }
120
+
121
+ /**
122
+ * Wrap v2.1 response to v2.2 format
123
+ */
124
+ function wrapV21ToV22(
125
+ response: EnvelopeResponse<unknown>,
126
+ riskRule: RiskRule = 'max_changes_risk'
127
+ ): EnvelopeResponseV22<unknown> {
128
+ if (isV22Envelope(response)) {
129
+ return response;
130
+ }
131
+
132
+ if (response.ok) {
133
+ const data = (response.data ?? {}) as Record<string, unknown>;
134
+ const confidence = (data.confidence as number) ?? 0.5;
135
+ const rationale = (data.rationale as string) ?? '';
136
+
137
+ return {
138
+ ok: true,
139
+ meta: {
140
+ confidence,
141
+ risk: aggregateRisk(data, riskRule),
142
+ explain: rationale.slice(0, 280) || 'No explanation provided'
143
+ },
144
+ data: data as ModuleResultData
145
+ };
146
+ } else {
147
+ const errorMsg = response.error?.message ?? 'Unknown error';
148
+ return {
149
+ ok: false,
150
+ meta: {
151
+ confidence: 0,
152
+ risk: 'high',
153
+ explain: errorMsg.slice(0, 280)
154
+ },
155
+ error: response.error ?? { code: 'UNKNOWN', message: errorMsg },
156
+ partial_data: response.partial_data
157
+ };
158
+ }
29
159
  }
30
160
 
31
161
  export async function runModule(
@@ -33,10 +163,17 @@ export async function runModule(
33
163
  provider: Provider,
34
164
  options: RunOptions = {}
35
165
  ): Promise<ModuleResult> {
36
- const { args, input, verbose = false, useEnvelope } = options;
166
+ const { args, input, verbose = false, useEnvelope, useV22, enableRepair = true } = options;
37
167
 
38
168
  // Determine if we should use envelope format
39
169
  const shouldUseEnvelope = useEnvelope ?? (module.output?.envelope === true || module.format === 'v2');
170
+
171
+ // Determine if we should use v2.2 format
172
+ const isV22Module = module.tier !== undefined || module.formatVersion === 'v2.2';
173
+ const shouldUseV22 = useV22 ?? (isV22Module || module.compat?.runtime_auto_wrap === true);
174
+
175
+ // Get risk_rule from module config
176
+ const riskRule: RiskRule = module.metaConfig?.risk_rule ?? 'max_changes_risk';
40
177
 
41
178
  // Build clean input data (v2 style: no $ARGUMENTS pollution)
42
179
  const inputData: ModuleInput = input || {};
@@ -97,11 +234,20 @@ export async function runModule(
97
234
 
98
235
  // Add envelope format instructions
99
236
  if (shouldUseEnvelope) {
100
- systemParts.push('', 'RESPONSE FORMAT (Envelope):');
101
- systemParts.push('- Wrap your response in the envelope format');
102
- systemParts.push('- Success: { "ok": true, "data": { ...your output... } }');
103
- systemParts.push('- Error: { "ok": false, "error": { "code": "ERROR_CODE", "message": "..." } }');
104
- systemParts.push('- Include "confidence" (0-1) and "rationale" in data');
237
+ if (shouldUseV22) {
238
+ systemParts.push('', 'RESPONSE FORMAT (Envelope v2.2):');
239
+ systemParts.push('- Wrap your response in the v2.2 envelope format with separate meta and data');
240
+ systemParts.push('- Success: { "ok": true, "meta": { "confidence": 0.9, "risk": "low", "explain": "short summary" }, "data": { ...payload... } }');
241
+ systemParts.push('- Error: { "ok": false, "meta": { "confidence": 0.0, "risk": "high", "explain": "error summary" }, "error": { "code": "ERROR_CODE", "message": "..." } }');
242
+ systemParts.push('- meta.explain must be ≤280 characters. data.rationale can be longer for detailed reasoning.');
243
+ systemParts.push('- meta.risk must be one of: "none", "low", "medium", "high"');
244
+ } else {
245
+ systemParts.push('', 'RESPONSE FORMAT (Envelope):');
246
+ systemParts.push('- Wrap your response in the envelope format');
247
+ systemParts.push('- Success: { "ok": true, "data": { ...your output... } }');
248
+ systemParts.push('- Error: { "ok": false, "error": { "code": "ERROR_CODE", "message": "..." } }');
249
+ systemParts.push('- Include "confidence" (0-1) and "rationale" in data');
250
+ }
105
251
  if (module.output?.require_behavior_equivalence) {
106
252
  systemParts.push('- Include "behavior_equivalence" (boolean) in data');
107
253
  }
@@ -144,11 +290,55 @@ export async function runModule(
144
290
 
145
291
  // Handle envelope format
146
292
  if (shouldUseEnvelope && isEnvelopeResponse(parsed)) {
147
- return parseEnvelopeResponse(parsed, result.content);
293
+ let response = parseEnvelopeResponse(parsed, result.content);
294
+
295
+ // Upgrade to v2.2 if needed
296
+ if (shouldUseV22 && response.ok && !('meta' in response && response.meta)) {
297
+ const upgraded = wrapV21ToV22(parsed as EnvelopeResponse<unknown>, riskRule);
298
+ response = {
299
+ ok: true,
300
+ meta: upgraded.meta as EnvelopeMeta,
301
+ data: (upgraded as { data?: ModuleResultData }).data,
302
+ raw: result.content
303
+ } as ModuleResultV22;
304
+ }
305
+
306
+ // Apply repair pass if enabled and response needs it
307
+ if (enableRepair && response.ok && shouldUseV22) {
308
+ const repaired = repairEnvelope(
309
+ response as unknown as Record<string, unknown>,
310
+ riskRule
311
+ );
312
+ response = {
313
+ ok: true,
314
+ meta: repaired.meta as EnvelopeMeta,
315
+ data: (repaired as { data?: ModuleResultData }).data,
316
+ raw: result.content
317
+ } as ModuleResultV22;
318
+ }
319
+
320
+ return response;
148
321
  }
149
322
 
150
323
  // Handle legacy format (non-envelope)
151
- return parseLegacyResponse(parsed, result.content);
324
+ const legacyResult = parseLegacyResponse(parsed, result.content);
325
+
326
+ // Upgrade to v2.2 if requested
327
+ if (shouldUseV22 && legacyResult.ok) {
328
+ const data = (legacyResult.data ?? {}) as Record<string, unknown>;
329
+ return {
330
+ ok: true,
331
+ meta: {
332
+ confidence: (data.confidence as number) ?? 0.5,
333
+ risk: aggregateRisk(data, riskRule),
334
+ explain: ((data.rationale as string) ?? '').slice(0, 280) || 'No explanation provided'
335
+ },
336
+ data: legacyResult.data,
337
+ raw: result.content
338
+ } as ModuleResultV22;
339
+ }
340
+
341
+ return legacyResult;
152
342
  }
153
343
 
154
344
  /**
@@ -161,11 +351,32 @@ function isEnvelopeResponse(obj: unknown): obj is EnvelopeResponse {
161
351
  }
162
352
 
163
353
  /**
164
- * Parse envelope format response
354
+ * Parse envelope format response (supports both v2.1 and v2.2)
165
355
  */
166
- function parseEnvelopeResponse(response: EnvelopeResponse, raw: string): ModuleResult {
356
+ function parseEnvelopeResponse(response: EnvelopeResponse<unknown>, raw: string): ModuleResult {
357
+ // Check if v2.2 format (has meta)
358
+ if (isV22Envelope(response)) {
359
+ if (response.ok) {
360
+ return {
361
+ ok: true,
362
+ meta: response.meta,
363
+ data: response.data as ModuleResultData,
364
+ raw,
365
+ } as ModuleResultV22;
366
+ } else {
367
+ return {
368
+ ok: false,
369
+ meta: response.meta,
370
+ error: response.error,
371
+ partial_data: response.partial_data,
372
+ raw,
373
+ } as ModuleResultV22;
374
+ }
375
+ }
376
+
377
+ // v2.1 format
167
378
  if (response.ok) {
168
- const data = response.data as ModuleResultData;
379
+ const data = (response.data ?? {}) as ModuleResultData & { confidence?: number };
169
380
  return {
170
381
  ok: true,
171
382
  data: {
@@ -175,14 +386,14 @@ function parseEnvelopeResponse(response: EnvelopeResponse, raw: string): ModuleR
175
386
  behavior_equivalence: data.behavior_equivalence,
176
387
  },
177
388
  raw,
178
- };
389
+ } as ModuleResultV21;
179
390
  } else {
180
391
  return {
181
392
  ok: false,
182
393
  error: response.error,
183
394
  partial_data: response.partial_data,
184
395
  raw,
185
- };
396
+ } as ModuleResultV21;
186
397
  }
187
398
  }
188
399
 
package/src/types.ts CHANGED
@@ -50,6 +50,9 @@ export type RiskLevel = 'none' | 'low' | 'medium' | 'high';
50
50
  /** Enum extension strategy */
51
51
  export type EnumStrategy = 'strict' | 'extensible';
52
52
 
53
+ /** Risk aggregation rule */
54
+ export type RiskRule = 'max_changes_risk' | 'max_issues_risk' | 'explicit';
55
+
53
56
  // =============================================================================
54
57
  // Module Configuration (v2.2)
55
58
  // =============================================================================
@@ -94,6 +97,9 @@ export interface CognitiveModule {
94
97
  // v2.2: Compatibility configuration
95
98
  compat?: CompatConfig;
96
99
 
100
+ // v2.2: Meta configuration (including risk_rule)
101
+ metaConfig?: MetaConfig;
102
+
97
103
  // Execution context
98
104
  context?: 'fork' | 'main';
99
105
 
@@ -181,6 +187,14 @@ export interface CompatConfig {
181
187
  schema_output_alias?: 'data' | 'output';
182
188
  }
183
189
 
190
+ /** Meta field configuration (v2.2) */
191
+ export interface MetaConfig {
192
+ required?: string[];
193
+ risk_rule?: RiskRule;
194
+ confidence?: { min?: number; max?: number };
195
+ explain?: { max_chars?: number };
196
+ }
197
+
184
198
  // =============================================================================
185
199
  // Envelope Types (v2.2)
186
200
  // =============================================================================
@@ -386,18 +400,21 @@ export function isEnvelopeSuccess<T>(
386
400
  }
387
401
 
388
402
  /** Extract meta from any envelope response */
389
- export function extractMeta<T>(response: EnvelopeResponse<T>): EnvelopeMeta {
403
+ export function extractMeta<T>(
404
+ response: EnvelopeResponse<T>,
405
+ riskRule: RiskRule = 'max_changes_risk'
406
+ ): EnvelopeMeta {
390
407
  if (isV22Envelope(response)) {
391
408
  return response.meta;
392
409
  }
393
410
 
394
411
  // Synthesize meta from v2.1 response
395
412
  if (response.ok) {
396
- const data = response.data as Record<string, unknown>;
413
+ const data = (response.data ?? {}) as Record<string, unknown>;
397
414
  return {
398
- confidence: (data?.confidence as number) ?? 0.5,
399
- risk: 'medium',
400
- explain: ((data?.rationale as string) ?? '').slice(0, 280) || 'No explanation',
415
+ confidence: (data.confidence as number) ?? 0.5,
416
+ risk: aggregateRisk(data, riskRule),
417
+ explain: ((data.rationale as string) ?? '').slice(0, 280) || 'No explanation',
401
418
  };
402
419
  } else {
403
420
  return {
@@ -408,24 +425,50 @@ export function extractMeta<T>(response: EnvelopeResponse<T>): EnvelopeMeta {
408
425
  }
409
426
  }
410
427
 
411
- /** Aggregate risk from list of changes */
412
- export function aggregateRisk(changes: Array<{ risk?: RiskLevel }>): RiskLevel {
428
+ /** Aggregate risk from list of items */
429
+ function aggregateRiskFromList(items: Array<{ risk?: RiskLevel }>): RiskLevel {
413
430
  const riskLevels: Record<RiskLevel, number> = { none: 0, low: 1, medium: 2, high: 3 };
414
431
  const riskNames: RiskLevel[] = ['none', 'low', 'medium', 'high'];
415
432
 
416
- if (!changes || changes.length === 0) {
433
+ if (!items || items.length === 0) {
417
434
  return 'medium';
418
435
  }
419
436
 
420
437
  let maxLevel = 0;
421
- for (const change of changes) {
422
- const level = riskLevels[change.risk ?? 'medium'];
438
+ for (const item of items) {
439
+ const level = riskLevels[item.risk ?? 'medium'];
423
440
  maxLevel = Math.max(maxLevel, level);
424
441
  }
425
442
 
426
443
  return riskNames[maxLevel];
427
444
  }
428
445
 
446
+ /**
447
+ * Aggregate risk based on configured rule.
448
+ *
449
+ * Rules:
450
+ * - max_changes_risk: max(data.changes[*].risk) - default
451
+ * - max_issues_risk: max(data.issues[*].risk) - for review modules
452
+ * - explicit: return "medium", module should set risk explicitly
453
+ */
454
+ export function aggregateRisk(
455
+ data: Record<string, unknown>,
456
+ riskRule: RiskRule = 'max_changes_risk'
457
+ ): RiskLevel {
458
+ if (riskRule === 'max_changes_risk') {
459
+ const changes = (data.changes as Array<{ risk?: RiskLevel }>) ?? [];
460
+ return aggregateRiskFromList(changes);
461
+ } else if (riskRule === 'max_issues_risk') {
462
+ const issues = (data.issues as Array<{ risk?: RiskLevel }>) ?? [];
463
+ return aggregateRiskFromList(issues);
464
+ } else if (riskRule === 'explicit') {
465
+ return 'medium'; // Module should override
466
+ }
467
+ // Fallback to changes
468
+ const changes = (data.changes as Array<{ risk?: RiskLevel }>) ?? [];
469
+ return aggregateRiskFromList(changes);
470
+ }
471
+
429
472
  /** Check if result should be escalated to human review */
430
473
  export function shouldEscalate<T>(
431
474
  response: EnvelopeResponse<T>,