cognitive-modules 0.4.0__py3-none-any.whl → 0.5.0__py3-none-any.whl
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.
- cognitive/__init__.py +1 -1
- cognitive/cli.py +173 -18
- cognitive/loader.py +180 -14
- cognitive/mcp_server.py +245 -0
- cognitive/migrate.py +624 -0
- cognitive/runner.py +409 -80
- cognitive/server.py +294 -0
- cognitive/validator.py +380 -122
- {cognitive_modules-0.4.0.dist-info → cognitive_modules-0.5.0.dist-info}/METADATA +179 -176
- cognitive_modules-0.5.0.dist-info/RECORD +18 -0
- cognitive_modules-0.4.0.dist-info/RECORD +0 -15
- {cognitive_modules-0.4.0.dist-info → cognitive_modules-0.5.0.dist-info}/WHEEL +0 -0
- {cognitive_modules-0.4.0.dist-info → cognitive_modules-0.5.0.dist-info}/entry_points.txt +0 -0
- {cognitive_modules-0.4.0.dist-info → cognitive_modules-0.5.0.dist-info}/licenses/LICENSE +0 -0
- {cognitive_modules-0.4.0.dist-info → cognitive_modules-0.5.0.dist-info}/top_level.txt +0 -0
cognitive/migrate.py
ADDED
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module Migration Tool - Migrate v1/v2.1 modules to v2.2 format.
|
|
3
|
+
|
|
4
|
+
Migration includes:
|
|
5
|
+
- Creating module.yaml with tier, overflow, enums, compat
|
|
6
|
+
- Updating schema.json with meta schema
|
|
7
|
+
- Creating/updating prompt.md with v2.2 envelope instructions
|
|
8
|
+
- Preserving MODULE.md for backward compatibility
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import shutil
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Optional
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
|
|
17
|
+
import yaml
|
|
18
|
+
|
|
19
|
+
from .registry import find_module, list_modules
|
|
20
|
+
from .loader import detect_format, parse_frontmatter
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# =============================================================================
|
|
24
|
+
# Migration Entry Points
|
|
25
|
+
# =============================================================================
|
|
26
|
+
|
|
27
|
+
def migrate_module(
|
|
28
|
+
name_or_path: str,
|
|
29
|
+
dry_run: bool = False,
|
|
30
|
+
backup: bool = True
|
|
31
|
+
) -> tuple[bool, list[str], list[str]]:
|
|
32
|
+
"""
|
|
33
|
+
Migrate a single module to v2.2 format.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
name_or_path: Module name or path
|
|
37
|
+
dry_run: If True, only show what would be done
|
|
38
|
+
backup: If True, create backup before migration
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Tuple of (success, changes, warnings)
|
|
42
|
+
"""
|
|
43
|
+
changes = []
|
|
44
|
+
warnings = []
|
|
45
|
+
|
|
46
|
+
# Find module
|
|
47
|
+
path = Path(name_or_path)
|
|
48
|
+
if path.exists() and path.is_dir():
|
|
49
|
+
module_path = path
|
|
50
|
+
else:
|
|
51
|
+
module_path = find_module(name_or_path)
|
|
52
|
+
if not module_path:
|
|
53
|
+
return False, [], [f"Module not found: {name_or_path}"]
|
|
54
|
+
|
|
55
|
+
# Detect current format
|
|
56
|
+
try:
|
|
57
|
+
fmt = detect_format(module_path)
|
|
58
|
+
except FileNotFoundError as e:
|
|
59
|
+
return False, [], [str(e)]
|
|
60
|
+
|
|
61
|
+
# Check if already v2.2
|
|
62
|
+
if fmt == "v2":
|
|
63
|
+
module_yaml_path = module_path / "module.yaml"
|
|
64
|
+
if module_yaml_path.exists():
|
|
65
|
+
with open(module_yaml_path, 'r', encoding='utf-8') as f:
|
|
66
|
+
manifest = yaml.safe_load(f)
|
|
67
|
+
if manifest.get('tier') is not None:
|
|
68
|
+
warnings.append("Module appears to already be v2.2 format")
|
|
69
|
+
return True, [], warnings
|
|
70
|
+
|
|
71
|
+
# Create backup
|
|
72
|
+
if backup and not dry_run:
|
|
73
|
+
backup_path = _create_backup(module_path)
|
|
74
|
+
changes.append(f"Created backup: {backup_path}")
|
|
75
|
+
|
|
76
|
+
# Perform migration based on format
|
|
77
|
+
if fmt == "v0":
|
|
78
|
+
return _migrate_from_v0(module_path, dry_run, changes, warnings)
|
|
79
|
+
elif fmt == "v1":
|
|
80
|
+
return _migrate_from_v1(module_path, dry_run, changes, warnings)
|
|
81
|
+
elif fmt == "v2":
|
|
82
|
+
return _migrate_from_v2(module_path, dry_run, changes, warnings)
|
|
83
|
+
else:
|
|
84
|
+
return False, [], [f"Unknown format: {fmt}"]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def migrate_all_modules(
|
|
88
|
+
dry_run: bool = False,
|
|
89
|
+
backup: bool = True
|
|
90
|
+
) -> list[tuple[str, bool, list[str], list[str]]]:
|
|
91
|
+
"""
|
|
92
|
+
Migrate all installed modules to v2.2 format.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
List of (module_name, success, changes, warnings) tuples
|
|
96
|
+
"""
|
|
97
|
+
results = []
|
|
98
|
+
modules = list_modules()
|
|
99
|
+
|
|
100
|
+
for module in modules:
|
|
101
|
+
name = module.get('name', 'unknown')
|
|
102
|
+
path = module.get('path')
|
|
103
|
+
|
|
104
|
+
if path:
|
|
105
|
+
success, changes, warnings = migrate_module(
|
|
106
|
+
str(path),
|
|
107
|
+
dry_run=dry_run,
|
|
108
|
+
backup=backup
|
|
109
|
+
)
|
|
110
|
+
results.append((name, success, changes, warnings))
|
|
111
|
+
|
|
112
|
+
return results
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# =============================================================================
|
|
116
|
+
# Format-Specific Migration
|
|
117
|
+
# =============================================================================
|
|
118
|
+
|
|
119
|
+
def _migrate_from_v0(
|
|
120
|
+
module_path: Path,
|
|
121
|
+
dry_run: bool,
|
|
122
|
+
changes: list[str],
|
|
123
|
+
warnings: list[str]
|
|
124
|
+
) -> tuple[bool, list[str], list[str]]:
|
|
125
|
+
"""Migrate from v0 (6-file) format."""
|
|
126
|
+
warnings.append("v0 format migration requires manual review")
|
|
127
|
+
|
|
128
|
+
# Load existing data
|
|
129
|
+
module_md_path = module_path / "module.md"
|
|
130
|
+
with open(module_md_path, 'r', encoding='utf-8') as f:
|
|
131
|
+
content = f.read()
|
|
132
|
+
|
|
133
|
+
frontmatter, _ = parse_frontmatter(content)
|
|
134
|
+
|
|
135
|
+
# Load prompt
|
|
136
|
+
prompt_txt_path = module_path / "prompt.txt"
|
|
137
|
+
with open(prompt_txt_path, 'r', encoding='utf-8') as f:
|
|
138
|
+
prompt = f.read()
|
|
139
|
+
|
|
140
|
+
# Load schemas
|
|
141
|
+
with open(module_path / "input.schema.json", 'r', encoding='utf-8') as f:
|
|
142
|
+
input_schema = json.load(f)
|
|
143
|
+
with open(module_path / "output.schema.json", 'r', encoding='utf-8') as f:
|
|
144
|
+
output_schema = json.load(f)
|
|
145
|
+
|
|
146
|
+
# Create module.yaml
|
|
147
|
+
manifest = _create_v22_manifest(frontmatter)
|
|
148
|
+
|
|
149
|
+
# Create combined schema.json
|
|
150
|
+
schema = _create_v22_schema(input_schema, output_schema)
|
|
151
|
+
|
|
152
|
+
# Create prompt.md
|
|
153
|
+
prompt_md = _create_v22_prompt(frontmatter, prompt)
|
|
154
|
+
|
|
155
|
+
if dry_run:
|
|
156
|
+
changes.append("[DRY RUN] Would create module.yaml")
|
|
157
|
+
changes.append("[DRY RUN] Would create schema.json (combined)")
|
|
158
|
+
changes.append("[DRY RUN] Would create prompt.md")
|
|
159
|
+
else:
|
|
160
|
+
# Write files
|
|
161
|
+
_write_yaml(module_path / "module.yaml", manifest)
|
|
162
|
+
changes.append("Created module.yaml")
|
|
163
|
+
|
|
164
|
+
_write_json(module_path / "schema.json", schema)
|
|
165
|
+
changes.append("Created schema.json (combined)")
|
|
166
|
+
|
|
167
|
+
_write_text(module_path / "prompt.md", prompt_md)
|
|
168
|
+
changes.append("Created prompt.md")
|
|
169
|
+
|
|
170
|
+
return True, changes, warnings
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _migrate_from_v1(
|
|
174
|
+
module_path: Path,
|
|
175
|
+
dry_run: bool,
|
|
176
|
+
changes: list[str],
|
|
177
|
+
warnings: list[str]
|
|
178
|
+
) -> tuple[bool, list[str], list[str]]:
|
|
179
|
+
"""Migrate from v1 (MODULE.md + schema.json) format."""
|
|
180
|
+
# Load MODULE.md
|
|
181
|
+
module_md_path = module_path / "MODULE.md"
|
|
182
|
+
with open(module_md_path, 'r', encoding='utf-8') as f:
|
|
183
|
+
content = f.read()
|
|
184
|
+
|
|
185
|
+
frontmatter, prompt_body = parse_frontmatter(content)
|
|
186
|
+
|
|
187
|
+
# Load schema.json if exists
|
|
188
|
+
schema_path = module_path / "schema.json"
|
|
189
|
+
if schema_path.exists():
|
|
190
|
+
with open(schema_path, 'r', encoding='utf-8') as f:
|
|
191
|
+
schema = json.load(f)
|
|
192
|
+
input_schema = schema.get('input', {})
|
|
193
|
+
output_schema = schema.get('output', {})
|
|
194
|
+
else:
|
|
195
|
+
input_schema = {}
|
|
196
|
+
output_schema = {}
|
|
197
|
+
|
|
198
|
+
# Create module.yaml
|
|
199
|
+
manifest = _create_v22_manifest(frontmatter)
|
|
200
|
+
|
|
201
|
+
# Create/update schema.json with meta
|
|
202
|
+
new_schema = _create_v22_schema(input_schema, output_schema)
|
|
203
|
+
|
|
204
|
+
# Create prompt.md
|
|
205
|
+
prompt_md = _create_v22_prompt(frontmatter, prompt_body)
|
|
206
|
+
|
|
207
|
+
if dry_run:
|
|
208
|
+
changes.append("[DRY RUN] Would create module.yaml")
|
|
209
|
+
changes.append("[DRY RUN] Would update schema.json (add meta)")
|
|
210
|
+
changes.append("[DRY RUN] Would create prompt.md")
|
|
211
|
+
else:
|
|
212
|
+
# Write files
|
|
213
|
+
_write_yaml(module_path / "module.yaml", manifest)
|
|
214
|
+
changes.append("Created module.yaml")
|
|
215
|
+
|
|
216
|
+
_write_json(module_path / "schema.json", new_schema)
|
|
217
|
+
changes.append("Updated schema.json (added meta)")
|
|
218
|
+
|
|
219
|
+
_write_text(module_path / "prompt.md", prompt_md)
|
|
220
|
+
changes.append("Created prompt.md")
|
|
221
|
+
|
|
222
|
+
# Keep MODULE.md for compatibility
|
|
223
|
+
changes.append("Preserved MODULE.md (backward compatibility)")
|
|
224
|
+
|
|
225
|
+
return True, changes, warnings
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _migrate_from_v2(
|
|
229
|
+
module_path: Path,
|
|
230
|
+
dry_run: bool,
|
|
231
|
+
changes: list[str],
|
|
232
|
+
warnings: list[str]
|
|
233
|
+
) -> tuple[bool, list[str], list[str]]:
|
|
234
|
+
"""Migrate from v2.0/v2.1 to v2.2 format."""
|
|
235
|
+
# Load module.yaml
|
|
236
|
+
module_yaml_path = module_path / "module.yaml"
|
|
237
|
+
with open(module_yaml_path, 'r', encoding='utf-8') as f:
|
|
238
|
+
manifest = yaml.safe_load(f)
|
|
239
|
+
|
|
240
|
+
# Load schema.json
|
|
241
|
+
schema_path = module_path / "schema.json"
|
|
242
|
+
if schema_path.exists():
|
|
243
|
+
with open(schema_path, 'r', encoding='utf-8') as f:
|
|
244
|
+
schema = json.load(f)
|
|
245
|
+
else:
|
|
246
|
+
schema = {}
|
|
247
|
+
|
|
248
|
+
# Load prompt.md
|
|
249
|
+
prompt_path = module_path / "prompt.md"
|
|
250
|
+
if prompt_path.exists():
|
|
251
|
+
with open(prompt_path, 'r', encoding='utf-8') as f:
|
|
252
|
+
prompt = f.read()
|
|
253
|
+
else:
|
|
254
|
+
prompt = ""
|
|
255
|
+
|
|
256
|
+
# Upgrade manifest to v2.2
|
|
257
|
+
manifest_changes = []
|
|
258
|
+
|
|
259
|
+
if 'tier' not in manifest:
|
|
260
|
+
manifest['tier'] = 'decision' # Safe default
|
|
261
|
+
manifest_changes.append("Added tier: decision")
|
|
262
|
+
|
|
263
|
+
if 'schema_strictness' not in manifest:
|
|
264
|
+
manifest['schema_strictness'] = 'medium'
|
|
265
|
+
manifest_changes.append("Added schema_strictness: medium")
|
|
266
|
+
|
|
267
|
+
if 'overflow' not in manifest:
|
|
268
|
+
manifest['overflow'] = {
|
|
269
|
+
'enabled': True,
|
|
270
|
+
'recoverable': True,
|
|
271
|
+
'max_items': 5,
|
|
272
|
+
'require_suggested_mapping': True
|
|
273
|
+
}
|
|
274
|
+
manifest_changes.append("Added overflow config")
|
|
275
|
+
|
|
276
|
+
if 'enums' not in manifest:
|
|
277
|
+
manifest['enums'] = {
|
|
278
|
+
'strategy': 'extensible'
|
|
279
|
+
}
|
|
280
|
+
manifest_changes.append("Added enums config")
|
|
281
|
+
|
|
282
|
+
if 'compat' not in manifest:
|
|
283
|
+
manifest['compat'] = {
|
|
284
|
+
'accepts_v21_payload': True,
|
|
285
|
+
'runtime_auto_wrap': True,
|
|
286
|
+
'schema_output_alias': 'data'
|
|
287
|
+
}
|
|
288
|
+
manifest_changes.append("Added compat config")
|
|
289
|
+
|
|
290
|
+
# Update IO references
|
|
291
|
+
if 'io' not in manifest:
|
|
292
|
+
manifest['io'] = {
|
|
293
|
+
'input': './schema.json#/input',
|
|
294
|
+
'data': './schema.json#/data',
|
|
295
|
+
'meta': './schema.json#/meta',
|
|
296
|
+
'error': './schema.json#/error'
|
|
297
|
+
}
|
|
298
|
+
manifest_changes.append("Added io references")
|
|
299
|
+
|
|
300
|
+
# Upgrade schema to v2.2
|
|
301
|
+
schema_changes = []
|
|
302
|
+
|
|
303
|
+
if 'meta' not in schema:
|
|
304
|
+
schema['meta'] = _create_meta_schema()
|
|
305
|
+
schema_changes.append("Added meta schema")
|
|
306
|
+
|
|
307
|
+
# Rename output to data if needed
|
|
308
|
+
if 'output' in schema and 'data' not in schema:
|
|
309
|
+
schema['data'] = schema.pop('output')
|
|
310
|
+
schema_changes.append("Renamed output to data")
|
|
311
|
+
|
|
312
|
+
# Add rationale requirement if missing
|
|
313
|
+
if 'data' in schema:
|
|
314
|
+
data_required = schema['data'].get('required', [])
|
|
315
|
+
if 'rationale' not in data_required:
|
|
316
|
+
data_required.append('rationale')
|
|
317
|
+
schema['data']['required'] = data_required
|
|
318
|
+
schema_changes.append("Added rationale to data.required")
|
|
319
|
+
|
|
320
|
+
# Add extensions if overflow enabled
|
|
321
|
+
if manifest.get('overflow', {}).get('enabled') and '$defs' not in schema:
|
|
322
|
+
schema['$defs'] = {
|
|
323
|
+
'extensions': _create_extensions_schema()
|
|
324
|
+
}
|
|
325
|
+
schema_changes.append("Added $defs.extensions")
|
|
326
|
+
|
|
327
|
+
# Update prompt if needed
|
|
328
|
+
prompt_changes = []
|
|
329
|
+
if 'meta' not in prompt.lower() or 'envelope' not in prompt.lower():
|
|
330
|
+
prompt = _add_v22_instructions_to_prompt(prompt, manifest)
|
|
331
|
+
prompt_changes.append("Added v2.2 envelope instructions")
|
|
332
|
+
|
|
333
|
+
if dry_run:
|
|
334
|
+
if manifest_changes:
|
|
335
|
+
changes.append(f"[DRY RUN] Would update module.yaml: {', '.join(manifest_changes)}")
|
|
336
|
+
if schema_changes:
|
|
337
|
+
changes.append(f"[DRY RUN] Would update schema.json: {', '.join(schema_changes)}")
|
|
338
|
+
if prompt_changes:
|
|
339
|
+
changes.append(f"[DRY RUN] Would update prompt.md: {', '.join(prompt_changes)}")
|
|
340
|
+
else:
|
|
341
|
+
if manifest_changes:
|
|
342
|
+
_write_yaml(module_yaml_path, manifest)
|
|
343
|
+
changes.append(f"Updated module.yaml: {', '.join(manifest_changes)}")
|
|
344
|
+
|
|
345
|
+
if schema_changes:
|
|
346
|
+
_write_json(schema_path, schema)
|
|
347
|
+
changes.append(f"Updated schema.json: {', '.join(schema_changes)}")
|
|
348
|
+
|
|
349
|
+
if prompt_changes:
|
|
350
|
+
_write_text(prompt_path, prompt)
|
|
351
|
+
changes.append(f"Updated prompt.md: {', '.join(prompt_changes)}")
|
|
352
|
+
|
|
353
|
+
return True, changes, warnings
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
# =============================================================================
|
|
357
|
+
# Helper Functions
|
|
358
|
+
# =============================================================================
|
|
359
|
+
|
|
360
|
+
def _create_backup(module_path: Path) -> Path:
|
|
361
|
+
"""Create a backup of the module directory."""
|
|
362
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
363
|
+
backup_path = module_path.parent / f"{module_path.name}_backup_{timestamp}"
|
|
364
|
+
shutil.copytree(module_path, backup_path)
|
|
365
|
+
return backup_path
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _create_v22_manifest(frontmatter: dict) -> dict:
|
|
369
|
+
"""Create v2.2 module.yaml from frontmatter."""
|
|
370
|
+
manifest = {
|
|
371
|
+
'name': frontmatter.get('name', 'unknown'),
|
|
372
|
+
'version': frontmatter.get('version', '2.2.0'),
|
|
373
|
+
'responsibility': frontmatter.get('responsibility', ''),
|
|
374
|
+
'tier': 'decision',
|
|
375
|
+
'schema_strictness': 'medium',
|
|
376
|
+
'excludes': frontmatter.get('excludes', []),
|
|
377
|
+
'policies': {
|
|
378
|
+
'network': 'deny',
|
|
379
|
+
'filesystem_write': 'deny',
|
|
380
|
+
'side_effects': 'deny',
|
|
381
|
+
'code_execution': 'deny'
|
|
382
|
+
},
|
|
383
|
+
'tools': {
|
|
384
|
+
'policy': 'deny_by_default',
|
|
385
|
+
'allowed': [],
|
|
386
|
+
'denied': ['write_file', 'shell', 'network']
|
|
387
|
+
},
|
|
388
|
+
'overflow': {
|
|
389
|
+
'enabled': True,
|
|
390
|
+
'recoverable': True,
|
|
391
|
+
'max_items': 5,
|
|
392
|
+
'require_suggested_mapping': True
|
|
393
|
+
},
|
|
394
|
+
'enums': {
|
|
395
|
+
'strategy': 'extensible'
|
|
396
|
+
},
|
|
397
|
+
'failure': {
|
|
398
|
+
'contract': 'error_union',
|
|
399
|
+
'partial_allowed': True,
|
|
400
|
+
'must_return_error_schema': True
|
|
401
|
+
},
|
|
402
|
+
'runtime_requirements': {
|
|
403
|
+
'structured_output': True,
|
|
404
|
+
'max_input_tokens': 8000,
|
|
405
|
+
'preferred_capabilities': ['json_mode']
|
|
406
|
+
},
|
|
407
|
+
'io': {
|
|
408
|
+
'input': './schema.json#/input',
|
|
409
|
+
'data': './schema.json#/data',
|
|
410
|
+
'meta': './schema.json#/meta',
|
|
411
|
+
'error': './schema.json#/error'
|
|
412
|
+
},
|
|
413
|
+
'compat': {
|
|
414
|
+
'accepts_v21_payload': True,
|
|
415
|
+
'runtime_auto_wrap': True,
|
|
416
|
+
'schema_output_alias': 'data'
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
# Preserve constraints if present
|
|
421
|
+
constraints = frontmatter.get('constraints', {})
|
|
422
|
+
if constraints:
|
|
423
|
+
manifest['constraints'] = constraints
|
|
424
|
+
|
|
425
|
+
# Preserve context if present
|
|
426
|
+
if 'context' in frontmatter:
|
|
427
|
+
manifest['context'] = frontmatter['context']
|
|
428
|
+
|
|
429
|
+
return manifest
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _create_v22_schema(input_schema: dict, output_schema: dict) -> dict:
|
|
433
|
+
"""Create v2.2 schema.json from input and output schemas."""
|
|
434
|
+
return {
|
|
435
|
+
'$schema': 'https://ziel-io.github.io/cognitive-modules/schema/v2.2.json',
|
|
436
|
+
'meta': _create_meta_schema(),
|
|
437
|
+
'input': input_schema,
|
|
438
|
+
'data': _add_rationale_to_output(output_schema),
|
|
439
|
+
'error': {
|
|
440
|
+
'type': 'object',
|
|
441
|
+
'required': ['code', 'message'],
|
|
442
|
+
'properties': {
|
|
443
|
+
'code': {'type': 'string'},
|
|
444
|
+
'message': {'type': 'string'}
|
|
445
|
+
}
|
|
446
|
+
},
|
|
447
|
+
'$defs': {
|
|
448
|
+
'extensions': _create_extensions_schema()
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def _create_meta_schema() -> dict:
|
|
454
|
+
"""Create the standard v2.2 meta schema."""
|
|
455
|
+
return {
|
|
456
|
+
'type': 'object',
|
|
457
|
+
'required': ['confidence', 'risk', 'explain'],
|
|
458
|
+
'properties': {
|
|
459
|
+
'confidence': {
|
|
460
|
+
'type': 'number',
|
|
461
|
+
'minimum': 0,
|
|
462
|
+
'maximum': 1,
|
|
463
|
+
'description': 'Confidence score, unified across all modules'
|
|
464
|
+
},
|
|
465
|
+
'risk': {
|
|
466
|
+
'type': 'string',
|
|
467
|
+
'enum': ['none', 'low', 'medium', 'high'],
|
|
468
|
+
'description': 'Aggregated risk level'
|
|
469
|
+
},
|
|
470
|
+
'explain': {
|
|
471
|
+
'type': 'string',
|
|
472
|
+
'maxLength': 280,
|
|
473
|
+
'description': 'Short explanation for control plane'
|
|
474
|
+
},
|
|
475
|
+
'trace_id': {'type': 'string'},
|
|
476
|
+
'model': {'type': 'string'},
|
|
477
|
+
'latency_ms': {'type': 'number', 'minimum': 0}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def _create_extensions_schema() -> dict:
|
|
483
|
+
"""Create the standard extensions schema for overflow."""
|
|
484
|
+
return {
|
|
485
|
+
'type': 'object',
|
|
486
|
+
'properties': {
|
|
487
|
+
'insights': {
|
|
488
|
+
'type': 'array',
|
|
489
|
+
'maxItems': 5,
|
|
490
|
+
'items': {
|
|
491
|
+
'type': 'object',
|
|
492
|
+
'required': ['text', 'suggested_mapping'],
|
|
493
|
+
'properties': {
|
|
494
|
+
'text': {'type': 'string'},
|
|
495
|
+
'suggested_mapping': {'type': 'string'},
|
|
496
|
+
'evidence': {'type': 'string'}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def _add_rationale_to_output(output_schema: dict) -> dict:
|
|
505
|
+
"""Ensure output schema has rationale field."""
|
|
506
|
+
schema = dict(output_schema)
|
|
507
|
+
|
|
508
|
+
# Ensure required includes rationale
|
|
509
|
+
required = schema.get('required', [])
|
|
510
|
+
if 'rationale' not in required:
|
|
511
|
+
required.append('rationale')
|
|
512
|
+
schema['required'] = required
|
|
513
|
+
|
|
514
|
+
# Ensure properties includes rationale
|
|
515
|
+
properties = schema.get('properties', {})
|
|
516
|
+
if 'rationale' not in properties:
|
|
517
|
+
properties['rationale'] = {
|
|
518
|
+
'type': 'string',
|
|
519
|
+
'description': 'Detailed explanation for audit and human review'
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
# Add extensions reference
|
|
523
|
+
if 'extensions' not in properties:
|
|
524
|
+
properties['extensions'] = {'$ref': '#/$defs/extensions'}
|
|
525
|
+
|
|
526
|
+
schema['properties'] = properties
|
|
527
|
+
|
|
528
|
+
# Remove confidence from data (moved to meta)
|
|
529
|
+
# But keep for backward compat if exists
|
|
530
|
+
|
|
531
|
+
return schema
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def _create_v22_prompt(frontmatter: dict, prompt_body: str) -> str:
|
|
535
|
+
"""Create v2.2 prompt.md with envelope instructions."""
|
|
536
|
+
name = frontmatter.get('name', 'Module')
|
|
537
|
+
|
|
538
|
+
return f"""# {name.replace('-', ' ').title()}
|
|
539
|
+
|
|
540
|
+
{prompt_body}
|
|
541
|
+
|
|
542
|
+
## Response Format (Envelope v2.2)
|
|
543
|
+
|
|
544
|
+
You MUST wrap your response in the v2.2 envelope format with separate meta and data sections.
|
|
545
|
+
|
|
546
|
+
### Success Response
|
|
547
|
+
|
|
548
|
+
```json
|
|
549
|
+
{{
|
|
550
|
+
"ok": true,
|
|
551
|
+
"meta": {{
|
|
552
|
+
"confidence": 0.9,
|
|
553
|
+
"risk": "low",
|
|
554
|
+
"explain": "Short summary (max 280 chars) for routing and UI display."
|
|
555
|
+
}},
|
|
556
|
+
"data": {{
|
|
557
|
+
"...your output fields...",
|
|
558
|
+
"rationale": "Detailed explanation for audit and human review..."
|
|
559
|
+
}}
|
|
560
|
+
}}
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
### Error Response
|
|
564
|
+
|
|
565
|
+
```json
|
|
566
|
+
{{
|
|
567
|
+
"ok": false,
|
|
568
|
+
"meta": {{
|
|
569
|
+
"confidence": 0.0,
|
|
570
|
+
"risk": "high",
|
|
571
|
+
"explain": "Brief error summary."
|
|
572
|
+
}},
|
|
573
|
+
"error": {{
|
|
574
|
+
"code": "ERROR_CODE",
|
|
575
|
+
"message": "Detailed error description"
|
|
576
|
+
}}
|
|
577
|
+
}}
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
## Important
|
|
581
|
+
|
|
582
|
+
- `meta.explain` is for **quick decisions** (≤280 chars)
|
|
583
|
+
- `data.rationale` is for **audit and review** (no limit)
|
|
584
|
+
- Both must be present in successful responses
|
|
585
|
+
"""
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def _add_v22_instructions_to_prompt(prompt: str, manifest: dict) -> str:
|
|
589
|
+
"""Add v2.2 envelope instructions to existing prompt."""
|
|
590
|
+
v22_section = """
|
|
591
|
+
|
|
592
|
+
## Response Format (Envelope v2.2)
|
|
593
|
+
|
|
594
|
+
You MUST wrap your response in the v2.2 envelope format with separate meta and data sections:
|
|
595
|
+
|
|
596
|
+
- Success: `{ "ok": true, "meta": { "confidence": 0.9, "risk": "low", "explain": "≤280 chars" }, "data": { ...payload... } }`
|
|
597
|
+
- Error: `{ "ok": false, "meta": { ... }, "error": { "code": "...", "message": "..." } }`
|
|
598
|
+
|
|
599
|
+
Important:
|
|
600
|
+
- `meta.explain` is for quick routing (≤280 chars)
|
|
601
|
+
- `data.rationale` is for detailed audit (no limit)
|
|
602
|
+
"""
|
|
603
|
+
return prompt + v22_section
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
def _write_yaml(path: Path, data: dict) -> None:
|
|
607
|
+
"""Write YAML file with nice formatting."""
|
|
608
|
+
with open(path, 'w', encoding='utf-8') as f:
|
|
609
|
+
# Add header comment
|
|
610
|
+
f.write("# Cognitive Module Manifest v2.2\n")
|
|
611
|
+
yaml.dump(data, f, default_flow_style=False, allow_unicode=True, sort_keys=False)
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
def _write_json(path: Path, data: dict) -> None:
|
|
615
|
+
"""Write JSON file with nice formatting."""
|
|
616
|
+
with open(path, 'w', encoding='utf-8') as f:
|
|
617
|
+
json.dump(data, f, indent=2, ensure_ascii=False)
|
|
618
|
+
f.write('\n')
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def _write_text(path: Path, content: str) -> None:
|
|
622
|
+
"""Write text file."""
|
|
623
|
+
with open(path, 'w', encoding='utf-8') as f:
|
|
624
|
+
f.write(content)
|