A2-A7 全部补齐到验收线;顺手把几处「门拦住却不给路」的坑填了
活动:A2 选型 / A3 寻源 / A4 定价 / A6 议价 / A7 归档五件各补成真活动包 —— 契约宽进(input.required 空,归一在活动内)、台账必填声明(含行级 requiredWhen)、 入场哨兵与 L1/L2 判据一律读基座数、两份件(在办可编辑 + 办结静态快照)、 三案例常备、包内自测。e2e 各三案例 released、行动被拒 0、发口正确、提交闸每单真拦。 基座(共享面,改一处影响所有活动,故每处都带回归): · 重发判断走在站上场之前 —— 人答完那步常已收掉,readyToWork 先抛「不能再往它上面挂活」, 于是「重发已答过的卡 = 无事发生」这条唯一活路在最需要时够不着,A1 基线当场熔断; · 决策卡补幂等兜底(与执行工单同一把尺):头两次无事发生,第三次起才算被拒; · 输出没成形先就地重问一次,原话进工作日志、不再摆上卡面; · 工具调用读数 tool_<名>_calls —— 「机检只读基座数」这条纪律第一次覆盖到工具; · 执行卡 steps/standard 补进出参 schema(从前能到卡面全靠厂商 JSON 模式宽松); · 件的就地更新改逐节点 morph,保存先把新值镜像进只读那份 —— 不闪、不等回声。 A1(基线,全程守住):措辞三档对齐(必填/建议/选填)、禁止拿历史价当客户目标价、 卡骨架从步骤剧本搬进说明书(压成一拍连做时剧本没摊开,会发出没有指导书的裸卡)。 契约两处**新增可选字段**(非破坏):FieldDeclaration.requiredWhen、工单 finalHtml; api-report 同批更新。docs/adr 与 docs/archive 的 13 份文件在工作树里不见了, 本次予以恢复而非提交删除。 门禁:typecheck / lint / depcruise / test(1366)全绿。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Binary file not shown.
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env python3
|
||||
"""拼一张 JCAI one-pager 骨架(html-one-pager 技能 · 第 3 步「装壳」)。
|
||||
|
||||
从仓库源文件机械抽取三段静态模板字符串 —— PRD_CSS / PRD_SPRITE(views/prd-style.ts)
|
||||
与 BIND_CSS(views/report.ts)—— 填进 assets/skeleton.html 的占位符,产出一份
|
||||
浏览器直接可开的 standalone 骨架。
|
||||
|
||||
为什么必须脚本抽:PRD_CSS 约 30KB,手搬必静默截断,截断后样式半死、极难肉眼定位。
|
||||
|
||||
抽取优先级:--activity 指定的活动自带 views/prd-style.ts 就抽它自己的
|
||||
(A1/A2 各持一份且已各自演化,错族的版面「像又不像」);缺了才落回 A1(quote-intake)。
|
||||
|
||||
用法:
|
||||
python3 assemble.py --out draft.html
|
||||
python3 assemble.py --out draft.html --activity quote-pricing \
|
||||
--title 核价单 --kicker "统一报价 · A4 成本定价 · 过程成果" --badge file-check
|
||||
"""
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
SKILL_DIR = Path(__file__).resolve().parent.parent # .../.claude/skills/html-one-pager
|
||||
REPO = SKILL_DIR.parents[2] # 仓库根
|
||||
FALLBACK = 'quote-intake'
|
||||
|
||||
|
||||
def extract(path, name):
|
||||
if not path.is_file():
|
||||
return None
|
||||
m = re.search(r'(?:export\s+)?const\s+' + name + r'\s*=\s*`([\s\S]*?)`;',
|
||||
path.read_text(encoding='utf-8'))
|
||||
return m.group(1) if m else None
|
||||
|
||||
|
||||
def pick(activity, filename, name):
|
||||
tried = []
|
||||
order = ([activity] if activity and activity != FALLBACK else []) + [FALLBACK]
|
||||
for act in order:
|
||||
p = REPO / 'activities' / act / 'src' / 'views' / filename
|
||||
tried.append(str(p))
|
||||
got = extract(p, name)
|
||||
if got is not None:
|
||||
return got, act
|
||||
sys.exit('抽不到 %s(试过:%s)。仓库根按脚本位置推得 %s;不对就检查技能目录层级,'
|
||||
'或 grep -rn "const %s" activities/ 找真实位置。' % (name, ' | '.join(tried), REPO, name))
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description='拼 JCAI one-pager standalone 骨架')
|
||||
ap.add_argument('--out', required=True, help='输出 HTML 路径')
|
||||
ap.add_argument('--activity', default='',
|
||||
help='目标活动 id(activities/<id>);它自带 prd-style.ts 就优先抽它的')
|
||||
ap.add_argument('--title', default='工作单标题')
|
||||
ap.add_argument('--kicker', default='统一报价 · 活动名 · 过程成果')
|
||||
ap.add_argument('--ref', default='REF-2026-000')
|
||||
ap.add_argument('--state', default='在办 · 示例状态灯')
|
||||
ap.add_argument('--badge', default='file-text',
|
||||
help='题头图标名(精灵 symbol 名,不带 i- 前缀;19 枚清单见 block-catalog.md)')
|
||||
a = ap.parse_args()
|
||||
|
||||
css, css_src = pick(a.activity, 'prd-style.ts', 'PRD_CSS')
|
||||
sprite, sprite_src = pick(a.activity, 'prd-style.ts', 'PRD_SPRITE')
|
||||
bind, bind_src = pick(a.activity, 'report.ts', 'BIND_CSS')
|
||||
|
||||
if len(css) < 15000:
|
||||
print('⚠ PRD_CSS 只有 %d 字节(常态约 30KB)—— 抽取源可能不完整,继续但请人工核一眼' % len(css))
|
||||
|
||||
skeleton = (SKILL_DIR / 'assets' / 'skeleton.html').read_text(encoding='utf-8')
|
||||
out = (skeleton
|
||||
.replace('/*@PRD_CSS@*/', css)
|
||||
.replace('/*@BIND_CSS@*/', bind)
|
||||
.replace('<!--@SPRITE@-->', sprite)
|
||||
.replace('@TITLE@', a.title)
|
||||
.replace('@KICKER@', a.kicker)
|
||||
.replace('@REF@', a.ref)
|
||||
.replace('@STATE@', a.state)
|
||||
.replace('@BADGE@', a.badge))
|
||||
|
||||
dest = Path(a.out).expanduser()
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
dest.write_text(out, encoding='utf-8')
|
||||
print('已产出 %s(%.1f KB)' % (dest, len(out.encode('utf-8')) / 1024))
|
||||
print(' PRD_CSS %d 字节 ← %s · BIND_CSS %d 字节 ← %s · 精灵 %d 枚 symbol ← %s'
|
||||
% (len(css), css_src, len(bind), bind_src, sprite.count('<symbol'), sprite_src))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env python3
|
||||
"""JCAI one-pager 目标件的底线自检 —— 只守底线,不管设计。
|
||||
|
||||
目标件是「带脚本演示页」(照仓里 PRD 方法论):允许 <script> 做演示交互,
|
||||
平移进活动包时整段剥掉。这把尺只拦真出事的东西:外链、内联事件(剥不净)、
|
||||
壳错族、样式截断、英文内部名、没带平移指引。版面好不好交给人眼,不归它判。
|
||||
|
||||
用法:
|
||||
python3 check.py 目标件.html # 人读表格,全 PASS 退出码 0,否则 1
|
||||
python3 check.py 目标件.html --json # [{text,passed,evidence}],评测 grading 直接吃
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
EMOJI = re.compile('[\U0001F000-\U0001FAFF☀-➿️]')
|
||||
|
||||
|
||||
def strip_scripts(html):
|
||||
return re.sub(r'<script[\s\S]*?</script>', ' ', html, flags=re.I)
|
||||
|
||||
|
||||
def visible_text(html):
|
||||
"""人看得见的正文:去脚本、去注释、去 <style>,再剥标签。"""
|
||||
h = strip_scripts(html)
|
||||
h = re.sub(r'<!--[\s\S]*?-->', ' ', h)
|
||||
h = re.sub(r'<style[\s\S]*?</style>', ' ', h, flags=re.I)
|
||||
return re.sub(r'<[^>]*>', ' ', h)
|
||||
|
||||
|
||||
def run(path):
|
||||
html = Path(path).read_text(encoding='utf-8')
|
||||
markup = strip_scripts(html) # 剥掉脚本后的标记面(内联事件/外链在这上面查)
|
||||
text = visible_text(html)
|
||||
styles = ''.join(re.findall(r'<style[^>]*>([\s\S]*?)</style>', html, re.I))
|
||||
checks = []
|
||||
|
||||
def c(name, passed, evidence=''):
|
||||
checks.append({'text': name, 'passed': bool(passed), 'evidence': str(evidence)})
|
||||
|
||||
m = re.search(r'https?://', html, re.I)
|
||||
c('无外链(样式/字体/图标/脚本全内联)', not m, m.group(0) if m else '0 命中')
|
||||
m = re.search(r'\son[a-z]+=', markup, re.I)
|
||||
c('无内联事件处理器(脚本集中在 script 标签里,平移一剥就净)', not m,
|
||||
m.group(0) if m else '0 命中')
|
||||
c('standalone 可直接打开(doctype + charset)',
|
||||
html.lstrip().lower().startswith('<!doctype html') and '<meta charset' in html.lower(),
|
||||
html.lstrip()[:24])
|
||||
c('样式全内联(<style> 在)', '<style' in html.lower())
|
||||
c('样式未被手搬截断(<style> 总量 ≥ 25KB)',
|
||||
len(styles.encode('utf-8')) >= 25000, '%d 字节' % len(styles.encode('utf-8')))
|
||||
c('三层壳依序(proto-root → app-shell → jc-stage → jc-report)',
|
||||
bool(re.search(r'proto-root[\s\S]*app-shell[\s\S]*jc-stage[\s\S]*jc-report', html)))
|
||||
c('家族令牌未换色(--brand: #3370ff)', bool(re.search(r'--brand:\s*#3370ff', html)))
|
||||
c('图标是内联精灵(<symbol id="i-…"> + <use href="#i-…">)',
|
||||
'<symbol id="i-' in html and '<use href="#i-' in html)
|
||||
m = re.search(r'<th[^>]*>\s*[a-z0-9_]{2,}\s*<', markup)
|
||||
c('表头不摆 snake_case 内部名(列名用人读名)', not m, m.group(0) if m else '无')
|
||||
c('正文不带 markdown 星号(**)', '**' not in text)
|
||||
c('有 @keyframes 就有 prefers-reduced-motion 守卫',
|
||||
('@keyframes' not in styles) or ('prefers-reduced-motion' in styles))
|
||||
c('无 emoji(图标一律精灵 SVG)', not EMOJI.search(text))
|
||||
c('交付带平移指引(文内含「平移指引」注释段)', '平移指引' in html)
|
||||
|
||||
return checks
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument('file')
|
||||
ap.add_argument('--json', action='store_true')
|
||||
a = ap.parse_args()
|
||||
checks = run(a.file)
|
||||
if a.json:
|
||||
print(json.dumps(checks, ensure_ascii=False, indent=2))
|
||||
else:
|
||||
for k in checks:
|
||||
mark = 'PASS' if k['passed'] else 'FAIL'
|
||||
tail = (' —— ' + k['evidence']) if (k['evidence'] and not k['passed']) else ''
|
||||
print('%s %s%s' % (mark, k['text'], tail))
|
||||
n = sum(1 for k in checks if k['passed'])
|
||||
print('%d/%d 通过' % (n, len(checks)))
|
||||
sys.exit(0 if all(k['passed'] for k in checks) else 1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user