@my-life-buddies/cli 0.13.3 → 0.15.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 +74 -22
- package/dist/bin/buddy.js +29 -10
- package/dist/bin/buddy.js.map +2 -2
- package/dist/bin/core.js +1480 -611
- package/dist/bin/core.js.map +4 -4
- package/dist/bin/preview.js +324 -27
- package/dist/bin/preview.js.map +4 -4
- package/dist/web/app.css +65 -6
- package/dist/web/app.js +103 -39
- package/dist/web/diagnostics.js +184 -0
- package/dist/web/index.html +34 -34
- package/dist/web/widgets.css +59 -28
- package/dist/web/widgets.js +230 -76
- package/package.json +3 -3
- package/resources/README.md +6 -50
- package/resources/agent-template/package-lock.json +4 -4
- package/resources/agent-template/package.json +2 -2
- package/resources/buddy-creator/INSTALL.md +0 -25
- package/resources/buddy-creator/PATCHES.md +0 -53
- package/resources/buddy-creator/SKILL.md +0 -76
- package/resources/buddy-creator/THIRD_PARTY_NOTICES.md +0 -154
- package/resources/buddy-creator/agents/openai.yaml +0 -4
- package/resources/buddy-creator/assets/licenses/bail.txt +0 -26
- package/resources/buddy-creator/assets/licenses/ccount.txt +0 -26
- package/resources/buddy-creator/assets/licenses/character-entities-html4.txt +0 -26
- package/resources/buddy-creator/assets/licenses/character-entities-legacy.txt +0 -26
- package/resources/buddy-creator/assets/licenses/character-entities.txt +0 -26
- package/resources/buddy-creator/assets/licenses/character-reference-invalid.txt +0 -26
- package/resources/buddy-creator/assets/licenses/comma-separated-tokens.txt +0 -26
- package/resources/buddy-creator/assets/licenses/debug.txt +0 -24
- package/resources/buddy-creator/assets/licenses/decode-named-character-reference.txt +0 -26
- package/resources/buddy-creator/assets/licenses/dequal.txt +0 -25
- package/resources/buddy-creator/assets/licenses/devlop.txt +0 -26
- package/resources/buddy-creator/assets/licenses/escape-string-regexp.txt +0 -13
- package/resources/buddy-creator/assets/licenses/estree-util-is-identifier-name.txt +0 -26
- package/resources/buddy-creator/assets/licenses/extend.txt +0 -27
- package/resources/buddy-creator/assets/licenses/hast-util-to-jsx-runtime.txt +0 -26
- package/resources/buddy-creator/assets/licenses/hast-util-whitespace.txt +0 -26
- package/resources/buddy-creator/assets/licenses/html-url-attributes.txt +0 -25
- package/resources/buddy-creator/assets/licenses/inline-style-parser.txt +0 -13
- package/resources/buddy-creator/assets/licenses/is-alphabetical.txt +0 -26
- package/resources/buddy-creator/assets/licenses/is-alphanumerical.txt +0 -26
- package/resources/buddy-creator/assets/licenses/is-decimal.txt +0 -26
- package/resources/buddy-creator/assets/licenses/is-hexadecimal.txt +0 -26
- package/resources/buddy-creator/assets/licenses/is-plain-obj.txt +0 -13
- package/resources/buddy-creator/assets/licenses/longest-streak.txt +0 -26
- package/resources/buddy-creator/assets/licenses/markdown-table.txt +0 -26
- package/resources/buddy-creator/assets/licenses/mdast-util-find-and-replace.txt +0 -26
- package/resources/buddy-creator/assets/licenses/mdast-util-from-markdown.txt +0 -26
- package/resources/buddy-creator/assets/licenses/mdast-util-gfm-autolink-literal.txt +0 -26
- package/resources/buddy-creator/assets/licenses/mdast-util-gfm-footnote.txt +0 -26
- package/resources/buddy-creator/assets/licenses/mdast-util-gfm-strikethrough.txt +0 -26
- package/resources/buddy-creator/assets/licenses/mdast-util-gfm-table.txt +0 -26
- package/resources/buddy-creator/assets/licenses/mdast-util-gfm-task-list-item.txt +0 -26
- package/resources/buddy-creator/assets/licenses/mdast-util-gfm.txt +0 -26
- package/resources/buddy-creator/assets/licenses/mdast-util-mdx-expression.txt +0 -26
- package/resources/buddy-creator/assets/licenses/mdast-util-mdx-jsx.txt +0 -26
- package/resources/buddy-creator/assets/licenses/mdast-util-mdxjs-esm.txt +0 -26
- package/resources/buddy-creator/assets/licenses/mdast-util-phrasing.txt +0 -27
- package/resources/buddy-creator/assets/licenses/mdast-util-to-hast.txt +0 -26
- package/resources/buddy-creator/assets/licenses/mdast-util-to-markdown.txt +0 -26
- package/resources/buddy-creator/assets/licenses/mdast-util-to-string.txt +0 -26
- package/resources/buddy-creator/assets/licenses/micromark-core-commonmark.txt +0 -26
- package/resources/buddy-creator/assets/licenses/micromark-extension-gfm-autolink-literal.txt +0 -26
- package/resources/buddy-creator/assets/licenses/micromark-extension-gfm-footnote.txt +0 -26
- package/resources/buddy-creator/assets/licenses/micromark-extension-gfm-strikethrough.txt +0 -26
- package/resources/buddy-creator/assets/licenses/micromark-extension-gfm-table.txt +0 -26
- package/resources/buddy-creator/assets/licenses/micromark-extension-gfm-tagfilter.txt +0 -26
- package/resources/buddy-creator/assets/licenses/micromark-extension-gfm-task-list-item.txt +0 -26
- package/resources/buddy-creator/assets/licenses/micromark-extension-gfm.txt +0 -26
- package/resources/buddy-creator/assets/licenses/micromark-factory-destination.txt +0 -26
- package/resources/buddy-creator/assets/licenses/micromark-factory-label.txt +0 -26
- package/resources/buddy-creator/assets/licenses/micromark-factory-space.txt +0 -26
- package/resources/buddy-creator/assets/licenses/micromark-factory-title.txt +0 -26
- package/resources/buddy-creator/assets/licenses/micromark-factory-whitespace.txt +0 -26
- package/resources/buddy-creator/assets/licenses/micromark-util-character.txt +0 -26
- package/resources/buddy-creator/assets/licenses/micromark-util-chunked.txt +0 -26
- package/resources/buddy-creator/assets/licenses/micromark-util-classify-character.txt +0 -26
- package/resources/buddy-creator/assets/licenses/micromark-util-combine-extensions.txt +0 -26
- package/resources/buddy-creator/assets/licenses/micromark-util-decode-numeric-character-reference.txt +0 -26
- package/resources/buddy-creator/assets/licenses/micromark-util-decode-string.txt +0 -26
- package/resources/buddy-creator/assets/licenses/micromark-util-encode.txt +0 -26
- package/resources/buddy-creator/assets/licenses/micromark-util-html-tag-name.txt +0 -26
- package/resources/buddy-creator/assets/licenses/micromark-util-normalize-identifier.txt +0 -26
- package/resources/buddy-creator/assets/licenses/micromark-util-resolve-all.txt +0 -26
- package/resources/buddy-creator/assets/licenses/micromark-util-sanitize-uri.txt +0 -26
- package/resources/buddy-creator/assets/licenses/micromark-util-subtokenize.txt +0 -26
- package/resources/buddy-creator/assets/licenses/micromark-util-symbol.txt +0 -26
- package/resources/buddy-creator/assets/licenses/micromark-util-types.txt +0 -26
- package/resources/buddy-creator/assets/licenses/micromark.txt +0 -26
- package/resources/buddy-creator/assets/licenses/ms.txt +0 -25
- package/resources/buddy-creator/assets/licenses/parse-entities.txt +0 -26
- package/resources/buddy-creator/assets/licenses/phosphor-icons--react.txt +0 -25
- package/resources/buddy-creator/assets/licenses/property-information.txt +0 -26
- package/resources/buddy-creator/assets/licenses/react-dom.txt +0 -25
- package/resources/buddy-creator/assets/licenses/react-markdown.txt +0 -25
- package/resources/buddy-creator/assets/licenses/react.txt +0 -25
- package/resources/buddy-creator/assets/licenses/remark-gfm.txt +0 -26
- package/resources/buddy-creator/assets/licenses/remark-parse.txt +0 -25
- package/resources/buddy-creator/assets/licenses/remark-rehype.txt +0 -26
- package/resources/buddy-creator/assets/licenses/remark-stringify.txt +0 -25
- package/resources/buddy-creator/assets/licenses/scheduler.txt +0 -25
- package/resources/buddy-creator/assets/licenses/space-separated-tokens.txt +0 -26
- package/resources/buddy-creator/assets/licenses/stringify-entities.txt +0 -26
- package/resources/buddy-creator/assets/licenses/style-to-js.txt +0 -26
- package/resources/buddy-creator/assets/licenses/style-to-object.txt +0 -26
- package/resources/buddy-creator/assets/licenses/trim-lines.txt +0 -26
- package/resources/buddy-creator/assets/licenses/trough.txt +0 -25
- package/resources/buddy-creator/assets/licenses/types--debug.txt +0 -25
- package/resources/buddy-creator/assets/licenses/types--estree-jsx.txt +0 -25
- package/resources/buddy-creator/assets/licenses/types--estree.txt +0 -25
- package/resources/buddy-creator/assets/licenses/types--hast.txt +0 -25
- package/resources/buddy-creator/assets/licenses/types--mdast.txt +0 -25
- package/resources/buddy-creator/assets/licenses/types--ms.txt +0 -25
- package/resources/buddy-creator/assets/licenses/types--unist.txt +0 -25
- package/resources/buddy-creator/assets/licenses/ungap--structured-clone.txt +0 -19
- package/resources/buddy-creator/assets/licenses/unified.txt +0 -25
- package/resources/buddy-creator/assets/licenses/unist-util-is.txt +0 -26
- package/resources/buddy-creator/assets/licenses/unist-util-position.txt +0 -26
- package/resources/buddy-creator/assets/licenses/unist-util-stringify-position.txt +0 -26
- package/resources/buddy-creator/assets/licenses/unist-util-visit-parents.txt +0 -26
- package/resources/buddy-creator/assets/licenses/unist-util-visit.txt +0 -26
- package/resources/buddy-creator/assets/licenses/vfile-message.txt +0 -26
- package/resources/buddy-creator/assets/licenses/vfile.txt +0 -25
- package/resources/buddy-creator/assets/licenses/zwitch.txt +0 -26
- package/resources/buddy-creator/assets/preview/assets/index-B5VmkoSx.js +0 -39
- package/resources/buddy-creator/assets/preview/assets/index-DWKP6b-3.css +0 -4
- package/resources/buddy-creator/assets/preview/brand-logo.png +0 -0
- package/resources/buddy-creator/assets/preview/brand.css +0 -100
- package/resources/buddy-creator/assets/preview/index.html +0 -7
- package/resources/buddy-creator/assets/preview/reader.css +0 -165
- package/resources/buddy-creator/assets/preview/reader.js +0 -135
- package/resources/buddy-creator/assets/preview/widgets.css +0 -1
- package/resources/buddy-creator/assets/preview/widgets.js +0 -168
- package/resources/buddy-creator/assets/widget-reference/conversation.png +0 -0
- package/resources/buddy-creator/assets/widget-reference/expanded.png +0 -0
- package/resources/buddy-creator/assets/widget-v2/app.js +0 -62
- package/resources/buddy-creator/assets/widget-v2/style.css +0 -1
- package/resources/buddy-creator/assets/widget-v3/app.js +0 -65
- package/resources/buddy-creator/assets/widget-v3/style.css +0 -103
- package/resources/buddy-creator/assets/widget-v4/app.js +0 -66
- package/resources/buddy-creator/assets/widget-v4/style.css +0 -187
- package/resources/buddy-creator/references/artifact-schema.md +0 -70
- package/resources/buddy-creator/references/catalog.json +0 -401
- package/resources/buddy-creator/references/definition.md +0 -24
- package/resources/buddy-creator/references/dialogue-examples.md +0 -133
- package/resources/buddy-creator/references/host-guide.md +0 -219
- package/resources/buddy-creator/references/interview.md +0 -235
- package/resources/buddy-creator/references/knowledge.md +0 -40
- package/resources/buddy-creator/references/methods.md +0 -77
- package/resources/buddy-creator/references/opening.md +0 -14
- package/resources/buddy-creator/references/preview-panel.md +0 -66
- package/resources/buddy-creator/references/recovery.md +0 -46
- package/resources/buddy-creator/references/service.md +0 -76
- package/resources/buddy-creator/references/widgets-protocol.md +0 -83
- package/resources/buddy-creator/references/widgets.md +0 -99
- package/resources/buddy-creator/scripts/buddy.py +0 -108
- package/resources/buddy-creator/scripts/buddy_client.py +0 -67
- package/resources/buddy-creator/scripts/buddy_core.py +0 -865
- package/resources/buddy-creator/scripts/completion.py +0 -296
- package/resources/buddy-creator/scripts/interview.py +0 -215
- package/resources/buddy-creator/scripts/preview.py +0 -617
- package/resources/buddy-creator/scripts/preview_panel.py +0 -80
- package/resources/buddy-creator/scripts/sources.py +0 -239
- package/resources/buddy-creator/scripts/widget_render.py +0 -66
- package/resources/buddy-creator/scripts/widget_render_v1.py +0 -176
- package/resources/buddy-creator/scripts/widget_render_v2.py +0 -98
- package/resources/buddy-creator/scripts/widget_render_v3.py +0 -86
- package/resources/buddy-creator/scripts/widget_render_v4.py +0 -121
- package/resources/buddy-creator/scripts/widgets.py +0 -227
- package/resources/buddy-creator/version.json +0 -8
- package/resources/buddy-creator.manifest.json +0 -791
- package/resources/dada-object-avatar/SKILL.md +0 -37
- package/resources/dada-object-avatar/agents/openai.yaml +0 -4
- package/resources/dada-object-avatar/assets/examples/cat.png +0 -0
- package/resources/dada-object-avatar/assets/examples/cooking.png +0 -0
- package/resources/dada-object-avatar/assets/examples/divination.png +0 -0
- package/resources/dada-object-avatar/assets/examples/doctor.png +0 -0
- package/resources/dada-object-avatar/assets/examples/relationship.png +0 -0
- package/resources/dada-object-avatar/assets/examples/work.png +0 -0
- package/resources/dada-object-avatar/assets/preview.png +0 -0
- package/resources/dada-object-avatar/references/evaluation-rubric.md +0 -22
- package/resources/dada-object-avatar/references/examples.json +0 -8
- package/resources/dada-object-avatar/references/prompt-template.md +0 -27
- package/resources/dada-object-avatar/references/style-system.md +0 -54
- package/resources/dada-object-avatar/references/subject-design.md +0 -23
- package/resources/dada-object-avatar/references/training-guide.md +0 -14
- package/resources/dada-object-avatar/references/training-log.jsonl +0 -0
- package/resources/dada-object-avatar/references/training-record.schema.json +0 -20
- package/resources/dada-object-avatar/scripts/choose_gaze.py +0 -36
- package/resources/dada-object-avatar/scripts/record_example.py +0 -22
- package/resources/dada-object-avatar/scripts/validate_png.py +0 -52
- package/resources/dada-object-avatar.manifest.json +0 -85
|
@@ -1,296 +0,0 @@
|
|
|
1
|
-
"""Create local deliverables from confirmed versions. No developer handoff."""
|
|
2
|
-
import hashlib
|
|
3
|
-
import copy
|
|
4
|
-
import html
|
|
5
|
-
import json
|
|
6
|
-
import os
|
|
7
|
-
import shutil
|
|
8
|
-
import tempfile
|
|
9
|
-
from datetime import datetime, timezone
|
|
10
|
-
from pathlib import Path
|
|
11
|
-
from buddy_core import BuddyError
|
|
12
|
-
|
|
13
|
-
LABELS = {'definition': '定义', 'knowledge': '知识', 'methods': '方法', 'service': '服务'}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def _bytes(value):
|
|
17
|
-
return json.dumps(value, ensure_ascii=False, sort_keys=True, indent=2, allow_nan=False).encode('utf-8')
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def _hash(raw):
|
|
21
|
-
return hashlib.sha256(raw).hexdigest()
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def _atomic(path, raw):
|
|
25
|
-
path = Path(path)
|
|
26
|
-
path.parent.mkdir(parents=True, exist_ok=True)
|
|
27
|
-
fd, temporary = tempfile.mkstemp(prefix='.' + path.name + '-', dir=str(path.parent))
|
|
28
|
-
try:
|
|
29
|
-
with os.fdopen(fd, 'wb') as stream:
|
|
30
|
-
stream.write(raw)
|
|
31
|
-
stream.flush()
|
|
32
|
-
os.fsync(stream.fileno())
|
|
33
|
-
os.replace(temporary, str(path))
|
|
34
|
-
finally:
|
|
35
|
-
if os.path.exists(temporary):
|
|
36
|
-
os.unlink(temporary)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def _confirmed(state, object_id):
|
|
40
|
-
artifact = state.get('artifacts', {}).get(object_id)
|
|
41
|
-
return bool(artifact and any(record.get('objectId') == object_id and record.get('hash') == artifact['hash']
|
|
42
|
-
and not record.get('invalidatedBy') and record.get('decision') == 'confirmed'
|
|
43
|
-
for record in state.get('confirmations', [])))
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def _ready(state, catalog):
|
|
47
|
-
import widgets
|
|
48
|
-
return not state.get('paused') and not state.get('pendingTurnId') and all(
|
|
49
|
-
_confirmed(state, '%s.%d' % (stage, index + 1))
|
|
50
|
-
for stage in catalog['stages'] for index in range(len(catalog['chapters'][stage]))) and widgets.ready(state)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def widget_files(workspace, state):
|
|
54
|
-
import widgets
|
|
55
|
-
import widget_render
|
|
56
|
-
if not widgets.enabled(state):
|
|
57
|
-
return {}
|
|
58
|
-
catalog = state['artifacts'][widgets.CATALOG]
|
|
59
|
-
files = {'widgets/catalog.json': _bytes(catalog)}
|
|
60
|
-
guide = ['# 小挂件交互设计', '', '已完成设计验收;真实会话数据接入、生产 H5 和端到端功能验收由后续开发完成。', '',
|
|
61
|
-
'小挂件只读。筛选、切换、折叠和浮层只改变阅读状态;编辑统一通过对话完成。',
|
|
62
|
-
'展开页宽度:屏幕逻辑宽度 − 32;高度:屏幕逻辑高度 × 65%;内容区上下滚动。标题建议 16 Medium、最大 20;正文 16 Regular。', '', catalog['markdown'], '']
|
|
63
|
-
if not widgets.items(state):
|
|
64
|
-
guide.extend(['本作品已明确确认不使用小挂件。', catalog['data']['noWidgetsReason'], ''])
|
|
65
|
-
for item in widgets.items(state):
|
|
66
|
-
prototype = state['artifacts'][widgets.object_id(item['id'])]
|
|
67
|
-
acceptance = state['artifacts'][widgets.object_id(item['id'], 'acceptance')]
|
|
68
|
-
widget_render.require_assets(workspace, state, prototype)
|
|
69
|
-
prefix = 'widgets/' + item['id'] + '/'
|
|
70
|
-
for name, raw in widget_render.files(prototype, item).items():
|
|
71
|
-
files[prefix + name] = raw
|
|
72
|
-
files[prefix + 'prototype.json'] = _bytes(prototype)
|
|
73
|
-
files[prefix + 'acceptance.json'] = _bytes({'artifact': acceptance, 'automaticCheck': widget_render.asset_status(workspace, prototype, state),
|
|
74
|
-
'developerConfirmations': [c for c in state['confirmations'] if c['objectId'] in (prototype['id'], acceptance['id']) and not c.get('invalidatedBy')]})
|
|
75
|
-
review_labels = {'booklet-fit': '符合搭子需求', 'reading-order': '阅读顺序、长内容与区分度',
|
|
76
|
-
'chat-consistency': '对话卡片与展开页一致', 'conversation-update': '数据随对话逐步更新',
|
|
77
|
-
'read-only': '只读交互边界'}
|
|
78
|
-
guide.extend(['## ' + item['name'], '', prototype['markdown'], '',
|
|
79
|
-
'[打开可阅读原型](' + prefix + 'index.html)', '',
|
|
80
|
-
'原型含首次生成、逐步补充、对话更新三态;每态提供对话卡片与展开页 SVG。H5 原型通过 ?view=expanded&state=partial 等参数查看。', '',
|
|
81
|
-
'### 设计验收', '', '当前原型及本版设计验收均已确认。原始确认稿与确认凭据保留在对应 JSON 文件中。', '',
|
|
82
|
-
'原型版本:' + prototype['hash'], ''])
|
|
83
|
-
for key in widgets.REVIEW_CHECKS:
|
|
84
|
-
guide.extend(['#### ' + review_labels[key], '', acceptance['data']['reviews'][key], ''])
|
|
85
|
-
files['WIDGET_DESIGN.md'] = '\n'.join(guide).encode('utf-8')
|
|
86
|
-
return files
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
def _line_chunks(text, columns=32, limit=3):
|
|
90
|
-
text = ' '.join(str(text or '待补充').split())
|
|
91
|
-
lines, line, width = [], '', 0
|
|
92
|
-
for char in text:
|
|
93
|
-
char_width = 1 if ord(char) < 128 else 2
|
|
94
|
-
if width + char_width > columns * 2:
|
|
95
|
-
lines.append(line)
|
|
96
|
-
line, width = '', 0
|
|
97
|
-
line += char
|
|
98
|
-
width += char_width
|
|
99
|
-
if line:
|
|
100
|
-
lines.append(line)
|
|
101
|
-
if len(lines) > limit:
|
|
102
|
-
lines = lines[:limit]
|
|
103
|
-
lines[-1] = lines[-1][:-1] + '…'
|
|
104
|
-
return lines
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
def _blueprint(state):
|
|
108
|
-
blueprint = copy.deepcopy(state['artifacts'].get('service.blueprint', {}).get('data') or {})
|
|
109
|
-
transitions = blueprint.setdefault('transitions', {})
|
|
110
|
-
for artifact in state['artifacts'].values():
|
|
111
|
-
if artifact.get('kind') == 'transition' and artifact.get('data'):
|
|
112
|
-
path_id = artifact['id'].removeprefix('transition.')
|
|
113
|
-
transitions[path_id] = dict(transitions.get(path_id, {}), **artifact['data'])
|
|
114
|
-
return blueprint
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
def _diagram(state):
|
|
118
|
-
blueprint = _blueprint(state)
|
|
119
|
-
stages = blueprint.get('stages', {})
|
|
120
|
-
transitions = blueprint.get('transitions', {})
|
|
121
|
-
boxes = []
|
|
122
|
-
for key, title, meaning, y in [('acquisition', '获客期', '先免费体验', 90),
|
|
123
|
-
('paid', '付费期', '订阅后持续获得帮助 · AI 对话不限次数', 325),
|
|
124
|
-
('maintenance', '维持期', '未订阅或到期未续费 · 保留基础对话与历史', 560)]:
|
|
125
|
-
config = stages.get(key, {})
|
|
126
|
-
lines = _line_chunks(config.get('result') or config.get('service'), columns=32, limit=3)
|
|
127
|
-
content = ''.join('<tspan x="76" dy="25">%s</tspan>' % html.escape(line) for line in lines)
|
|
128
|
-
boxes.append('<g><rect x="48" y="%d" width="540" height="160" rx="12" fill="#20271b" stroke="#556446"/>'
|
|
129
|
-
'<text x="76" y="%d" fill="#b8d879" font-size="24" font-weight="700">%s</text>'
|
|
130
|
-
'<text x="76" y="%d" fill="#bac2b0" font-size="14">%s</text>'
|
|
131
|
-
'<text y="%d" fill="#edf0e8" font-size="17">%s</text></g>' %
|
|
132
|
-
(y, y + 35, title, y + 61, html.escape(meaning), y + 71, content))
|
|
133
|
-
cycle = blueprint.get('billingCycle') or {}
|
|
134
|
-
units = {'day': '天', 'week': '周', 'month': '个月', 'year': '年'}
|
|
135
|
-
duration = str(cycle.get('count', '')) + units.get(cycle.get('unit'), '') if isinstance(cycle, dict) else str(cycle)
|
|
136
|
-
terms = ('订阅:¥%s / %s' % (blueprint.get('price', '待定'), duration or '待定'))
|
|
137
|
-
def caption(key, fallback):
|
|
138
|
-
return html.escape(str(transitions.get(key, {}).get('title') or fallback))
|
|
139
|
-
return ('<svg xmlns="http://www.w3.org/2000/svg" width="980" height="810" viewBox="0 0 980 810" role="img" aria-labelledby="title desc">'
|
|
140
|
-
'<title id="title">Buddy 服务模式</title><desc id="desc">免费体验、付费订阅和维持三阶段,以及五条用户路径。完整约定见服务手册。</desc>'
|
|
141
|
-
'<defs><marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">'
|
|
142
|
-
'<path d="M0 0L10 5L0 10Z" fill="#b8d879"/></marker></defs>'
|
|
143
|
-
'<rect width="980" height="810" rx="16" fill="#151a13"/><g font-family="system-ui,sans-serif">'
|
|
144
|
-
'<text x="48" y="43" fill="#edf0e8" font-size="25" font-weight="700">Buddy · 服务模式</text>'
|
|
145
|
-
'<text x="48" y="69" fill="#bac2b0" font-size="15">%s</text>%s'
|
|
146
|
-
'<g fill="none" stroke="#94aa76" stroke-width="2" marker-end="url(#arrow)">'
|
|
147
|
-
'<path d="M260 250V325"/><path d="M260 485V560"/>'
|
|
148
|
-
'<path d="M588 150H902V640H588"/>'
|
|
149
|
-
'<path d="M588 355H682V445H588"/>'
|
|
150
|
-
'<path d="M588 670H780V410H588" stroke-dasharray="6 5"/></g>'
|
|
151
|
-
'<g fill="#dce8ca" font-size="16">'
|
|
152
|
-
'<text x="278" y="292">%s</text><text x="278" y="527">%s</text>'
|
|
153
|
-
'<text x="808" y="284">%s</text><text x="693" y="390">%s</text>'
|
|
154
|
-
'<text x="793" y="555">%s</text></g>'
|
|
155
|
-
'<text x="48" y="769" fill="#bac2b0" font-size="15">阶段卡片展示摘要;具体服务、权益和转换条件以已确认手册为准。</text></g></svg>') % (
|
|
156
|
-
html.escape(terms), ''.join(boxes), caption('acquisition-paid', '体验后订阅'),
|
|
157
|
-
caption('paid-maintenance', '到期未续费'), caption('acquisition-maintenance', '暂不订阅'),
|
|
158
|
-
caption('paid-paid', '续费'), caption('maintenance-paid', '恢复订阅'))
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
def snapshot(workspace, state):
|
|
162
|
-
path = Path(workspace) / 'completion.json'
|
|
163
|
-
if not path.exists() or state.get('pendingTurnId') or state.get('paused'):
|
|
164
|
-
return None
|
|
165
|
-
try:
|
|
166
|
-
record = json.loads(path.read_text(encoding='utf-8'))
|
|
167
|
-
if record.get('revision') != state.get('revision'):
|
|
168
|
-
return None
|
|
169
|
-
checked = return_to_caller(workspace, state, {})
|
|
170
|
-
if checked.get('completion'):
|
|
171
|
-
record.update(checked['completion'])
|
|
172
|
-
result = {key: record.get(key) for key in ('revision', 'status', 'message', 'fileCount', 'updatedAt')}
|
|
173
|
-
if checked.get('directive') == 'creator_complete':
|
|
174
|
-
inventory = json.loads((Path(record['directory']) / 'MANIFEST.json').read_text(encoding='utf-8'))
|
|
175
|
-
result['files'] = [entry['path'] for entry in inventory['files']]
|
|
176
|
-
return result
|
|
177
|
-
except (OSError, ValueError, KeyError):
|
|
178
|
-
return None
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
def return_to_caller(workspace, state, result):
|
|
182
|
-
"""Report the Skill boundary, not the caller's next implementation step. Read-only."""
|
|
183
|
-
catalog = json.loads((Path(__file__).resolve().parent.parent / 'references' / 'catalog.json').read_text(encoding='utf-8'))
|
|
184
|
-
if not _ready(state, catalog):
|
|
185
|
-
return result
|
|
186
|
-
workspace = Path(workspace).resolve()
|
|
187
|
-
record = None
|
|
188
|
-
try:
|
|
189
|
-
record = json.loads((workspace / 'completion.json').read_text(encoding='utf-8'))
|
|
190
|
-
revision = state['revision']
|
|
191
|
-
if not isinstance(revision, str) or not revision or any(c not in 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-' for c in revision):
|
|
192
|
-
raise ValueError('版本身份无效。')
|
|
193
|
-
if not isinstance(record, dict) or record.get('revision') != revision or record.get('status') != 'ready':
|
|
194
|
-
raise ValueError('当前确认版本的成果尚未生成成功。')
|
|
195
|
-
directory = workspace / 'deliverables' / revision
|
|
196
|
-
manual, manifest = directory / 'BUDDY_MANUAL.md', directory / 'MANIFEST.json'
|
|
197
|
-
if (directory.parent.is_symlink() or directory.is_symlink() or manual.is_symlink() or manifest.is_symlink()
|
|
198
|
-
or record.get('directory') != str(directory) or record.get('manualPath') != str(manual)):
|
|
199
|
-
raise ValueError('成果路径与当前作品不一致。')
|
|
200
|
-
inventory = json.loads(manifest.read_text(encoding='utf-8'))
|
|
201
|
-
entries = [f for f in inventory['files'] if f['path'] == 'BUDDY_MANUAL.md']
|
|
202
|
-
if (inventory.get('revision') != revision or inventory.get('buddyId') != state['buddyId']
|
|
203
|
-
or inventory.get('complete') is not True or len(entries) != 1
|
|
204
|
-
or _hash(manual.read_bytes()) != entries[0]['sha256']):
|
|
205
|
-
raise ValueError('手册与已确认成果不一致。')
|
|
206
|
-
import widgets
|
|
207
|
-
if widgets.enabled(state):
|
|
208
|
-
required = {'BUDDY_MANUAL.md', 'SERVICE_MODEL.svg', 'service-blueprint.json', 'versions.json', 'sources.json'} | {'booklets/' + s + '.md' for s in catalog['stages']} | set(widget_files(workspace, state))
|
|
209
|
-
if {entry['path'] for entry in inventory['files']} != required or len(inventory['files']) != len(required):
|
|
210
|
-
raise ValueError('交付文件清单缺项或重复。')
|
|
211
|
-
for entry in inventory['files']:
|
|
212
|
-
target = directory / entry['path']
|
|
213
|
-
if any(p.is_symlink() for p in [target, *target.parents] if p != workspace.parent) or _hash(target.read_bytes()) != entry['sha256']:
|
|
214
|
-
raise ValueError('交付文件缺失或被修改:' + entry['path'])
|
|
215
|
-
result['completion'] = {key: record[key] for key in
|
|
216
|
-
('revision', 'status', 'directory', 'manualPath', 'fileCount', 'message', 'updatedAt')}
|
|
217
|
-
result['directive'] = 'creator_complete'
|
|
218
|
-
except (OSError, ValueError, KeyError, TypeError, BuddyError) as error:
|
|
219
|
-
# Never hand off a stale path or a success-shaped but unusable export.
|
|
220
|
-
result['completion'] = {'revision': state.get('revision'), 'status': 'failed',
|
|
221
|
-
'message': '四册确认已保留,当前成果缺失、失效或被修改;先用 finalize 恢复交付。',
|
|
222
|
-
'error': (record.get('error') if isinstance(record, dict)
|
|
223
|
-
and record.get('revision') == state['revision'] else None) or str(error)}
|
|
224
|
-
result['directive'] = 'finalize_required'
|
|
225
|
-
return result
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
def finalize(workspace, state):
|
|
229
|
-
workspace = Path(workspace)
|
|
230
|
-
catalog = json.loads((Path(__file__).resolve().parent.parent / 'references' / 'catalog.json').read_text(encoding='utf-8'))
|
|
231
|
-
if not _ready(state, catalog):
|
|
232
|
-
return None
|
|
233
|
-
revision = str(state['revision'])
|
|
234
|
-
if not revision or any(char not in 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-' for char in revision):
|
|
235
|
-
raise ValueError('REVISION_INVALID: 无法识别当前版本身份。')
|
|
236
|
-
output = workspace / 'deliverables' / revision
|
|
237
|
-
record = {'revision': revision, 'updatedAt': datetime.now(timezone.utc).isoformat()}
|
|
238
|
-
staging = None
|
|
239
|
-
try:
|
|
240
|
-
files, manual = {}, ['# Buddy 创作手册', '', '搭子:' + state['buddyId'], '']
|
|
241
|
-
for stage in catalog['stages']:
|
|
242
|
-
parts = ['# ' + LABELS[stage] + '手册', '']
|
|
243
|
-
for index, title in enumerate(catalog['chapters'][stage]):
|
|
244
|
-
artifact = state['artifacts']['%s.%d' % (stage, index + 1)]
|
|
245
|
-
parts.extend(['## %d. %s' % (index + 1, title), '', artifact['markdown'], ''])
|
|
246
|
-
if artifact.get('unresolved'):
|
|
247
|
-
parts.extend(['待补充:' + ';'.join(artifact['unresolved']), ''])
|
|
248
|
-
text = '\n'.join(parts)
|
|
249
|
-
files['booklets/%s.md' % stage] = text.encode('utf-8')
|
|
250
|
-
manual.extend([text, ''])
|
|
251
|
-
manual.extend(['## 服务模式图', '', '', '',
|
|
252
|
-
'本手册记录已确认的创作设计,后续开发、内容核实和实际履约仍按具体项目推进。', ''])
|
|
253
|
-
import widgets
|
|
254
|
-
if widgets.enabled(state):
|
|
255
|
-
files.update(widget_files(workspace, state))
|
|
256
|
-
manual.extend(['## 小挂件交互设计', '', '[小挂件清单、原型与设计验收](WIDGET_DESIGN.md)', '',
|
|
257
|
-
'只读 H5 模板由开发者前置定义,对话负责创建与逐步更新实例数据。', ''])
|
|
258
|
-
files['BUDDY_MANUAL.md'] = '\n'.join(manual).encode('utf-8')
|
|
259
|
-
files['SERVICE_MODEL.svg'] = _diagram(state).encode('utf-8')
|
|
260
|
-
files['service-blueprint.json'] = _bytes({
|
|
261
|
-
'data': _blueprint(state),
|
|
262
|
-
'sourceArtifact': {key: state['artifacts'].get('service.blueprint', {}).get(key) for key in ('id', 'hash')},
|
|
263
|
-
'transitionArtifacts': [{key: artifact.get(key) for key in ('id', 'hash')} for artifact in state['artifacts'].values() if artifact.get('kind') == 'transition']})
|
|
264
|
-
files['versions.json'] = _bytes({'buddyId': state['buddyId'], 'revision': revision, 'bookletRevision': widgets.booklet_revision(state),
|
|
265
|
-
'artifacts': state['artifacts'], 'confirmations': state['confirmations']})
|
|
266
|
-
files['sources.json'] = _bytes(list(state.get('sources', {}).values()))
|
|
267
|
-
files['MANIFEST.json'] = _bytes({'format': 'buddy-creator-1', 'buddyId': state['buddyId'],
|
|
268
|
-
'revision': revision, 'complete': True,
|
|
269
|
-
'files': [{'path': path, 'sha256': _hash(raw), 'bytes': len(raw)} for path, raw in files.items()]})
|
|
270
|
-
output.parent.mkdir(parents=True, exist_ok=True)
|
|
271
|
-
staging = Path(tempfile.mkdtemp(prefix='.build-', dir=str(output.parent)))
|
|
272
|
-
for path, raw in files.items():
|
|
273
|
-
_atomic(staging / path, raw)
|
|
274
|
-
if output.exists():
|
|
275
|
-
# Reuse intact outputs; preserve manually edited exports as a separate directory.
|
|
276
|
-
intact = all((output / path).is_file() and (output / path).read_bytes() == raw for path, raw in files.items())
|
|
277
|
-
if intact:
|
|
278
|
-
shutil.rmtree(str(staging))
|
|
279
|
-
staging = None
|
|
280
|
-
else:
|
|
281
|
-
saved = output.with_name(output.name + '-previous-' + datetime.now().strftime('%Y%m%d%H%M%S%f'))
|
|
282
|
-
os.replace(str(output), str(saved))
|
|
283
|
-
os.replace(str(staging), str(output))
|
|
284
|
-
staging = None
|
|
285
|
-
else:
|
|
286
|
-
os.replace(str(staging), str(output))
|
|
287
|
-
staging = None
|
|
288
|
-
record.update(status='ready', directory=str(output), manualPath=str(output / 'BUDDY_MANUAL.md'),
|
|
289
|
-
fileCount=len(files), message='四册及小挂件设计已确认,手册、原型和验收记录已保存在本机。' if widgets.enabled(state) else '四册已确认,创作手册和服务模式图已保存在本机。')
|
|
290
|
-
except Exception as error:
|
|
291
|
-
record.update(status='failed', error=str(error), message='四册确认已保留,本地成果生成需要重试。')
|
|
292
|
-
finally:
|
|
293
|
-
if staging:
|
|
294
|
-
shutil.rmtree(str(staging), ignore_errors=True)
|
|
295
|
-
_atomic(workspace / 'completion.json', _bytes(record))
|
|
296
|
-
return record
|
|
@@ -1,215 +0,0 @@
|
|
|
1
|
-
"""Bound interview depth by both a concrete gap and its parent target.
|
|
2
|
-
|
|
3
|
-
The host assesses meaning and novelty with evidence; code owns counters, bounds
|
|
4
|
-
and history. No model calls and no keyword-based inference of user consent.
|
|
5
|
-
"""
|
|
6
|
-
from __future__ import annotations
|
|
7
|
-
|
|
8
|
-
import copy
|
|
9
|
-
import re
|
|
10
|
-
|
|
11
|
-
from buddy_core import CLOSED, catalog, evidence, require, text
|
|
12
|
-
|
|
13
|
-
POLICY = catalog()['interviewPolicy']
|
|
14
|
-
GAP_LIMIT = POLICY['gapMaxAnswers']
|
|
15
|
-
NO_GAIN_LIMIT = POLICY['noGainLimit']
|
|
16
|
-
|
|
17
|
-
def tracked(target_id):
|
|
18
|
-
# Source discovery still ends by the creator's explicit choice.
|
|
19
|
-
return target_id != 'K02'
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def initial_id(target_id):
|
|
23
|
-
return target_id + '.initial'
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def ensure(state, target_id):
|
|
27
|
-
history = state.setdefault('interview', {'targets': {}, 'gaps': {}})
|
|
28
|
-
if target_id not in history['targets']:
|
|
29
|
-
answers = state['targets'][target_id]['answerInputIds']
|
|
30
|
-
history['targets'][target_id] = {
|
|
31
|
-
'windowStart': 0, 'noGainStreak': 0, 'lastGain': False,
|
|
32
|
-
'stopReason': None, 'reopens': [],
|
|
33
|
-
}
|
|
34
|
-
history['gaps'][initial_id(target_id)] = {
|
|
35
|
-
'id': initial_id(target_id), 'targetId': target_id,
|
|
36
|
-
'description': catalog()['cards'][target_id]['minimum'],
|
|
37
|
-
'impact': '当前目标的必要内容', 'blocking': catalog()['cards'][target_id]['hard'], 'evidence': [],
|
|
38
|
-
'status': 'open', 'windowStart': 0, 'noGainStreak': 0,
|
|
39
|
-
'answers': [{'inputId': key, 'hasNewInformation': None} for key in answers],
|
|
40
|
-
}
|
|
41
|
-
return history['targets'][target_id]
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def count(state, target_id):
|
|
45
|
-
progress = state.get('interview', {}).get('targets', {}).get(target_id, {})
|
|
46
|
-
return len(state['targets'][target_id]['answerInputIds']) - progress.get('windowStart', 0)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def target_open(state, target_id):
|
|
50
|
-
card = catalog()['cards'][target_id]
|
|
51
|
-
require(count(state, target_id) < card['maxAnswers'], 'TARGET_LIMIT',
|
|
52
|
-
'本目标已到本轮回答上限;保留待补,转到其他目标。更换缺口或案例不会重置次数。', {'targetId': target_id})
|
|
53
|
-
progress = state.get('interview', {}).get('targets', {}).get(target_id, {})
|
|
54
|
-
require(not progress.get('stopReason'), 'TARGET_DEFERRED',
|
|
55
|
-
'本轮已收口;只能依据用户明确针对该目标的续谈请求重新展开。', {'targetId': target_id})
|
|
56
|
-
review = card.get('reviewAfter', card['maxAnswers'])
|
|
57
|
-
if count(state, target_id) >= review:
|
|
58
|
-
require(progress.get('lastGain') is True, 'NO_EXTENSION',
|
|
59
|
-
'整理节点后没有实质新增,不能继续延长本目标。', {'targetId': target_id})
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def question_gap(state, question):
|
|
63
|
-
target_id = question['targetId']
|
|
64
|
-
ensure(state, target_id)
|
|
65
|
-
gap_id = question.get('gapId', initial_id(target_id))
|
|
66
|
-
gap = state['interview']['gaps'].get(gap_id)
|
|
67
|
-
require(gap and gap['targetId'] == target_id, 'GAP_NOT_FOUND',
|
|
68
|
-
'问题必须绑定本目标已登记的具体缺口。', {'gapId': gap_id})
|
|
69
|
-
require(gap['status'] == 'open' and len(gap['answers']) - gap['windowStart'] < GAP_LIMIT
|
|
70
|
-
and gap['noGainStreak'] < NO_GAIN_LIMIT, 'GAP_CLOSED',
|
|
71
|
-
'该缺口已讲清、暂放或达到边界;不要换一种问法重复追问。', {'gapId': gap_id})
|
|
72
|
-
card = catalog()['cards'][target_id]
|
|
73
|
-
if count(state, target_id) >= card.get('reviewAfter', card['maxAnswers']):
|
|
74
|
-
require(gap['blocking'], 'EXTENSION_REQUIRES_BLOCKER',
|
|
75
|
-
'四轮整理后只补影响当前结论或执行的关键缺口,其他细节留待补充。')
|
|
76
|
-
return {'targetId': target_id, 'gapId': gap_id}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def prepare(state, patch, inp, intent):
|
|
80
|
-
require(isinstance(patch, dict) and not set(patch) - {'gaps', 'assessment', 'reopen', 'resolutions'},
|
|
81
|
-
'INTERVIEW_SCHEMA', 'interview 仅使用 gaps、assessment、reopen、resolutions。')
|
|
82
|
-
changed = set()
|
|
83
|
-
proposals = patch.get('gaps', [])
|
|
84
|
-
require(isinstance(proposals, list), 'GAP_SCHEMA', 'gaps 使用数组。')
|
|
85
|
-
for proposal in proposals:
|
|
86
|
-
require(isinstance(proposal, dict) and set(proposal) ==
|
|
87
|
-
{'id', 'targetId', 'description', 'impact', 'blocking', 'evidence'},
|
|
88
|
-
'GAP_SCHEMA', '缺口使用 id/targetId/description/impact/blocking/evidence。')
|
|
89
|
-
key, target_id = proposal['id'], proposal['targetId']
|
|
90
|
-
require(target_id in state['targets'] and tracked(target_id), 'GAP_TARGET', '缺口必须属于现有采访目标。')
|
|
91
|
-
require(catalog()['cards'][target_id]['stage'] == state['stage'], 'STAGE_SCOPE', '只登记当前阶段的采访缺口。')
|
|
92
|
-
require(text(key) and re.fullmatch(re.escape(target_id) + r'\.[a-z0-9_-]{1,48}', key)
|
|
93
|
-
and text(proposal['description']) and text(proposal['impact'])
|
|
94
|
-
and type(proposal['blocking']) is bool, 'GAP_SCHEMA', '缺口须有稳定编号、具体内容和对本目标的影响。')
|
|
95
|
-
evidence(state, proposal['evidence'])
|
|
96
|
-
require(proposal['evidence'], 'EVIDENCE_REQUIRED', '新增缺口必须有真实原话、资料或产物依据。')
|
|
97
|
-
ensure(state, target_id)
|
|
98
|
-
gaps = state['interview']['gaps']
|
|
99
|
-
old = gaps.get(key)
|
|
100
|
-
if old:
|
|
101
|
-
require(all(old[field] == proposal[field] for field in proposal), 'GAP_IMMUTABLE',
|
|
102
|
-
'沿用已有缺口编号和含义,不能改写它以重置采访。')
|
|
103
|
-
continue
|
|
104
|
-
target_open(state, target_id)
|
|
105
|
-
require(state['targets'][target_id]['status'] not in CLOSED, 'FOLLOWUP_CLOSED', '目标已收敛,不能靠新增缺口重开。')
|
|
106
|
-
normalized = re.sub(r'\s+', '', proposal['description'])
|
|
107
|
-
require(not any(g['targetId'] == target_id and re.sub(r'\s+', '', g['description']) == normalized
|
|
108
|
-
for g in gaps.values()), 'DUPLICATE_GAP', '相同缺口沿用原编号。')
|
|
109
|
-
gaps[key] = dict(copy.deepcopy(proposal), status='open', windowStart=0, noGainStreak=0, answers=[])
|
|
110
|
-
changed.add(target_id)
|
|
111
|
-
resolutions = patch.get('resolutions', [])
|
|
112
|
-
require(isinstance(resolutions, list), 'RESOLUTION_SCHEMA', 'resolutions 使用数组。')
|
|
113
|
-
for resolution in resolutions:
|
|
114
|
-
require(isinstance(resolution, dict) and set(resolution) == {'gapId', 'summary', 'evidence'}
|
|
115
|
-
and text(resolution['summary']), 'RESOLUTION_SCHEMA', '补充解决使用 gapId/summary/evidence。')
|
|
116
|
-
evidence(state, resolution['evidence'], inp['id'])
|
|
117
|
-
gap = state.get('interview', {}).get('gaps', {}).get(resolution['gapId'])
|
|
118
|
-
require(gap, 'GAP_NOT_FOUND', '未找到待补缺口。')
|
|
119
|
-
require(catalog()['stages'].index(catalog()['cards'][gap['targetId']]['stage']) <=
|
|
120
|
-
catalog()['stages'].index(state['stage']), 'STAGE_SCOPE', '不能提前解决后续阶段目标。')
|
|
121
|
-
gap['status'] = 'resolved'
|
|
122
|
-
gap.setdefault('resolutions', []).append(dict(copy.deepcopy(resolution), inputId=inp['id']))
|
|
123
|
-
changed.add(gap['targetId'])
|
|
124
|
-
reopen = patch.get('reopen')
|
|
125
|
-
if reopen is not None:
|
|
126
|
-
require(isinstance(reopen, dict) and set(reopen) == {'targetId', 'gapId', 'reason', 'evidence'}
|
|
127
|
-
and intent in {'resume', 'revision'} and text(reopen['reason']),
|
|
128
|
-
'REOPEN_SCHEMA', '续谈须为 resume/revision,指定 targetId/gapId/reason/evidence。')
|
|
129
|
-
target_id = reopen['targetId']
|
|
130
|
-
require(target_id in state['targets'] and tracked(target_id)
|
|
131
|
-
and catalog()['cards'][target_id]['stage'] == state['stage'], 'REOPEN_SCOPE', '仅恢复当前阶段的指定目标。')
|
|
132
|
-
evidence(state, reopen['evidence'], inp['id'])
|
|
133
|
-
progress = ensure(state, target_id)
|
|
134
|
-
gap = state['interview']['gaps'].get(reopen['gapId'])
|
|
135
|
-
require(gap and gap['targetId'] == target_id and state['targets'][target_id]['status'] in CLOSED,
|
|
136
|
-
'REOPEN_SCOPE', '仅恢复已经收口的目标及其已记录缺口。')
|
|
137
|
-
progress['reopens'].append(dict(copy.deepcopy(reopen), inputId=inp['id'],
|
|
138
|
-
previousWindowStart=progress['windowStart']))
|
|
139
|
-
progress.update(windowStart=len(state['targets'][target_id]['answerInputIds']),
|
|
140
|
-
noGainStreak=0, lastGain=False, stopReason=None)
|
|
141
|
-
gap.update(windowStart=len(gap['answers']), noGainStreak=0, status='open')
|
|
142
|
-
state['targets'][target_id]['status'] = 'exploring'
|
|
143
|
-
changed.add(target_id)
|
|
144
|
-
return changed
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
def assess(state, target_id, gap_id, assessment, inp):
|
|
148
|
-
require(isinstance(assessment, dict) and set(assessment) ==
|
|
149
|
-
{'hasNewInformation', 'resolved', 'summary', 'evidence'}
|
|
150
|
-
and type(assessment['hasNewInformation']) is bool and type(assessment['resolved']) is bool
|
|
151
|
-
and text(assessment['summary']), 'ASSESSMENT_REQUIRED',
|
|
152
|
-
'采访回答须提交 interview.assessment:hasNewInformation/resolved/summary/evidence。')
|
|
153
|
-
evidence(state, assessment['evidence'], inp['id'])
|
|
154
|
-
progress = ensure(state, target_id)
|
|
155
|
-
gap = state['interview']['gaps'].get(gap_id or initial_id(target_id))
|
|
156
|
-
require(gap and gap['targetId'] == target_id, 'GAP_NOT_FOUND', '未找到本轮实际展示的待答缺口。')
|
|
157
|
-
gain = assessment['hasNewInformation']
|
|
158
|
-
gap['answers'].append(dict(copy.deepcopy(assessment), inputId=inp['id']))
|
|
159
|
-
gap['noGainStreak'] = 0 if gain else gap['noGainStreak'] + 1
|
|
160
|
-
progress['noGainStreak'] = 0 if gain else progress['noGainStreak'] + 1
|
|
161
|
-
progress['lastGain'] = gain
|
|
162
|
-
if assessment['resolved']:
|
|
163
|
-
gap['status'] = 'resolved'
|
|
164
|
-
elif gap['noGainStreak'] >= NO_GAIN_LIMIT or len(gap['answers']) - gap['windowStart'] >= GAP_LIMIT:
|
|
165
|
-
gap['status'] = 'deferred'
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
def settle(state):
|
|
169
|
-
for target_id, progress in state.get('interview', {}).get('targets', {}).items():
|
|
170
|
-
target, card = state['targets'][target_id], catalog()['cards'][target_id]
|
|
171
|
-
gaps = [g for g in state['interview']['gaps'].values() if g['targetId'] == target_id]
|
|
172
|
-
unresolved = [g for g in gaps if g['status'] != 'resolved'
|
|
173
|
-
and (g['answers'] or g['id'] != initial_id(target_id))]
|
|
174
|
-
require(target['status'] != 'sufficient' or not unresolved, 'UNRESOLVED_GAP',
|
|
175
|
-
'已记录的缺口仍未解决,不能将目标标为充分;可暂放并保留范围说明。', {'targetId': target_id})
|
|
176
|
-
reason = ('target_limit' if count(state, target_id) >= card['maxAnswers'] else
|
|
177
|
-
'no_new_information' if progress['noGainStreak'] >= NO_GAIN_LIMIT else
|
|
178
|
-
'review_without_gain' if count(state, target_id) >= card.get('reviewAfter', card['maxAnswers'])
|
|
179
|
-
and not progress['lastGain'] else None)
|
|
180
|
-
if target['status'] not in CLOSED:
|
|
181
|
-
if reason:
|
|
182
|
-
progress['stopReason'] = reason
|
|
183
|
-
target['status'] = 'exhausted'
|
|
184
|
-
elif any(g['status'] == 'deferred' for g in gaps) and not any(g['status'] == 'open' for g in gaps):
|
|
185
|
-
progress['stopReason'] = 'gap_limit'
|
|
186
|
-
target['status'] = 'exhausted'
|
|
187
|
-
if target['status'] in CLOSED:
|
|
188
|
-
for gap in gaps:
|
|
189
|
-
if gap['status'] == 'open':
|
|
190
|
-
gap['status'] = 'resolved' if target['status'] == 'sufficient' else 'deferred'
|
|
191
|
-
if gap['status'] == 'deferred' and gap['description'] not in target['gaps']:
|
|
192
|
-
target['gaps'].append(gap['description'])
|
|
193
|
-
resolved = {g['description'] for g in gaps if g['status'] == 'resolved'}
|
|
194
|
-
target['gaps'] = [description for description in target['gaps'] if description not in resolved]
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
def blockers(state, stage, target_id=None):
|
|
198
|
-
return [g for g in state.get('interview', {}).get('gaps', {}).values()
|
|
199
|
-
if g['blocking'] and g['status'] != 'resolved'
|
|
200
|
-
and catalog()['cards'][g['targetId']]['stage'] == stage
|
|
201
|
-
and (target_id is None or g['targetId'] == target_id)]
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
def guidance(state):
|
|
205
|
-
result = {}
|
|
206
|
-
for target_id, target in state['targets'].items():
|
|
207
|
-
if catalog()['cards'][target_id]['stage'] != state['stage'] or not tracked(target_id):
|
|
208
|
-
continue
|
|
209
|
-
card = catalog()['cards'][target_id]
|
|
210
|
-
result[target_id] = {
|
|
211
|
-
'answersThisWindow': count(state, target_id), 'maxAnswers': card['maxAnswers'],
|
|
212
|
-
'reviewDue': count(state, target_id) >= card.get('reviewAfter', card['maxAnswers']),
|
|
213
|
-
'initialGapId': initial_id(target_id),
|
|
214
|
-
}
|
|
215
|
-
return result
|