@akemi-mio/creativity 0.1.0 → 0.1.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.
@@ -0,0 +1,358 @@
1
+ "use strict";
2
+ /**
3
+ * WhenThenRunner — 声明式 WHEN/THEN 受限 DSL 运行器(机制涌现 P2a,定案 D5/R5/A2)
4
+ *
5
+ * 候选执行不写任意代码:DSL 查表求值(阈值比较 + 字段存在性 + 枚举匹配,禁 JS eval)。
6
+ * 步骤依赖执行 + 显式 named outputs(无隐式上下文注入)。
7
+ * resourceBudget 编译为 CapabilityToken.constraints。
8
+ * 求值器输入输出全白名单序列化(JSON number/boolean/string)。
9
+ * 条件字符串长度上限,解析失败 = fail-closed(步骤跳过 + 记 guardrail 事件,不中断实验、不泄漏沙箱内部)。
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.MAX_CONDITION_LIST_LEN = exports.MAX_CONDITION_STRING_VALUE_LEN = exports.MAX_CONDITION_FIELD_LEN = void 0;
13
+ exports.lookupField = lookupField;
14
+ exports.evaluateCondition = evaluateCondition;
15
+ exports.evaluateWhen = evaluateWhen;
16
+ exports.compileBudgetToConstraints = compileBudgetToConstraints;
17
+ exports.topologicalOrder = topologicalOrder;
18
+ exports.runWhenThenRule = runWhenThenRule;
19
+ // ══════════════════════════════════════════
20
+ // 求值器(白名单查表,禁 eval)
21
+ // ══════════════════════════════════════════
22
+ /** 条件字符串长度上限(防超长注入) */
23
+ exports.MAX_CONDITION_FIELD_LEN = 128;
24
+ exports.MAX_CONDITION_STRING_VALUE_LEN = 512;
25
+ exports.MAX_CONDITION_LIST_LEN = 32;
26
+ function isScalar(v) {
27
+ return typeof v === 'number' || typeof v === 'boolean' || typeof v === 'string';
28
+ }
29
+ /** 按点分路径从 context 取值;中间路径不存在返回 undefined。不深入数组索引(防原型链)。 */
30
+ function lookupField(context, field) {
31
+ if (typeof field !== 'string' || field.length === 0 || field.length > exports.MAX_CONDITION_FIELD_LEN)
32
+ return undefined;
33
+ // 禁原型链:字段不得含 `__proto__` / `constructor` / `prototype`
34
+ if (field.includes('__proto__') || field.includes('constructor') || field.includes('prototype'))
35
+ return undefined;
36
+ const parts = field.split('.');
37
+ let cur = context;
38
+ for (const p of parts) {
39
+ if (cur === null || typeof cur !== 'object' || Array.isArray(cur))
40
+ return undefined;
41
+ const obj = cur;
42
+ if (!Object.prototype.hasOwnProperty.call(obj, p))
43
+ return undefined;
44
+ cur = obj[p];
45
+ }
46
+ return cur;
47
+ }
48
+ /** 求值单个 WHEN 条件;解析失败(非法字段/值/操作符)返回 null(调用方 fail-closed)。 */
49
+ function evaluateCondition(context, cond) {
50
+ if (!cond || typeof cond !== 'object')
51
+ return null;
52
+ const { field, op, value } = cond;
53
+ if (typeof field !== 'string' || field.length === 0 || field.length > exports.MAX_CONDITION_FIELD_LEN)
54
+ return null;
55
+ if (field.includes('__proto__') || field.includes('constructor') || field.includes('prototype'))
56
+ return null;
57
+ const actual = lookupField(context, field);
58
+ switch (op) {
59
+ case 'exists':
60
+ return actual !== undefined && actual !== null;
61
+ case 'notExists':
62
+ return actual === undefined || actual === null;
63
+ case 'eq':
64
+ case 'neq':
65
+ case 'gt':
66
+ case 'gte':
67
+ case 'lt':
68
+ case 'lte': {
69
+ if (!isScalar(value))
70
+ return null;
71
+ if (!isScalar(actual))
72
+ return false;
73
+ switch (op) {
74
+ case 'eq':
75
+ return actual === value;
76
+ case 'neq':
77
+ return actual !== value;
78
+ case 'gt':
79
+ return typeof actual === 'number' && typeof value === 'number' && actual > value;
80
+ case 'gte':
81
+ return typeof actual === 'number' && typeof value === 'number' && actual >= value;
82
+ case 'lt':
83
+ return typeof actual === 'number' && typeof value === 'number' && actual < value;
84
+ case 'lte':
85
+ return typeof actual === 'number' && typeof value === 'number' && actual <= value;
86
+ default:
87
+ return null;
88
+ }
89
+ }
90
+ case 'in':
91
+ case 'nin': {
92
+ if (!Array.isArray(value) || value.length > exports.MAX_CONDITION_LIST_LEN)
93
+ return null;
94
+ if (!value.every((v) => typeof v === 'string' && v.length <= exports.MAX_CONDITION_STRING_VALUE_LEN))
95
+ return null;
96
+ if (!isScalar(actual))
97
+ return false;
98
+ const hit = value.includes(actual);
99
+ return op === 'in' ? hit : !hit;
100
+ }
101
+ default:
102
+ // 未知操作符 = 解析失败
103
+ return null;
104
+ }
105
+ }
106
+ /** 求值全部 WHEN 条件(AND);任一解析失败返回 null(fail-closed)。 */
107
+ function evaluateWhen(context, when) {
108
+ if (!when || when.length === 0)
109
+ return true;
110
+ for (const c of when) {
111
+ const r = evaluateCondition(context, c);
112
+ if (r === null)
113
+ return null;
114
+ if (r === false)
115
+ return false;
116
+ }
117
+ return true;
118
+ }
119
+ // ══════════════════════════════════════════
120
+ // resourceBudget → CapabilityToken.constraints
121
+ // ══════════════════════════════════════════
122
+ /** 把 ResourceBudgetSpec 编译为 CapabilityToken.constraints(白名单字段)。 */
123
+ function compileBudgetToConstraints(budget, ruleBudget) {
124
+ // 步骤级覆盖规则级
125
+ const b = budget ?? ruleBudget;
126
+ if (!b)
127
+ return undefined;
128
+ const constraints = {};
129
+ if (typeof b.maxCalls === 'number' && Number.isFinite(b.maxCalls) && b.maxCalls >= 0) {
130
+ constraints.maxCalls = Math.floor(b.maxCalls);
131
+ }
132
+ if (typeof b.maxTokens === 'number' && Number.isFinite(b.maxTokens) && b.maxTokens >= 0) {
133
+ constraints.maxTokens = Math.floor(b.maxTokens);
134
+ }
135
+ if (typeof b.expiryMs === 'number' && Number.isFinite(b.expiryMs) && b.expiryMs >= 0) {
136
+ constraints.expiryMs = Math.floor(b.expiryMs);
137
+ }
138
+ if (b.allowedValues && typeof b.allowedValues === 'object' && !Array.isArray(b.allowedValues)) {
139
+ const allowed = {};
140
+ for (const [k, arr] of Object.entries(b.allowedValues)) {
141
+ if (Array.isArray(arr) && arr.every((v) => typeof v === 'string' && v.length <= exports.MAX_CONDITION_STRING_VALUE_LEN)) {
142
+ if (arr.length <= exports.MAX_CONDITION_LIST_LEN)
143
+ allowed[k] = arr;
144
+ }
145
+ }
146
+ if (Object.keys(allowed).length > 0)
147
+ constraints.allowedValues = allowed;
148
+ }
149
+ return Object.keys(constraints).length > 0 ? constraints : undefined;
150
+ }
151
+ // ══════════════════════════════════════════
152
+ // 依赖调度(DAG 拓扑 + named outputs)
153
+ // ══════════════════════════════════════════
154
+ /** 拓扑排序步骤;环路/未知依赖返回 null(fail-closed)。 */
155
+ function topologicalOrder(steps) {
156
+ const byId = new Map();
157
+ for (const s of steps) {
158
+ if (!s || typeof s.id !== 'string' || s.id.length === 0)
159
+ return null;
160
+ if (byId.has(s.id))
161
+ return null;
162
+ byId.set(s.id, s);
163
+ }
164
+ const indegree = new Map();
165
+ const children = new Map();
166
+ for (const s of steps) {
167
+ indegree.set(s.id, 0);
168
+ }
169
+ for (const s of steps) {
170
+ const deps = s.dependsOn ?? [];
171
+ for (const d of deps) {
172
+ if (!byId.has(d))
173
+ return null; // 未知依赖
174
+ indegree.set(s.id, (indegree.get(s.id) ?? 0) + 1);
175
+ const list = children.get(d) ?? [];
176
+ list.push(s.id);
177
+ children.set(d, list);
178
+ }
179
+ }
180
+ const queue = [];
181
+ for (const [id, deg] of indegree) {
182
+ if (deg === 0)
183
+ queue.push(id);
184
+ }
185
+ queue.sort();
186
+ const order = [];
187
+ while (queue.length > 0) {
188
+ const id = queue.shift();
189
+ order.push(byId.get(id));
190
+ for (const child of children.get(id) ?? []) {
191
+ const nd = (indegree.get(child) ?? 1) - 1;
192
+ indegree.set(child, nd);
193
+ if (nd === 0)
194
+ queue.push(child);
195
+ }
196
+ queue.sort();
197
+ }
198
+ if (order.length !== steps.length)
199
+ return null; // 环
200
+ return order;
201
+ }
202
+ /**
203
+ * 执行一条 WHEN/THEN 规则。
204
+ *
205
+ * - 按 DAG 拓扑顺序调度
206
+ * - 每步先求值 WHEN(解析失败 → fail-closed skip + guardrail 事件)
207
+ * - 输入从初始 context + 上游 named outputs 显式解析(无隐式注入)
208
+ * - 预算编译为 CapabilityToken.constraints 传给 executor
209
+ * - 任一步失败不中断实验:后续依赖步骤 blocked,独立步骤继续
210
+ */
211
+ async function runWhenThenRule(rule, context, executeStep, options) {
212
+ const events = [];
213
+ const namedOutputs = { ...(options?.initialOutputs ?? {}) };
214
+ const stepStatus = new Map();
215
+ const stepReason = new Map();
216
+ const stepOutputs = new Map();
217
+ const sink = (ev) => {
218
+ events.push(ev);
219
+ options?.onEvent?.(ev);
220
+ };
221
+ // 1. 拓扑排序(环/未知依赖 → 整规则 fail-closed)
222
+ const order = topologicalOrder(rule.steps ?? []);
223
+ if (!order) {
224
+ sink({
225
+ event_type: 'whenthen.rule.failed',
226
+ rule_id: rule.id,
227
+ reason: 'invalid_graph',
228
+ outcome: 'failed',
229
+ timestamp: Date.now(),
230
+ });
231
+ return { success: false, namedOutputs, steps: [], events };
232
+ }
233
+ // 2. 逐步骤
234
+ for (const step of order) {
235
+ const deps = step.dependsOn ?? [];
236
+ const depBlocked = deps.some((d) => {
237
+ const st = stepStatus.get(d);
238
+ return st === 'failed' || st === 'blocked' || st === 'skipped';
239
+ });
240
+ if (depBlocked) {
241
+ stepStatus.set(step.id, 'blocked');
242
+ stepReason.set(step.id, 'upstream_blocked');
243
+ sink({
244
+ event_type: 'whenthen.step.skipped',
245
+ rule_id: rule.id,
246
+ step_id: step.id,
247
+ reason: 'upstream_blocked',
248
+ outcome: 'skipped',
249
+ timestamp: Date.now(),
250
+ });
251
+ continue;
252
+ }
253
+ // 3. WHEN 求值(fail-closed)
254
+ const whenOk = evaluateWhen(context, step.when);
255
+ if (whenOk === null) {
256
+ stepStatus.set(step.id, 'skipped');
257
+ stepReason.set(step.id, 'when_parse_failed');
258
+ sink({
259
+ event_type: 'whenthen.step.skipped',
260
+ rule_id: rule.id,
261
+ step_id: step.id,
262
+ reason: 'when_parse_failed',
263
+ outcome: 'skipped',
264
+ timestamp: Date.now(),
265
+ });
266
+ continue;
267
+ }
268
+ if (whenOk === false) {
269
+ stepStatus.set(step.id, 'skipped');
270
+ stepReason.set(step.id, 'when_not_satisfied');
271
+ sink({
272
+ event_type: 'whenthen.step.skipped',
273
+ rule_id: rule.id,
274
+ step_id: step.id,
275
+ reason: 'when_not_satisfied',
276
+ outcome: 'skipped',
277
+ timestamp: Date.now(),
278
+ });
279
+ continue;
280
+ }
281
+ // 4. 显式 named inputs 解析(路径 = 初始 context 或 namedOutputs)
282
+ const inputs = {};
283
+ if (step.inputs) {
284
+ for (const [name, path] of Object.entries(step.inputs)) {
285
+ if (typeof path !== 'string' || path.length === 0 || path.length > exports.MAX_CONDITION_FIELD_LEN)
286
+ continue;
287
+ if (path.includes('__proto__') || path.includes('constructor') || path.includes('prototype'))
288
+ continue;
289
+ const v = lookupField(namedOutputs, path) ?? lookupField(context, path);
290
+ if (v !== undefined)
291
+ inputs[name] = v;
292
+ }
293
+ }
294
+ // 5. 预算编译
295
+ const constraints = compileBudgetToConstraints(step.resourceBudget, rule.resourceBudget);
296
+ // 6. 执行
297
+ try {
298
+ const result = await executeStep({
299
+ stepId: step.id,
300
+ capabilityId: step.capabilityId,
301
+ inputs,
302
+ constraints,
303
+ });
304
+ if (result.success) {
305
+ stepStatus.set(step.id, 'ok');
306
+ const outs = result.outputs ?? {};
307
+ stepOutputs.set(step.id, outs);
308
+ for (const [k, v] of Object.entries(outs)) {
309
+ namedOutputs[k] = v;
310
+ namedOutputs[`${step.id}.${k}`] = v;
311
+ }
312
+ }
313
+ else {
314
+ stepStatus.set(step.id, 'failed');
315
+ stepReason.set(step.id, result.error ?? 'step_failed');
316
+ sink({
317
+ event_type: 'whenthen.step.failed',
318
+ rule_id: rule.id,
319
+ step_id: step.id,
320
+ reason: result.error ?? 'step_failed',
321
+ outcome: 'failed',
322
+ timestamp: Date.now(),
323
+ });
324
+ }
325
+ }
326
+ catch {
327
+ stepStatus.set(step.id, 'failed');
328
+ stepReason.set(step.id, 'step_exception');
329
+ sink({
330
+ event_type: 'whenthen.step.failed',
331
+ rule_id: rule.id,
332
+ step_id: step.id,
333
+ reason: 'step_exception',
334
+ outcome: 'failed',
335
+ timestamp: Date.now(),
336
+ });
337
+ }
338
+ }
339
+ const allOk = order.every((s) => stepStatus.get(s.id) === 'ok' || stepStatus.get(s.id) === 'skipped');
340
+ sink({
341
+ event_type: allOk ? 'whenthen.rule.complete' : 'whenthen.rule.failed',
342
+ rule_id: rule.id,
343
+ reason: allOk ? 'complete' : 'partial_failure',
344
+ outcome: allOk ? 'success' : 'partial',
345
+ timestamp: Date.now(),
346
+ });
347
+ return {
348
+ success: allOk,
349
+ namedOutputs,
350
+ steps: order.map((s) => ({
351
+ id: s.id,
352
+ status: stepStatus.get(s.id) ?? 'blocked',
353
+ reason: stepReason.get(s.id),
354
+ outputs: stepOutputs.get(s.id),
355
+ })),
356
+ events,
357
+ };
358
+ }
package/dist/types.d.ts CHANGED
@@ -85,7 +85,7 @@ export interface Hypothesis {
85
85
  impact: number;
86
86
  /** 逻辑一致性评分 0-100(第四维,评审/生成时打分,可缺省视为 50) */
87
87
  logic?: number;
88
- status: 'draft' | 'active' | 'experimenting' | 'validated' | 'rejected';
88
+ status: 'draft' | 'active' | 'testing' | 'validated' | 'rejected';
89
89
  createdAt: number;
90
90
  /** The rotation technique this hypothesis was generated with (idea.generate provenance.technique). */
91
91
  technique?: string;
@@ -112,6 +112,20 @@ export interface Hypothesis {
112
112
  implementedCommitSha?: string;
113
113
  implementedAt?: number;
114
114
  implementationNote?: string;
115
+ /** 机制假设:提出的关系边(带 proposedRelations 即机制假设,否则普通创意) */
116
+ proposedRelations?: ProposedRelation[];
117
+ /** 证伪条件:命中任一条件则机制假设可被拒绝 */
118
+ falsificationConditions?: string[];
119
+ /** 触发本假设的 observation/trace id 溯源 */
120
+ sourceObservationIds?: string[];
121
+ }
122
+ /** 机制图的一条候选边:A --[effect]--> B */
123
+ export interface ProposedRelation {
124
+ from: string;
125
+ to: string;
126
+ effect: string;
127
+ /** 推断来源:identity=verified | inferred(A6 confidence 映射) */
128
+ inference?: 'identity' | 'inferred';
115
129
  }
116
130
  export interface ExperimentPlan {
117
131
  hypothesisId: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akemi-mio/creativity",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Creativity layer: ideas, writing, blog, image, inspiration",
5
5
  "main": "./dist/IdeaGenerator.js",
6
6
  "types": "./dist/IdeaGenerator.d.ts",
@@ -25,14 +25,43 @@
25
25
  "require": "./dist/types.js",
26
26
  "default": "./dist/types.js"
27
27
  },
28
+ "./MechanismRegistry": {
29
+ "types": "./dist/MechanismRegistry.d.ts",
30
+ "require": "./dist/MechanismRegistry.js",
31
+ "default": "./dist/MechanismRegistry.js"
32
+ },
33
+ "./ExperimentRunStore": {
34
+ "types": "./dist/ExperimentRunStore.d.ts",
35
+ "require": "./dist/ExperimentRunStore.js",
36
+ "default": "./dist/ExperimentRunStore.js"
37
+ },
38
+ "./WhenThenRunner": {
39
+ "types": "./dist/WhenThenRunner.d.ts",
40
+ "require": "./dist/WhenThenRunner.js",
41
+ "default": "./dist/WhenThenRunner.js"
42
+ },
43
+ "./MechanismGraph": {
44
+ "types": "./dist/MechanismGraph.d.ts",
45
+ "require": "./dist/MechanismGraph.js",
46
+ "default": "./dist/MechanismGraph.js"
47
+ },
48
+ "./InsightSamplingExperiment": {
49
+ "types": "./dist/InsightSamplingExperiment.d.ts",
50
+ "require": "./dist/InsightSamplingExperiment.js",
51
+ "default": "./dist/InsightSamplingExperiment.js"
52
+ },
28
53
  "./package.json": "./package.json"
29
54
  },
30
- "files": ["dist"],
55
+ "files": [
56
+ "dist"
57
+ ],
31
58
  "scripts": {
32
59
  "build": "tsc -p tsconfig.build.json",
33
60
  "prepublishOnly": "npm run build"
34
61
  },
35
- "engines": { "node": ">=18" },
62
+ "engines": {
63
+ "node": ">=18"
64
+ },
36
65
  "license": "MIT",
37
66
  "dependencies": {
38
67
  "@akemi-mio/core": "^0.1.0"