abelien-harness 0.18.0 → 0.19.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/package.json +1 -1
- package/skills/abelien-algorithm/contract/host-tools.generated.json +7 -7
- package/skills/abelien-algorithm/scripts/research.mjs +1 -1
- package/skills/abelien-appearance/contract/host-tools.generated.json +15 -3
- package/skills/abelien-appearance/references/actions/appearance.points.generated.md +2 -2
- package/skills/abelien-appearance/references/points.md +3 -3
- package/skills/abelien-appearance/schemas/points-reading-reply.v1.json +100 -0
- package/skills/abelien-appearance/schemas/points-reading.v1.json +7 -3
- package/skills/abelien-appearance/scripts/points.mjs +102 -8
- package/skills/abelien-appearance/scripts/schemas.mjs +1 -1
- package/skills/abelien-clarify/SKILL.md +23 -7
- package/skills/abelien-clarify/references/examples.md +13 -2
- package/skills/abelien-clarify/references/runtime-contract.md +5 -3
- package/skills/abelien-clarify/schemas/skill-reply.v1.json +57 -0
- package/skills/abelien-clarify/schemas/skill-result.v1.json +264 -2
- package/skills/abelien-clarify/scripts/check_result.py +94 -6
- package/skills/abelien-clarify/scripts/dimensions.mjs +141 -0
- package/skills/abelien-clarify/scripts/test_check_result.py +66 -27
- package/skills/abelien-clarify/scripts/validate-result.mjs +32 -9
- package/skills/abelien-hardware/SKILL.md +2 -0
- package/skills/abelien-hardware/assets/AGENTS.md +10 -0
- package/skills/abelien-hardware/assets/category-default-dimensions.v1.json +169 -0
- package/skills/abelien-hardware/domain.json +1 -1
- package/skills/abelien-hardware/references/category-default-dimensions.md +55 -0
- package/skills/abelien-hardware/schemas/category-default-dimensions.v1.json +70 -0
- package/skills/abelien-hardware/scripts/oss-wiki-search.mjs +1 -1
- package/skills/abelien-hardware/scripts/provider-chain-contract.mjs +1 -1
- package/skills/abelien-harness/contracts/README.md +1 -1
- package/skills/abelien-harness/contracts/index.d.mts +4 -2
- package/skills/abelien-harness/contracts/index.mjs +19 -14
- package/skills/abelien-harness/contracts/schemas/cross-domain.v1.json +1 -1
- package/skills/abelien-harness/runtime/README.md +1 -1
- package/skills/abelien-harness/runtime/engine.mjs +2 -2
- package/skills/abelien-harness/runtime/index.mjs +1 -1
- package/skills/abelien-harness/runtime/task-view/README.md +3 -1
- package/skills/abelien-harness/runtime/task-view/index.mjs +117 -0
- package/skills/abelien-harness/scripts/ab.mjs +1 -1
- package/skills/abelien-harness/scripts/calc/index.mjs +1 -0
- package/skills/abelien-harness/scripts/calc/sources.mjs +90 -0
- package/skills/abelien-reference/actions/reference.page.json +1 -1
- package/skills/abelien-reference/contract/host-tools.generated.json +222 -34
- package/skills/abelien-reference/fixtures/restore.final.json +13 -2
- package/skills/abelien-reference/references/actions/reference.finalize.generated.md +2 -2
- package/skills/abelien-reference/references/actions/reference.page.generated.md +6 -3
- package/skills/abelien-reference/references/actions.generated.md +1 -1
- package/skills/abelien-reference/references/research-contract.md +2 -2
- package/skills/abelien-reference/schemas/host-tools.v1.json +5 -3
- package/skills/abelien-reference/schemas/reference-evidence.v1.json +3 -2
- package/skills/abelien-reference/schemas/reference-judgement.v1.json +6 -6
- package/skills/abelien-reference/scripts/finalize.mjs +49 -43
- package/skills/abelien-reference/scripts/research.mjs +1 -1
- package/skills/abelien-structure/SKILL.md +1 -1
- package/skills/abelien-structure/actions/structure.cad-check.json +1 -1
- package/skills/abelien-structure/actions/structure.layout-check.json +6 -1
- package/skills/abelien-structure/contract/host-tools.generated.json +7008 -2234
- package/skills/abelien-structure/flows/user-journey-cad.json +7 -2
- package/skills/abelien-structure/references/actions/structure.assets-request.generated.md +15 -0
- package/skills/abelien-structure/references/actions/structure.cad-assemble.generated.md +49 -6
- package/skills/abelien-structure/references/actions/structure.cad-check.generated.md +45 -1
- package/skills/abelien-structure/references/actions/structure.layout-check.generated.md +44 -1
- package/skills/abelien-structure/references/actions/structure.layout-request.generated.md +82 -9
- package/skills/abelien-structure/references/actions/structure.prelude.generated.md +17 -0
- package/skills/abelien-structure/references/cad-functions.generated.md +76 -23
- package/skills/abelien-structure/references/cad-modules.md +25 -4
- package/skills/abelien-structure/references/skeleton.md +7 -4
- package/skills/abelien-structure/references/structure-reasonableness.md +4 -4
- package/skills/abelien-structure/schemas/cad-assemble-input.v1.json +27 -2
- package/skills/abelien-structure/schemas/cad-build.v1.json +27 -0
- package/skills/abelien-structure/schemas/cad-modules.v1.json +5 -6
- package/skills/abelien-structure/schemas/cad-source.v1.json +6 -1
- package/skills/abelien-structure/schemas/layout-check-result.v1.json +2 -1
- package/skills/abelien-structure/schemas/layout-request-input.v1.json +103 -2
- package/skills/abelien-structure/schemas/layout-request.v1.json +30 -2
- package/skills/abelien-structure/schemas/prelude.v1.json +9 -0
- package/skills/abelien-structure/schemas/skeleton.v1.json +2 -1
- package/skills/abelien-structure/scripts/cad.mjs +359 -37
- package/skills/abelien-structure/scripts/layout.mjs +191 -27
- package/skills/abelien-structure/scripts/source-limits.mjs +5 -0
- package/skills/abelien-structure-worker/domain.json +3 -0
- package/skills/abelien-structure-worker/worker/abelien_cad.py +199 -34
- package/skills/abelien-structure-worker/worker/cad.py +92 -35
- package/skills/abelien-structure-worker/worker/jobs.mjs +5 -2
- package/skills/abelien-structure-worker/worker/main.mjs +5 -3
- package/skills/abelien-structure-worker/worker/source-contract.mjs +26 -19
- package/skills/abelien-workspace/actions/calc.evaluate.json +1 -1
- package/skills/abelien-workspace/assets/creation-start-flow.json +4 -1
- package/skills/abelien-workspace/assets/journey-stage-plan.json +61 -7
- package/skills/abelien-workspace/contract/host-tools.generated.json +1173 -45
- package/skills/abelien-workspace/flows/creation.create.json +73 -17
- package/skills/abelien-workspace/flows/creation.modify.json +73 -17
- package/skills/abelien-workspace/flows/creation.remix.json +4 -2
- package/skills/abelien-workspace/flows/creation.restore.json +73 -17
- package/skills/abelien-workspace/references/actions/calc.evaluate.generated.md +1 -1
- package/skills/abelien-workspace/references/actions/workspace.hardware-reselect-input.generated.md +49 -0
- package/skills/abelien-workspace/references/actions/workspace.intake-complete.generated.md +49 -0
- package/skills/abelien-workspace/references/actions/workspace.intake-route.generated.md +50 -1
- package/skills/abelien-workspace/references/actions/workspace.intake-wait.generated.md +49 -0
- package/skills/abelien-workspace/references/actions.generated.md +1 -1
- package/skills/abelien-workspace/references/creation-intake.md +1 -1
- package/skills/abelien-workspace/references/journey-stage-plan.md +11 -10
- package/skills/abelien-workspace/references/product-flow.md +4 -4
- package/skills/abelien-workspace/schemas/skeleton-model-reply.v1.json +3 -3
- package/skills/abelien-workspace/schemas/skeleton-reply.v1.json +37 -20
- package/skills/abelien-workspace/scripts/calc.mjs +10 -57
- package/skills/abelien-workspace/scripts/creation-intake.mjs +22 -8
- package/skills/abelien-workspace/scripts/creation-tasks.mjs +81 -23
- package/skills/abelien-workspace/scripts/flow-compile.mjs +6 -1
- package/skills/abelien-workspace/scripts/plans.mjs +6 -3
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://git.abelien.cn/abelien/abelienharness/skills/abelien-clarify/schemas/skill-reply.v1.json",
|
|
4
|
+
"title": "clarify SkillResult reply",
|
|
5
|
+
"description": "clarify 模型回复契约(CLR-1):与 skill-result.v1.json 同形,但 limits 只写 dimensions,不写 envelope;定稿代码核对引文出现在用户原话里,用共用求值核心换算出 envelope,得到 skill-result.v1.json 的完整快照。",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": [],
|
|
8
|
+
"properties": {
|
|
9
|
+
"result": {
|
|
10
|
+
"allOf": [
|
|
11
|
+
{
|
|
12
|
+
"$ref": "skill-result.v1.json#/$defs/ReplyRequirements"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"required": [
|
|
16
|
+
"intent"
|
|
17
|
+
]
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
"questions": {
|
|
22
|
+
"type": "array",
|
|
23
|
+
"items": {
|
|
24
|
+
"$ref": "skill-result.v1.json#/$defs/Question"
|
|
25
|
+
},
|
|
26
|
+
"maxItems": 3
|
|
27
|
+
},
|
|
28
|
+
"note": {
|
|
29
|
+
"type": "string",
|
|
30
|
+
"minLength": 1,
|
|
31
|
+
"pattern": "\\S"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"additionalProperties": false,
|
|
35
|
+
"anyOf": [
|
|
36
|
+
{
|
|
37
|
+
"required": [
|
|
38
|
+
"result"
|
|
39
|
+
]
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"required": [
|
|
43
|
+
"questions"
|
|
44
|
+
],
|
|
45
|
+
"properties": {
|
|
46
|
+
"questions": {
|
|
47
|
+
"minItems": 1
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"required": [
|
|
53
|
+
"note"
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
]
|
|
57
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "https://git.abelien.cn/abelien/abelienharness/skills/abelien-clarify/schemas/skill-result.v1.json",
|
|
4
4
|
"title": "clarify SkillResult",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "clarify 完整快照契约(定稿后的形状):limits 的 envelope 只能由代码按 dimensions 的引文换算得出(CLR-1),两者同在同缺;模型回复的形状见 skill-reply.v1.json。来源、稳定ID、系统假设 note 与 Envelope 算术由同包只读校验器复核。",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"required": [],
|
|
8
8
|
"properties": {
|
|
@@ -149,6 +149,9 @@
|
|
|
149
149
|
"preference"
|
|
150
150
|
]
|
|
151
151
|
},
|
|
152
|
+
"dimensions": {
|
|
153
|
+
"$ref": "#/$defs/Dimensions"
|
|
154
|
+
},
|
|
152
155
|
"envelope": {
|
|
153
156
|
"$ref": "../../abelien-harness/contracts/schemas/cross-domain.v1.json#/$defs/Envelope"
|
|
154
157
|
},
|
|
@@ -156,7 +159,16 @@
|
|
|
156
159
|
"$ref": "#/$defs/Network"
|
|
157
160
|
}
|
|
158
161
|
},
|
|
159
|
-
"additionalProperties": false
|
|
162
|
+
"additionalProperties": false,
|
|
163
|
+
"description": "需求限制。envelope 是代码从 dimensions 换算出的毫米包络(abelien-clarify/scripts/dimensions.mjs),不是模型写的数;有 dimensions 就有 envelope,反之亦然,校验时按 dimensions 重新换算并要求与 envelope 完全一致。",
|
|
164
|
+
"dependentRequired": {
|
|
165
|
+
"dimensions": [
|
|
166
|
+
"envelope"
|
|
167
|
+
],
|
|
168
|
+
"envelope": [
|
|
169
|
+
"dimensions"
|
|
170
|
+
]
|
|
171
|
+
}
|
|
160
172
|
}
|
|
161
173
|
}
|
|
162
174
|
},
|
|
@@ -258,6 +270,256 @@
|
|
|
258
270
|
"offline",
|
|
259
271
|
"no_cloud"
|
|
260
272
|
]
|
|
273
|
+
},
|
|
274
|
+
"Dimensions": {
|
|
275
|
+
"description": "尺寸限制的读法(CLR-1):模型只给形状和每个边界的来源——用户原话的逐字引文,或系统估算的选择——以及它对应的轴和上下限;毫米数由代码用共用求值核心换算(半径乘 2 也由代码算)。盒体轴 width、depth、height 依次对应 envelope.max_mm 的三个分量;圆柱轴 diameter 或 radius、height;wall 是壁厚。",
|
|
276
|
+
"type": "object",
|
|
277
|
+
"required": [
|
|
278
|
+
"shape",
|
|
279
|
+
"bounds"
|
|
280
|
+
],
|
|
281
|
+
"properties": {
|
|
282
|
+
"shape": {
|
|
283
|
+
"enum": [
|
|
284
|
+
"box",
|
|
285
|
+
"cylinder"
|
|
286
|
+
]
|
|
287
|
+
},
|
|
288
|
+
"bounds": {
|
|
289
|
+
"type": "array",
|
|
290
|
+
"items": {
|
|
291
|
+
"$ref": "#/$defs/DimensionBound"
|
|
292
|
+
},
|
|
293
|
+
"minItems": 1,
|
|
294
|
+
"maxItems": 12
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
"additionalProperties": false
|
|
298
|
+
},
|
|
299
|
+
"DimensionBound": {
|
|
300
|
+
"description": "一个尺寸边界。axes:这个数管哪些轴(\"10 厘米见方\"写 width、depth、height 三个);wall 只能单独出现。bound:max 上限、min 下限、exact 精确(上下限都取它);wall 用 min(保证的最小壁厚)或 estimate(估算壁厚,只作风险提示)。quote 与 choice 二选一:source=user 的限制只能用 quote。",
|
|
301
|
+
"type": "object",
|
|
302
|
+
"required": [
|
|
303
|
+
"axes",
|
|
304
|
+
"bound"
|
|
305
|
+
],
|
|
306
|
+
"properties": {
|
|
307
|
+
"axes": {
|
|
308
|
+
"type": "array",
|
|
309
|
+
"items": {
|
|
310
|
+
"enum": [
|
|
311
|
+
"width",
|
|
312
|
+
"depth",
|
|
313
|
+
"height",
|
|
314
|
+
"diameter",
|
|
315
|
+
"radius",
|
|
316
|
+
"wall"
|
|
317
|
+
]
|
|
318
|
+
},
|
|
319
|
+
"minItems": 1,
|
|
320
|
+
"maxItems": 3,
|
|
321
|
+
"uniqueItems": true
|
|
322
|
+
},
|
|
323
|
+
"bound": {
|
|
324
|
+
"enum": [
|
|
325
|
+
"max",
|
|
326
|
+
"min",
|
|
327
|
+
"exact",
|
|
328
|
+
"estimate"
|
|
329
|
+
]
|
|
330
|
+
},
|
|
331
|
+
"quote": {
|
|
332
|
+
"$ref": "#/$defs/DimensionQuote"
|
|
333
|
+
},
|
|
334
|
+
"choice": {
|
|
335
|
+
"$ref": "#/$defs/DimensionChoice"
|
|
336
|
+
}
|
|
337
|
+
},
|
|
338
|
+
"additionalProperties": false,
|
|
339
|
+
"oneOf": [
|
|
340
|
+
{
|
|
341
|
+
"required": [
|
|
342
|
+
"quote"
|
|
343
|
+
]
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
"required": [
|
|
347
|
+
"choice"
|
|
348
|
+
]
|
|
349
|
+
}
|
|
350
|
+
]
|
|
351
|
+
},
|
|
352
|
+
"DimensionQuote": {
|
|
353
|
+
"description": "用户原话的逐字引文(本轮用户消息或卡片答复里的原文,做 NFKC 规范化后比对),由代码从中取量。引文里不止一个数时用 index(从 0 起,数所有的数)指出取第几个。unit 只在取到的数自己没写单位时填(例如\"长 100、宽 80、高 60 毫米\"里的 100),写的是原话里共用的那个单位;数自己写了单位时不许填。",
|
|
354
|
+
"type": "object",
|
|
355
|
+
"required": [
|
|
356
|
+
"text"
|
|
357
|
+
],
|
|
358
|
+
"properties": {
|
|
359
|
+
"text": {
|
|
360
|
+
"type": "string",
|
|
361
|
+
"minLength": 1,
|
|
362
|
+
"maxLength": 200,
|
|
363
|
+
"pattern": "\\S"
|
|
364
|
+
},
|
|
365
|
+
"index": {
|
|
366
|
+
"type": "integer",
|
|
367
|
+
"minimum": 0,
|
|
368
|
+
"maximum": 63
|
|
369
|
+
},
|
|
370
|
+
"unit": {
|
|
371
|
+
"enum": [
|
|
372
|
+
"um",
|
|
373
|
+
"mm",
|
|
374
|
+
"cm",
|
|
375
|
+
"m",
|
|
376
|
+
"in",
|
|
377
|
+
"mil"
|
|
378
|
+
]
|
|
379
|
+
}
|
|
380
|
+
},
|
|
381
|
+
"additionalProperties": false
|
|
382
|
+
},
|
|
383
|
+
"DimensionChoice": {
|
|
384
|
+
"description": "系统估算:模型的选择,写数值、单位(原样,代码换算成毫米)和依据。只用于 source=system 的限制,换算结果不带硬筛选权限。",
|
|
385
|
+
"type": "object",
|
|
386
|
+
"required": [
|
|
387
|
+
"value",
|
|
388
|
+
"unit",
|
|
389
|
+
"basis"
|
|
390
|
+
],
|
|
391
|
+
"properties": {
|
|
392
|
+
"value": {
|
|
393
|
+
"type": "number",
|
|
394
|
+
"minimum": 0
|
|
395
|
+
},
|
|
396
|
+
"unit": {
|
|
397
|
+
"enum": [
|
|
398
|
+
"um",
|
|
399
|
+
"mm",
|
|
400
|
+
"cm",
|
|
401
|
+
"m",
|
|
402
|
+
"in",
|
|
403
|
+
"mil"
|
|
404
|
+
]
|
|
405
|
+
},
|
|
406
|
+
"basis": {
|
|
407
|
+
"type": "string",
|
|
408
|
+
"minLength": 1,
|
|
409
|
+
"maxLength": 300,
|
|
410
|
+
"pattern": "\\S"
|
|
411
|
+
}
|
|
412
|
+
},
|
|
413
|
+
"additionalProperties": false
|
|
414
|
+
},
|
|
415
|
+
"ReplyRequirements": {
|
|
416
|
+
"type": "object",
|
|
417
|
+
"required": [
|
|
418
|
+
"reaction",
|
|
419
|
+
"limits"
|
|
420
|
+
],
|
|
421
|
+
"properties": {
|
|
422
|
+
"context": {
|
|
423
|
+
"type": "object",
|
|
424
|
+
"required": [
|
|
425
|
+
"text",
|
|
426
|
+
"source"
|
|
427
|
+
],
|
|
428
|
+
"properties": {
|
|
429
|
+
"text": {
|
|
430
|
+
"type": "string",
|
|
431
|
+
"minLength": 1,
|
|
432
|
+
"pattern": "\\S"
|
|
433
|
+
},
|
|
434
|
+
"source": {
|
|
435
|
+
"enum": [
|
|
436
|
+
"user",
|
|
437
|
+
"system"
|
|
438
|
+
]
|
|
439
|
+
}
|
|
440
|
+
},
|
|
441
|
+
"additionalProperties": false
|
|
442
|
+
},
|
|
443
|
+
"intent": {
|
|
444
|
+
"$ref": "#/$defs/Intent"
|
|
445
|
+
},
|
|
446
|
+
"reaction": {
|
|
447
|
+
"type": "array",
|
|
448
|
+
"items": {
|
|
449
|
+
"type": "object",
|
|
450
|
+
"required": [
|
|
451
|
+
"id",
|
|
452
|
+
"text",
|
|
453
|
+
"source"
|
|
454
|
+
],
|
|
455
|
+
"properties": {
|
|
456
|
+
"id": {
|
|
457
|
+
"type": "string",
|
|
458
|
+
"minLength": 1,
|
|
459
|
+
"pattern": "\\S"
|
|
460
|
+
},
|
|
461
|
+
"text": {
|
|
462
|
+
"type": "string",
|
|
463
|
+
"minLength": 1,
|
|
464
|
+
"pattern": "\\S"
|
|
465
|
+
},
|
|
466
|
+
"source": {
|
|
467
|
+
"enum": [
|
|
468
|
+
"user",
|
|
469
|
+
"system"
|
|
470
|
+
]
|
|
471
|
+
}
|
|
472
|
+
},
|
|
473
|
+
"additionalProperties": false
|
|
474
|
+
}
|
|
475
|
+
},
|
|
476
|
+
"limits": {
|
|
477
|
+
"type": "array",
|
|
478
|
+
"items": {
|
|
479
|
+
"type": "object",
|
|
480
|
+
"required": [
|
|
481
|
+
"id",
|
|
482
|
+
"text",
|
|
483
|
+
"source",
|
|
484
|
+
"strength"
|
|
485
|
+
],
|
|
486
|
+
"properties": {
|
|
487
|
+
"id": {
|
|
488
|
+
"type": "string",
|
|
489
|
+
"minLength": 1,
|
|
490
|
+
"pattern": "\\S"
|
|
491
|
+
},
|
|
492
|
+
"text": {
|
|
493
|
+
"type": "string",
|
|
494
|
+
"minLength": 1,
|
|
495
|
+
"pattern": "\\S"
|
|
496
|
+
},
|
|
497
|
+
"source": {
|
|
498
|
+
"enum": [
|
|
499
|
+
"user",
|
|
500
|
+
"system"
|
|
501
|
+
]
|
|
502
|
+
},
|
|
503
|
+
"strength": {
|
|
504
|
+
"enum": [
|
|
505
|
+
"hard",
|
|
506
|
+
"preference"
|
|
507
|
+
]
|
|
508
|
+
},
|
|
509
|
+
"dimensions": {
|
|
510
|
+
"$ref": "#/$defs/Dimensions"
|
|
511
|
+
},
|
|
512
|
+
"network": {
|
|
513
|
+
"$ref": "#/$defs/Network"
|
|
514
|
+
}
|
|
515
|
+
},
|
|
516
|
+
"additionalProperties": false,
|
|
517
|
+
"description": "模型回复里的限制:尺寸只写 dimensions(引文或系统估算 + 轴 + 上下限),不写 envelope 或任何换算后的毫米数。"
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
},
|
|
521
|
+
"additionalProperties": false,
|
|
522
|
+
"description": "模型回复里的需求:与 Requirements 同形,只是 limits 不带 envelope(毫米包络由代码从 dimensions 换算)。"
|
|
261
523
|
}
|
|
262
524
|
}
|
|
263
525
|
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""Read-only validation and ID-based diff of a clarify SkillResult (Python 3.9+).
|
|
2
|
+
"""Read-only validation and ID-based diff of a clarify SkillResult reply (Python 3.9+).
|
|
3
|
+
|
|
4
|
+
Sizes are limits[].dimensions (quotes or system choices, axes, bounds); the millimetre envelope is computed by the
|
|
5
|
+
runtime from them, so a reply never carries one. This script checks structure only: it does not look the quotes up in
|
|
6
|
+
the user's message or convert units."""
|
|
3
7
|
|
|
4
8
|
import argparse
|
|
5
9
|
import json
|
|
@@ -18,6 +22,10 @@ INTENT_KINDS = ("restore", "create", "modify")
|
|
|
18
22
|
DEPTHS = ("appearance", "assembly", "functional")
|
|
19
23
|
MAX_FEATURES = 20
|
|
20
24
|
MAX_FEATURE_CHARS = 500
|
|
25
|
+
LENGTH_UNITS = ("um", "mm", "cm", "m", "in", "mil")
|
|
26
|
+
SHAPE_AXES = {"box": ("width", "depth", "height", "wall"), "cylinder": ("diameter", "radius", "height", "wall")}
|
|
27
|
+
BOUNDS = ("max", "min", "exact", "estimate")
|
|
28
|
+
MAX_BOUNDS = 12
|
|
21
29
|
|
|
22
30
|
|
|
23
31
|
def nonempty_string(value: Any) -> bool:
|
|
@@ -96,6 +104,76 @@ def check_envelope(value: Any, path: str, errors: list) -> None:
|
|
|
96
104
|
issue(errors, f"{path}.wall_mm.estimate", "估算壁厚必须有限且不小于保证下界")
|
|
97
105
|
|
|
98
106
|
|
|
107
|
+
def check_dimensions(value: Any, path: str, user_limit: bool, errors: list) -> None:
|
|
108
|
+
"""Structure of limits[].dimensions (CLR-1). Quote lookup and unit conversion are code's job in the runtime."""
|
|
109
|
+
if not object_fields(value, ("shape", "bounds"), (), path, errors):
|
|
110
|
+
return
|
|
111
|
+
shape = value.get("shape")
|
|
112
|
+
if shape not in SHAPE_AXES:
|
|
113
|
+
issue(errors, f"{path}.shape", "shape 必须是 box 或 cylinder")
|
|
114
|
+
return
|
|
115
|
+
bounds = value.get("bounds")
|
|
116
|
+
if not isinstance(bounds, list) or not 1 <= len(bounds) <= MAX_BOUNDS:
|
|
117
|
+
issue(errors, f"{path}.bounds", f"必须是 1–{MAX_BOUNDS} 项的数组")
|
|
118
|
+
return
|
|
119
|
+
slots = set()
|
|
120
|
+
for index, bound in enumerate(bounds):
|
|
121
|
+
location = f"{path}.bounds[{index}]"
|
|
122
|
+
if not object_fields(bound, ("axes", "bound"), ("quote", "choice"), location, errors):
|
|
123
|
+
continue
|
|
124
|
+
axes = bound.get("axes")
|
|
125
|
+
if (not isinstance(axes, list) or not 1 <= len(axes) <= 3 or len(set(map(str, axes))) != len(axes)
|
|
126
|
+
or any(axis not in SHAPE_AXES[shape] for axis in axes)):
|
|
127
|
+
issue(errors, f"{location}.axes", f"必须是 1–3 个不重复的 {shape} 轴:{', '.join(SHAPE_AXES[shape])}")
|
|
128
|
+
continue
|
|
129
|
+
if ("wall" in axes or "radius" in axes) and len(axes) > 1:
|
|
130
|
+
issue(errors, f"{location}.axes", "wall、radius 只能单独出现")
|
|
131
|
+
kind = bound.get("bound")
|
|
132
|
+
allowed = ("min", "estimate") if "wall" in axes else ("max", "min", "exact")
|
|
133
|
+
if kind not in allowed:
|
|
134
|
+
issue(errors, f"{location}.bound", f"必须是 {' / '.join(allowed)}")
|
|
135
|
+
for axis in axes:
|
|
136
|
+
for side in (("max", "min") if kind == "exact" else (kind,)):
|
|
137
|
+
slot = ("diameter" if axis == "radius" else axis, side)
|
|
138
|
+
if slot in slots:
|
|
139
|
+
issue(errors, f"{location}.axes", f"{slot[0]} 的 {slot[1]} 给了两次")
|
|
140
|
+
slots.add(slot)
|
|
141
|
+
if ("quote" in bound) == ("choice" in bound):
|
|
142
|
+
issue(errors, location, "quote 与 choice 必须二选一")
|
|
143
|
+
continue
|
|
144
|
+
if "quote" in bound:
|
|
145
|
+
quote = bound["quote"]
|
|
146
|
+
if object_fields(quote, ("text",), ("index", "unit"), f"{location}.quote", errors):
|
|
147
|
+
if not nonempty_string(quote.get("text")) or len(quote["text"]) > 200:
|
|
148
|
+
issue(errors, f"{location}.quote.text", "必须是 1–200 字的逐字引文")
|
|
149
|
+
if "index" in quote and not (type(quote["index"]) is int and 0 <= quote["index"] <= 63):
|
|
150
|
+
issue(errors, f"{location}.quote.index", "必须是 0–63 的整数")
|
|
151
|
+
if "unit" in quote and quote["unit"] not in LENGTH_UNITS:
|
|
152
|
+
issue(errors, f"{location}.quote.unit", f"必须是长度单位:{', '.join(LENGTH_UNITS)}")
|
|
153
|
+
else:
|
|
154
|
+
choice = bound["choice"]
|
|
155
|
+
if user_limit:
|
|
156
|
+
issue(errors, f"{location}.choice", "source=user 的限制只能引用用户原话;系统估算写在 source=system 的限制里")
|
|
157
|
+
if object_fields(choice, ("value", "unit", "basis"), (), f"{location}.choice", errors):
|
|
158
|
+
if not finite_number(choice.get("value"), inclusive=True):
|
|
159
|
+
issue(errors, f"{location}.choice.value", "必须是有限非负数")
|
|
160
|
+
if choice.get("unit") not in LENGTH_UNITS:
|
|
161
|
+
issue(errors, f"{location}.choice.unit", f"必须是长度单位:{', '.join(LENGTH_UNITS)}")
|
|
162
|
+
if not nonempty_string(choice.get("basis")) or len(choice["basis"]) > 300:
|
|
163
|
+
issue(errors, f"{location}.choice.basis", "必须写 1–300 字的依据")
|
|
164
|
+
check_dimension_cover(shape, slots, path, errors)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def check_dimension_cover(shape: str, slots: set, path: str, errors: list) -> None:
|
|
168
|
+
axes = ("width", "depth", "height") if shape == "box" else ("diameter", "height")
|
|
169
|
+
missing = [axis for axis in axes if (axis, "max") not in slots]
|
|
170
|
+
if missing:
|
|
171
|
+
issue(errors, f"{path}.bounds", f"{shape} 需要 {'、'.join(axes)} 的上限,缺 {'、'.join(missing)};凑不齐就不写 dimensions")
|
|
172
|
+
mins = [axis for axis in axes if (axis, "min") in slots]
|
|
173
|
+
if shape == "box" and mins and len(mins) != 3:
|
|
174
|
+
issue(errors, f"{path}.bounds", "盒体下限要三个轴一起给")
|
|
175
|
+
|
|
176
|
+
|
|
99
177
|
def check_features(value: Any, path: str, errors: list) -> None:
|
|
100
178
|
if not isinstance(value, list) or len(value) > MAX_FEATURES:
|
|
101
179
|
issue(errors, path, f"必须是数组,至多 {MAX_FEATURES} 项")
|
|
@@ -122,7 +200,8 @@ def check_intent(value: Any, path: str, errors: list) -> None:
|
|
|
122
200
|
issue(errors, f"{path}.keep", "create 没有来源产品,keep 必须为空")
|
|
123
201
|
|
|
124
202
|
|
|
125
|
-
def check_requirements(value: Any, path: str, errors: list, require_intent: bool = False) -> None:
|
|
203
|
+
def check_requirements(value: Any, path: str, errors: list, require_intent: bool = False, reply: bool = False) -> None:
|
|
204
|
+
"""reply=True: a model reply, whose limits carry dimensions only; otherwise stored requirements (envelope comes with dimensions)."""
|
|
126
205
|
if not object_fields(value, ("reaction", "limits"), ("context", "intent"), path, errors):
|
|
127
206
|
return
|
|
128
207
|
if "intent" in value:
|
|
@@ -145,7 +224,11 @@ def check_requirements(value: Any, path: str, errors: list, require_intent: bool
|
|
|
145
224
|
fields = ("id", "text", "source") + (("strength",) if group == "limits" else ())
|
|
146
225
|
for index, entry in enumerate(entries):
|
|
147
226
|
location = f"{path}.{group}[{index}]"
|
|
148
|
-
if
|
|
227
|
+
if group == "limits" and reply and "envelope" in entry:
|
|
228
|
+
issue(errors, f"{location}.envelope", "envelope 由代码按 dimensions 换算,回复只写 dimensions(引文、轴和上下限),不写毫米数")
|
|
229
|
+
continue
|
|
230
|
+
optional = (("dimensions", "network") if reply else ("dimensions", "envelope", "network")) if group == "limits" else ()
|
|
231
|
+
if not object_fields(entry, fields, optional, location, errors):
|
|
149
232
|
continue
|
|
150
233
|
text_field(entry, "id", location, errors)
|
|
151
234
|
text_field(entry, "text", location, errors)
|
|
@@ -156,6 +239,10 @@ def check_requirements(value: Any, path: str, errors: list, require_intent: bool
|
|
|
156
239
|
seen_ids.add(entry_id)
|
|
157
240
|
if entry.get("source") not in SOURCES:
|
|
158
241
|
issue(errors, f"{location}.source", "必须是 user 或 system")
|
|
242
|
+
if group == "limits" and "dimensions" in entry:
|
|
243
|
+
check_dimensions(entry["dimensions"], f"{location}.dimensions", entry.get("source") == "user", errors)
|
|
244
|
+
if group == "limits" and not reply and ("envelope" in entry) != ("dimensions" in entry):
|
|
245
|
+
issue(errors, location, "envelope 与 dimensions 同在同缺:envelope 由代码按 dimensions 换算")
|
|
159
246
|
if group == "limits" and "envelope" in entry:
|
|
160
247
|
check_envelope(entry["envelope"], f"{location}.envelope", errors)
|
|
161
248
|
if group == "limits" and "network" in entry and entry["network"] not in NETWORKS:
|
|
@@ -189,7 +276,8 @@ def check_questions(value: Any, errors: list) -> None:
|
|
|
189
276
|
|
|
190
277
|
|
|
191
278
|
def requirement_items(requirements: dict) -> dict:
|
|
192
|
-
"""Keys are tuples: an opaque ID can contain any character without collision.
|
|
279
|
+
"""Keys are tuples: an opaque ID can contain any character without collision.
|
|
280
|
+
A stored envelope is derived from dimensions, so it is left out of the comparison."""
|
|
193
281
|
items = {}
|
|
194
282
|
if "context" in requirements:
|
|
195
283
|
items[("context", None)] = requirements["context"]
|
|
@@ -197,7 +285,7 @@ def requirement_items(requirements: dict) -> dict:
|
|
|
197
285
|
items[("intent", None)] = requirements["intent"]
|
|
198
286
|
for group in ("reaction", "limits"):
|
|
199
287
|
for entry in requirements[group]:
|
|
200
|
-
items[(group, entry["id"])] = entry
|
|
288
|
+
items[(group, entry["id"])] = {key: item for key, item in entry.items() if key != "envelope"}
|
|
201
289
|
return items
|
|
202
290
|
|
|
203
291
|
|
|
@@ -222,7 +310,7 @@ def validate(proposal: Any, current: Any = None) -> dict:
|
|
|
222
310
|
if not object_fields(proposal, (), ("result", "questions", "note"), "$", errors):
|
|
223
311
|
return report
|
|
224
312
|
if "result" in proposal:
|
|
225
|
-
check_requirements(proposal["result"], "$.result", errors, require_intent=True)
|
|
313
|
+
check_requirements(proposal["result"], "$.result", errors, require_intent=True, reply=True)
|
|
226
314
|
if "questions" in proposal:
|
|
227
315
|
check_questions(proposal["questions"], errors)
|
|
228
316
|
if "note" in proposal:
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// 需求尺寸的代码换算(模型算术 CLR-1):模型只给用户原话的数值引文(或系统估算的选择)和轴向映射,
|
|
2
|
+
// 毫米包络由这里用共用求值核心换算(英寸、厘米换毫米,半径乘 2),并核对引文确实出现在用户本轮的原话里,
|
|
3
|
+
// 做法同 web clarify.ts 的 withdraw。换算出的 envelope 与 abelien-hardware dimensions.mjs 的 validateEnvelope 同形,
|
|
4
|
+
// 下游硬件选型、骨架照旧只读 envelope。模型回复里不再有任何换算后的毫米数,也没有兼容别名。
|
|
5
|
+
import { canonical } from "../../abelien-harness/contracts/index.mjs";
|
|
6
|
+
import { evaluateFormula, parseQuantities } from "../../abelien-harness/scripts/calc/index.mjs";
|
|
7
|
+
import { validateEnvelope } from "../../abelien-hardware/scripts/dimensions.mjs";
|
|
8
|
+
|
|
9
|
+
/** 尺寸换算的拒收码;定稿把它们包进 TASK_REPLY_INVALID,details.issues[].code 是这里的码。 */
|
|
10
|
+
export const CLARIFY_DIMENSION_CODES = Object.freeze([
|
|
11
|
+
"CLARIFY_ENVELOPE_FROM_MODEL", // 模型回复里写了 envelope(毫米数只由代码换算)
|
|
12
|
+
"CLARIFY_QUOTE_NOT_IN_MESSAGE", // 引文没有逐字出现在本轮用户消息或卡片答复里(沿用的旧边界除外)
|
|
13
|
+
"CLARIFY_QUOTE_NO_LENGTH", // 引文里取不到唯一一个长度量(没有数、不止一个数却没给 index、index 越界、单位不是长度)
|
|
14
|
+
"CLARIFY_QUOTE_UNIT", // 数自己写了单位却又填 unit,或裸数没填 unit,或链式尺寸里单位有歧义
|
|
15
|
+
"CLARIFY_CHOICE_ON_USER_LIMIT", // source=user 的限制用了系统估算的 choice
|
|
16
|
+
"CLARIFY_DIMENSIONS_MAPPING", // 轴与形状不符、wall 和别的轴混写、上下限不适用于该轴、同一边界给了两次、缺必需的上限
|
|
17
|
+
"CLARIFY_ENVELOPE_INVALID", // 换算出的包络不成立(下限大于上限、保证壁厚吃掉腔体等)
|
|
18
|
+
"CLARIFY_ENVELOPE_MISMATCH", // 存下的 envelope 与按 dimensions 重新换算的结果不一致,或两者只有其一
|
|
19
|
+
]);
|
|
20
|
+
const LENGTH_UNITS = new Set(["um", "mm", "cm", "m", "in", "mil"]);
|
|
21
|
+
const AXES = { box: ["width", "depth", "height"], cylinder: ["diameter", "height"] };
|
|
22
|
+
const SHAPE_AXES = { box: new Set(["width", "depth", "height", "wall"]), cylinder: new Set(["diameter", "radius", "height", "wall"]) };
|
|
23
|
+
|
|
24
|
+
function fail(code, path, message) {
|
|
25
|
+
throw Object.assign(new Error(`${code} at ${path}: ${message}`), { code: "TASK_REPLY_INVALID", details: { issues: [{ code, path, message }] } });
|
|
26
|
+
}
|
|
27
|
+
const normalize = text => text.normalize("NFKC");
|
|
28
|
+
|
|
29
|
+
/** 引文 → 带单位的操作数(值仍是原文十进制,换算交给求值核心)。 */
|
|
30
|
+
function quoteOperand(quote, path) {
|
|
31
|
+
const found = parseQuantities(quote.text);
|
|
32
|
+
if (!found.length) fail("CLARIFY_QUOTE_NO_LENGTH", `${path}/quote`, `the quote has no number: ${JSON.stringify(quote.text)}`);
|
|
33
|
+
if (quote.index === undefined && found.length !== 1)
|
|
34
|
+
fail("CLARIFY_QUOTE_NO_LENGTH", `${path}/quote`, `the quote has ${found.length} numbers (${found.map(item => item.text).join(", ")}); give index to pick one`);
|
|
35
|
+
const picked = found[quote.index ?? 0];
|
|
36
|
+
if (!picked) fail("CLARIFY_QUOTE_NO_LENGTH", `${path}/quote/index`, `index ${quote.index} is outside the quote's ${found.length} numbers`);
|
|
37
|
+
if (picked.ambiguous) fail("CLARIFY_QUOTE_UNIT", `${path}/quote`, `${picked.text} has no unit of its own and its chain mixes units`);
|
|
38
|
+
const bare = picked.unit === null && picked.unit_text === null && !picked.inherited;
|
|
39
|
+
if (bare) {
|
|
40
|
+
if (quote.unit === undefined) fail("CLARIFY_QUOTE_UNIT", `${path}/quote/unit`, `${picked.text} has no unit; give the length unit the user stated for it`);
|
|
41
|
+
return { value: Number(picked.value), unit: quote.unit, source: { choice: `the user wrote ${picked.text} with the shared unit ${quote.unit}` } };
|
|
42
|
+
}
|
|
43
|
+
if (quote.unit !== undefined) fail("CLARIFY_QUOTE_UNIT", `${path}/quote/unit`, `${picked.text} states its own unit; leave unit out`);
|
|
44
|
+
if (!LENGTH_UNITS.has(picked.unit)) fail("CLARIFY_QUOTE_NO_LENGTH", `${path}/quote`, `${picked.text} is not a length (${picked.unit ?? picked.unit_text})`);
|
|
45
|
+
return { source: { quote: { text: quote.text, source_id: "user-message", ...(quote.index !== undefined ? { index: quote.index } : {}) } } };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** 一个边界的毫米数,全部经求值核心(引文取量、单位换算、半径乘 2)。 */
|
|
49
|
+
function boundMillimetres(bound, radius, path) {
|
|
50
|
+
const operand = bound.quote ? quoteOperand(bound.quote, path) : { value: bound.choice.value, unit: bound.choice.unit, source: { choice: bound.choice.basis } };
|
|
51
|
+
if (bound.choice && !LENGTH_UNITS.has(bound.choice.unit)) fail("CLARIFY_QUOTE_NO_LENGTH", `${path}/choice/unit`, "the unit must be a length");
|
|
52
|
+
try {
|
|
53
|
+
return evaluateFormula({ expression: radius ? "2 * q" : "q", operands: { q: operand }, unit: "mm" }, { resolveQuote: () => true }).value;
|
|
54
|
+
} catch (error) {
|
|
55
|
+
fail("CLARIFY_QUOTE_NO_LENGTH", path, `${error.code ?? "CALC"}: ${error.message}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* dimensions → envelope。onQuote(quote, path, bound) 核对引文出处(定稿:本轮原话;复核存档:不再核对);
|
|
61
|
+
* userLimit 为 true 时只收引文。返回与 validateEnvelope 同形的包络。
|
|
62
|
+
*/
|
|
63
|
+
export function deriveEnvelope(dimensions, { userLimit, onQuote = () => {}, path = "/dimensions" } = {}) {
|
|
64
|
+
const shape = dimensions?.shape;
|
|
65
|
+
if (!Object.hasOwn(AXES, shape)) fail("CLARIFY_DIMENSIONS_MAPPING", `${path}/shape`, "shape must be box or cylinder");
|
|
66
|
+
const slots = new Map();
|
|
67
|
+
for (const [index, bound] of (dimensions.bounds ?? []).entries()) {
|
|
68
|
+
const at = `${path}/bounds/${index}`;
|
|
69
|
+
if (Boolean(bound.quote) === Boolean(bound.choice)) fail("CLARIFY_DIMENSIONS_MAPPING", at, "give exactly one of quote or choice");
|
|
70
|
+
if (bound.choice && userLimit) fail("CLARIFY_CHOICE_ON_USER_LIMIT", `${at}/choice`, "a source=user limit takes the user's words only; a system estimate belongs to a source=system limit");
|
|
71
|
+
const axes = bound.axes ?? [];
|
|
72
|
+
for (const axis of axes) if (!SHAPE_AXES[shape].has(axis)) fail("CLARIFY_DIMENSIONS_MAPPING", `${at}/axes`, `${axis} is not an axis of a ${shape}`);
|
|
73
|
+
if (axes.includes("wall") && axes.length > 1) fail("CLARIFY_DIMENSIONS_MAPPING", `${at}/axes`, "wall stands alone");
|
|
74
|
+
if (axes.includes("wall") ? !["min", "estimate"].includes(bound.bound) : !["max", "min", "exact"].includes(bound.bound))
|
|
75
|
+
fail("CLARIFY_DIMENSIONS_MAPPING", `${at}/bound`, axes.includes("wall") ? "a wall takes min or estimate" : "a size takes max, min or exact");
|
|
76
|
+
if (bound.quote) onQuote(bound.quote, `${at}/quote`, bound);
|
|
77
|
+
const radius = axes.includes("radius");
|
|
78
|
+
if (radius && axes.length > 1) fail("CLARIFY_DIMENSIONS_MAPPING", `${at}/axes`, "radius stands alone");
|
|
79
|
+
const mm = boundMillimetres(bound, radius, at);
|
|
80
|
+
for (const axis of axes) for (const kind of bound.bound === "exact" ? ["max", "min"] : [bound.bound]) {
|
|
81
|
+
const key = `${radius ? "diameter" : axis}:${kind}`;
|
|
82
|
+
if (slots.has(key)) fail("CLARIFY_DIMENSIONS_MAPPING", `${at}/axes`, `${key} is given twice`);
|
|
83
|
+
slots.set(key, mm);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const get = (axis, kind) => slots.get(`${axis}:${kind}`);
|
|
87
|
+
const missing = AXES[shape].filter(axis => get(axis, "max") === undefined);
|
|
88
|
+
if (missing.length) fail("CLARIFY_DIMENSIONS_MAPPING", `${path}/bounds`, `a ${shape} needs an upper bound on ${AXES[shape].join(", ")}; missing ${missing.join(", ")}`);
|
|
89
|
+
const mins = AXES[shape].filter(axis => get(axis, "min") !== undefined);
|
|
90
|
+
let envelope;
|
|
91
|
+
if (shape === "box") {
|
|
92
|
+
if (mins.length && mins.length !== 3) fail("CLARIFY_DIMENSIONS_MAPPING", `${path}/bounds`, "a box lower bound needs width, depth and height together");
|
|
93
|
+
envelope = { shape, max_mm: AXES.box.map(axis => get(axis, "max")), ...(mins.length ? { min_mm: AXES.box.map(axis => get(axis, "min")) } : {}) };
|
|
94
|
+
} else {
|
|
95
|
+
envelope = { shape, max_diameter_mm: get("diameter", "max"), max_height_mm: get("height", "max"),
|
|
96
|
+
...(get("diameter", "min") !== undefined ? { min_diameter_mm: get("diameter", "min") } : {}),
|
|
97
|
+
...(get("height", "min") !== undefined ? { min_height_mm: get("height", "min") } : {}) };
|
|
98
|
+
}
|
|
99
|
+
const wallMin = get("wall", "min"), wallEstimate = get("wall", "estimate");
|
|
100
|
+
// 没写保证壁厚时按 0(契约原有口径),估算只作风险提示。
|
|
101
|
+
if (wallMin !== undefined || wallEstimate !== undefined) envelope.wall_mm = { minimum: wallMin ?? 0, ...(wallEstimate !== undefined ? { estimate: wallEstimate } : {}) };
|
|
102
|
+
try { validateEnvelope(envelope); }
|
|
103
|
+
catch (error) { fail("CLARIFY_ENVELOPE_INVALID", path, `${error.message}; derived ${canonical(envelope)}`); }
|
|
104
|
+
return envelope;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const boundKey = bound => canonical(bound);
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 定稿:把模型回复里的需求补成完整快照。每个带 dimensions 的限制由代码换算出 envelope;
|
|
111
|
+
* 新写或改动的引文必须逐字出现在 userTexts(本轮用户消息与卡片答复)里,与当前需求同 ID 同形状里原样沿用的边界不再核对。
|
|
112
|
+
*/
|
|
113
|
+
export function completeRequirements(result, { current = null, userTexts = [] } = {}) {
|
|
114
|
+
const words = userTexts.filter(text => typeof text === "string").map(normalize);
|
|
115
|
+
const priorLimits = new Map((current?.limits ?? []).map(item => [item.id, item]));
|
|
116
|
+
const completed = structuredClone(result);
|
|
117
|
+
for (const [index, limit] of completed.limits.entries()) {
|
|
118
|
+
const path = `/result/limits/${index}`;
|
|
119
|
+
if (Object.hasOwn(limit, "envelope")) fail("CLARIFY_ENVELOPE_FROM_MODEL", `${path}/envelope`, "write dimensions (quotes, axes, bounds); the millimetre envelope is computed by code");
|
|
120
|
+
if (limit.dimensions === undefined) continue;
|
|
121
|
+
const prior = priorLimits.get(limit.id)?.dimensions;
|
|
122
|
+
const carried = new Set(prior?.shape === limit.dimensions.shape ? prior.bounds.map(boundKey) : []);
|
|
123
|
+
limit.envelope = deriveEnvelope(limit.dimensions, { userLimit: limit.source === "user", path: `${path}/dimensions`,
|
|
124
|
+
onQuote: (quote, at, bound) => {
|
|
125
|
+
if (carried.has(boundKey(bound))) return;
|
|
126
|
+
if (!words.some(text => text.includes(normalize(quote.text))))
|
|
127
|
+
fail("CLARIFY_QUOTE_NOT_IN_MESSAGE", at, `${JSON.stringify(quote.text)} is not word for word in this turn's user message or card answer`);
|
|
128
|
+
} });
|
|
129
|
+
}
|
|
130
|
+
return completed;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** 复核存档:envelope 与 dimensions 同在同缺,且 envelope 等于按 dimensions 重新换算的结果(不再核对引文出处)。 */
|
|
134
|
+
export function checkStoredEnvelope(limit, path) {
|
|
135
|
+
const has = [Object.hasOwn(limit, "envelope"), Object.hasOwn(limit, "dimensions")];
|
|
136
|
+
if (has[0] !== has[1]) fail("CLARIFY_ENVELOPE_MISMATCH", path, "envelope and dimensions come together: the envelope is derived from dimensions");
|
|
137
|
+
if (!has[0]) return;
|
|
138
|
+
const derived = deriveEnvelope(limit.dimensions, { userLimit: limit.source === "user", path: `${path}/dimensions` });
|
|
139
|
+
if (canonical(derived) !== canonical(limit.envelope))
|
|
140
|
+
fail("CLARIFY_ENVELOPE_MISMATCH", `${path}/envelope`, `the stored envelope ${canonical(limit.envelope)} is not the one its dimensions give (${canonical(derived)})`);
|
|
141
|
+
}
|