feat: improve skill import and runtime delivery

This commit is contained in:
lofyer
2026-08-07 21:01:17 +08:00
parent 954b42ef55
commit 417a9fccb6
63 changed files with 6136 additions and 527 deletions
@@ -0,0 +1,88 @@
---
name: product-evidence
version: 1.0.0
description: |
建立和维护产品市场材料的事实与证据清单,统一产品版本、功能状态、技术参数、
术语、适用边界和可公开主张。用于开始任何产品介绍、投标参数、PPT、技术方案、
白皮书或案例材料之前,也用于跨产物一致性核验。
allowed-tools:
- Read
- Grep
- Glob
- Execute
compatibility: Python 3.9+,校验脚本不依赖第三方包
---
# 产品事实与证据
所有产品市场产物都必须从同一份 `product-evidence.json` 取事实。缺少依据时标记
待核验,不允许由 Agent 补造参数、客户结果、认证、兼容性或竞争结论。
`<skill-dir>` 指本 `SKILL.md` 所在目录。
## 建立清单
```bash
cp "<skill-dir>/templates/product-evidence.example.json" ./product-evidence.json
```
逐项填写:
- `product`:产品名称、版本、类别、定位、成熟度和目标读者。
- `terminology`:统一术语、定义和禁用旧称。
- `features`:功能 ID、用户动作、结果、状态、版本范围和证据。
- `parameters`:参数值、单位、测试条件、适用版本、公开级别和证据。
- `claims`:允许对外使用的事实、目标或比较主张及适用产物。
- `use_cases`:角色、问题、工作流、人工复核点和已有结果。
- `differentiators`:比较对象、比较范围和支持证据。
- `limitations`:部署条件、依赖、适用边界和必要人工复核。
- `evidence`:来源、定位、核验日期、责任人和公开级别。
- `prohibited_claims`:不得出现在任何材料中的绝对化或未经批准表述。
## 事实分级
- `released`:当前版本已提供,必须有可定位证据。
- `beta`:可试用但存在范围限制,正文必须同时说明限制。
- `planned`:仅可用将来时或规划表述,不得写成现有能力。
- `deprecated`:不得作为当前卖点。
证据公开级别:
- `public`:可进入公开网站、彩页和公开演示。
- `restricted`:只在授权客户或受控投标材料中使用。
- `internal`:只用于内部判断,不能原样写入外发产物。
## 校验
先探测可用的 Python 3 解释器:Windows 优先使用 `python`macOS/Linux
优先使用 `python3`。下文 `<python>` 表示探测成功的解释器命令。
```bash
<python> "<skill-dir>/scripts/validate_evidence.py" ./product-evidence.json
<python> "<skill-dir>/scripts/validate_evidence.py" ./product-evidence.json --json
<python> "<skill-dir>/scripts/validate_evidence.py" ./product-evidence.json \
--strict --channel public
```
结构错误、重复 ID、失效引用、无证据的已批准事实主张、非法状态或疑似密钥参数
必须阻断。缺证据、占位符和待核验项在普通模式下告警,在 `--strict` 下阻断。
`--channel` 按目标渠道核对公开级别:已批准且带 `allowed_outputs` 的主张,其
引用证据的 `disclosure` 不得低于渠道要求。对外产物必须以 `--strict` 加目标
渠道运行通过后才能进入下游技能。
## 给下游技能的输入
调用任何产品产物技能时,同时提供:
1. 已通过校验的 `product-evidence.json`
2. 目标受众、使用场景、发布渠道和保密级别。
3. 本次产物允许引用的证据范围。
4. 截止日期、页数或篇幅、格式和品牌要求。
下游产物中的每个数字、兼容性、认证、客户效果和比较结论都应能追溯到清单 ID。
## 完成标准
清单结构校验通过;公开级别与使用渠道匹配;所有现有功能、参数和批准主张有
证据;规划能力、限制和人工复核要求没有被省略;不含密钥、客户隐私或私有地址。
@@ -0,0 +1,431 @@
#!/usr/bin/env python3
"""Validate the shared product evidence manifest used by marketing skills."""
import argparse
import datetime as dt
import json
import re
import sys
from pathlib import Path
COLLECTIONS = (
"features",
"parameters",
"claims",
"use_cases",
"differentiators",
"limitations",
"evidence",
)
REQUIRED_FIELDS = {
"features": ("id", "name", "summary", "status"),
"parameters": ("id", "name", "value", "conditions", "disclosure"),
"claims": ("id", "text", "type", "status"),
"use_cases": ("id", "name", "audience", "problem", "workflow", "outcome"),
"differentiators": ("id", "statement", "comparison_scope"),
"limitations": ("id", "text", "applies_to"),
"evidence": (
"id",
"type",
"title",
"source",
"locator",
"verified_on",
"owner",
"disclosure",
),
}
ALLOWED = {
"product.maturity": {"released", "beta", "planned", "deprecated"},
"features.status": {"released", "beta", "planned", "deprecated"},
"claims.type": {"fact", "goal", "comparison"},
"claims.status": {"approved", "draft", "rejected"},
"disclosure": {"public", "restricted", "internal"},
}
OUTPUT_TYPES = {
"feature-catalog",
"technical-spec",
"presentation",
"technical-proposal",
"one-pager",
"whitepaper",
"tender-response",
"sales-demo",
"case-study",
"competitive-positioning",
}
DISCLOSURE_RANK = {"internal": 0, "restricted": 1, "public": 2}
PLACEHOLDERS = re.compile(
r"(?i)(?:\bTBD\b|\bTODO\b|待补充|待确认|placeholder|changeme)"
)
SECRET_QUERY = re.compile(r"(?i)(?:token|api[_-]?key|secret|password)=")
def issue(path, message):
return {"path": path, "message": message}
def is_nonempty_string(value):
return isinstance(value, str) and bool(value.strip())
def validate_record_shape(collection, index, record, errors):
path = f"{collection}[{index}]"
if not isinstance(record, dict):
errors.append(issue(path, "必须是对象"))
return False
for field in REQUIRED_FIELDS[collection]:
value = record.get(field)
if field in ("applies_to",):
if not isinstance(value, list) or not value:
errors.append(issue(f"{path}.{field}", "必须是非空数组"))
elif not is_nonempty_string(value):
errors.append(issue(f"{path}.{field}", "必须是非空字符串"))
return True
def evidence_disclosure_map(records):
return {
record.get("id"): record.get("disclosure")
for record in records["evidence"]
if isinstance(record, dict)
}
def validate_disclosure_chain(records, errors):
"""A record must not be more public than the evidence backing it."""
evidence_disclosure = evidence_disclosure_map(records)
for collection in ("parameters", "claims"):
for index, record in enumerate(records[collection]):
if not isinstance(record, dict):
continue
own = record.get("disclosure")
if own not in DISCLOSURE_RANK:
continue
for ref in record.get("evidence_ids", []) or []:
backing = evidence_disclosure.get(ref)
if backing not in DISCLOSURE_RANK:
continue
if DISCLOSURE_RANK[backing] < DISCLOSURE_RANK[own]:
errors.append(
issue(
f"{collection}[{index}].disclosure",
f"标记为 {own},但证据 {ref} 仅为 {backing}",
)
)
def validate_channel(records, channel, errors):
"""Reject claims cleared for delivery whose evidence is too restricted."""
target = DISCLOSURE_RANK[channel]
evidence_disclosure = evidence_disclosure_map(records)
for index, claim in enumerate(records["claims"]):
if not isinstance(claim, dict) or claim.get("status") != "approved":
continue
if not claim.get("allowed_outputs"):
continue
for ref in claim.get("evidence_ids", []) or []:
disclosure = evidence_disclosure.get(ref)
if disclosure not in DISCLOSURE_RANK:
continue
if DISCLOSURE_RANK[disclosure] < target:
errors.append(
issue(
f"claims[{index}].evidence_ids",
f"证据 {ref}{disclosure},不足以支撑 {channel} 渠道",
)
)
def validate_manifest(data, channel=None):
errors = []
warnings = []
if not isinstance(data, dict):
return [issue("$", "根节点必须是对象")], warnings
if data.get("schema_version") != 1:
errors.append(issue("schema_version", "当前仅支持整数 1"))
product = data.get("product")
if not isinstance(product, dict):
errors.append(issue("product", "必须是对象"))
product = {}
for field in ("name", "version", "category", "summary", "maturity"):
if not is_nonempty_string(product.get(field)):
errors.append(issue(f"product.{field}", "必须是非空字符串"))
if product.get("maturity") not in ALLOWED["product.maturity"]:
errors.append(
issue(
"product.maturity",
f"必须是 {sorted(ALLOWED['product.maturity'])} 之一",
)
)
records = {}
all_ids = {}
for collection in COLLECTIONS:
values = data.get(collection, [])
if not isinstance(values, list):
errors.append(issue(collection, "必须是数组"))
values = []
records[collection] = values
for index, record in enumerate(values):
if not validate_record_shape(collection, index, record, errors):
continue
record_id = record.get("id")
if is_nonempty_string(record_id):
if record_id in all_ids:
errors.append(
issue(
f"{collection}[{index}].id",
f"ID 与 {all_ids[record_id]} 重复:{record_id}",
)
)
else:
all_ids[record_id] = f"{collection}[{index}]"
evidence_ids = {
record.get("id")
for record in records["evidence"]
if isinstance(record, dict) and is_nonempty_string(record.get("id"))
}
feature_ids = {
record.get("id")
for record in records["features"]
if isinstance(record, dict) and is_nonempty_string(record.get("id"))
}
use_case_ids = {
record.get("id")
for record in records["use_cases"]
if isinstance(record, dict) and is_nonempty_string(record.get("id"))
}
for collection in COLLECTIONS:
for index, record in enumerate(records[collection]):
if not isinstance(record, dict):
continue
path = f"{collection}[{index}]"
refs = record.get("evidence_ids", [])
if refs is not None and not isinstance(refs, list):
errors.append(issue(f"{path}.evidence_ids", "必须是数组"))
refs = []
for ref in refs or []:
if ref not in evidence_ids:
errors.append(
issue(f"{path}.evidence_ids", f"引用不存在:{ref}")
)
for ref in record.get("use_case_ids", []) or []:
if ref not in use_case_ids:
errors.append(
issue(f"{path}.use_case_ids", f"引用不存在:{ref}")
)
for index, limitation in enumerate(records["limitations"]):
if not isinstance(limitation, dict):
continue
for ref in limitation.get("applies_to", []) or []:
if ref not in feature_ids and ref not in all_ids:
errors.append(
issue(
f"limitations[{index}].applies_to",
f"引用不存在:{ref}",
)
)
for index, feature in enumerate(records["features"]):
if not isinstance(feature, dict):
continue
status = feature.get("status")
if status not in ALLOWED["features.status"]:
errors.append(
issue(
f"features[{index}].status",
f"必须是 {sorted(ALLOWED['features.status'])} 之一",
)
)
if status in {"released", "beta"} and not feature.get("evidence_ids"):
warnings.append(
issue(
f"features[{index}].evidence_ids",
"已发布或 beta 功能缺少证据",
)
)
for index, parameter in enumerate(records["parameters"]):
if not isinstance(parameter, dict):
continue
disclosure = parameter.get("disclosure")
if disclosure not in ALLOWED["disclosure"]:
errors.append(
issue(
f"parameters[{index}].disclosure",
f"必须是 {sorted(ALLOWED['disclosure'])} 之一",
)
)
if not parameter.get("evidence_ids"):
warnings.append(
issue(
f"parameters[{index}].evidence_ids",
"技术参数缺少证据",
)
)
for index, claim in enumerate(records["claims"]):
if not isinstance(claim, dict):
continue
if claim.get("type") not in ALLOWED["claims.type"]:
errors.append(
issue(
f"claims[{index}].type",
f"必须是 {sorted(ALLOWED['claims.type'])} 之一",
)
)
if claim.get("status") not in ALLOWED["claims.status"]:
errors.append(
issue(
f"claims[{index}].status",
f"必须是 {sorted(ALLOWED['claims.status'])} 之一",
)
)
if (
claim.get("type") in {"fact", "comparison"}
and claim.get("status") == "approved"
and not claim.get("evidence_ids")
):
errors.append(
issue(
f"claims[{index}].evidence_ids",
"已批准的事实或比较主张必须有证据",
)
)
outputs = claim.get("allowed_outputs", [])
if outputs is not None and not isinstance(outputs, list):
errors.append(
issue(f"claims[{index}].allowed_outputs", "必须是数组")
)
for output in outputs or []:
if output not in OUTPUT_TYPES:
errors.append(
issue(
f"claims[{index}].allowed_outputs",
f"未知产物类型:{output}",
)
)
for index, evidence in enumerate(records["evidence"]):
if not isinstance(evidence, dict):
continue
disclosure = evidence.get("disclosure")
if disclosure not in ALLOWED["disclosure"]:
errors.append(
issue(
f"evidence[{index}].disclosure",
f"必须是 {sorted(ALLOWED['disclosure'])} 之一",
)
)
verified_on = evidence.get("verified_on")
if is_nonempty_string(verified_on):
try:
dt.date.fromisoformat(verified_on)
except ValueError:
errors.append(
issue(
f"evidence[{index}].verified_on",
"必须使用 YYYY-MM-DD",
)
)
source = evidence.get("source", "")
if is_nonempty_string(source) and SECRET_QUERY.search(source):
errors.append(
issue(
f"evidence[{index}].source",
"来源中疑似包含密钥或令牌参数",
)
)
for index, differentiator in enumerate(records["differentiators"]):
if isinstance(differentiator, dict) and not differentiator.get(
"evidence_ids"
):
warnings.append(
issue(
f"differentiators[{index}].evidence_ids",
"差异点缺少证据",
)
)
validate_disclosure_chain(records, errors)
if channel in DISCLOSURE_RANK:
validate_channel(records, channel, errors)
for path, value in walk_strings(data):
if PLACEHOLDERS.search(value):
warnings.append(issue(path, f"包含占位内容:{value[:60]}"))
if not evidence_ids:
warnings.append(issue("evidence", "没有任何证据记录"))
return errors, warnings
def walk_strings(value, path="$"):
if isinstance(value, dict):
for key, item in value.items():
yield from walk_strings(item, f"{path}.{key}")
elif isinstance(value, list):
for index, item in enumerate(value):
yield from walk_strings(item, f"{path}[{index}]")
elif isinstance(value, str):
yield path, value
def parse_args():
parser = argparse.ArgumentParser(description="验证产品事实与证据清单")
parser.add_argument("manifest", help="product-evidence.json")
parser.add_argument("--json", action="store_true", help="输出 JSON")
parser.add_argument(
"--strict",
action="store_true",
help="存在占位符、缺证据等警告时也失败",
)
parser.add_argument(
"--channel",
choices=sorted(DISCLOSURE_RANK),
help="目标发布渠道,校验主张引用证据的公开级别是否足够",
)
return parser.parse_args()
def main():
args = parse_args()
path = Path(args.manifest).expanduser().resolve()
with path.open(encoding="utf-8") as handle:
data = json.load(handle)
errors, warnings = validate_manifest(data, channel=args.channel)
report = {
"file": str(path),
"channel": args.channel,
"valid": not errors and (not args.strict or not warnings),
"errors": errors,
"warnings": warnings,
"stats": {
collection: len(data.get(collection, []))
if isinstance(data, dict) and isinstance(data.get(collection, []), list)
else 0
for collection in COLLECTIONS
},
}
if args.json:
print(json.dumps(report, ensure_ascii=False, indent=2))
else:
for level, items in (("错误", errors), ("警告", warnings)):
for item in items:
print(f"[{level}] {item['path']}{item['message']}")
print(
f"核验完成:错误 {len(errors)},警告 {len(warnings)}"
f"状态 {'通过' if report['valid'] else '失败'}"
)
return 0 if report["valid"] else 1
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,120 @@
{
"schema_version": 1,
"product": {
"name": "示例产品",
"version": "1.0",
"category": "产品类别",
"summary": "用一句可验证的话说明产品面向谁、解决什么问题。",
"maturity": "released",
"audiences": [
"技术负责人",
"业务负责人"
],
"deployment_modes": [
"私有化部署"
]
},
"terminology": [
{
"term": "标准术语",
"definition": "术语在所有材料中的统一定义。",
"avoid": [
"不再使用的旧称谓"
]
}
],
"features": [
{
"id": "FEAT-001",
"name": "示例功能",
"summary": "说明用户可执行的动作和可观察结果。",
"status": "released",
"availability": "标准版本",
"evidence_ids": [
"EVD-001"
],
"use_case_ids": [
"CASE-001"
]
}
],
"parameters": [
{
"id": "PAR-001",
"name": "示例技术参数",
"value": "已核验值",
"unit": "",
"conditions": "说明测试条件、版本和统计口径。",
"scope": "标准版本",
"disclosure": "public",
"evidence_ids": [
"EVD-001"
]
}
],
"claims": [
{
"id": "CLM-001",
"text": "产品在指定版本和条件下提供示例功能。",
"type": "fact",
"status": "approved",
"allowed_outputs": [
"feature-catalog",
"technical-proposal",
"presentation"
],
"evidence_ids": [
"EVD-001"
]
}
],
"use_cases": [
{
"id": "CASE-001",
"name": "示例场景",
"audience": "技术负责人",
"problem": "说明现有工作中的具体问题。",
"workflow": "说明产品参与的步骤和人工复核点。",
"outcome": "只写已有证据支持的结果。",
"evidence_ids": [
"EVD-001"
]
}
],
"differentiators": [
{
"id": "DIF-001",
"statement": "说明可验证的产品差异。",
"comparison_scope": "明确比较对象和版本范围。",
"evidence_ids": [
"EVD-001"
]
}
],
"limitations": [
{
"id": "LIM-001",
"text": "说明适用边界、依赖条件或人工复核要求。",
"applies_to": [
"FEAT-001"
]
}
],
"evidence": [
{
"id": "EVD-001",
"type": "product-documentation",
"title": "产品说明书",
"source": "docs/product.md",
"locator": "功能章节",
"verified_on": "2026-01-01",
"owner": "产品负责人",
"disclosure": "public",
"notes": "发布前重新核验版本。"
}
],
"prohibited_claims": [
"绝对领先",
"百分之百准确"
]
}
@@ -0,0 +1,113 @@
import copy
import importlib.util
import json
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
SKILL_DIR = Path(__file__).resolve().parents[1]
SCRIPT = SKILL_DIR / "scripts" / "validate_evidence.py"
SPEC = importlib.util.spec_from_file_location("validate_evidence", SCRIPT)
validate_evidence = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(validate_evidence)
class ProductEvidenceTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
template = SKILL_DIR / "templates" / "product-evidence.example.json"
cls.valid_data = json.loads(template.read_text(encoding="utf-8"))
def test_example_manifest_is_valid(self):
errors, warnings = validate_evidence.validate_manifest(self.valid_data)
self.assertEqual(errors, [])
self.assertEqual(warnings, [])
def test_rejects_duplicate_ids_and_unknown_references(self):
data = copy.deepcopy(self.valid_data)
data["parameters"][0]["id"] = "FEAT-001"
data["claims"][0]["evidence_ids"] = ["EVD-MISSING"]
errors, _ = validate_evidence.validate_manifest(data)
messages = "\n".join(item["message"] for item in errors)
self.assertIn("重复", messages)
self.assertIn("引用不存在", messages)
def test_rejects_secret_in_evidence_source(self):
data = copy.deepcopy(self.valid_data)
data["evidence"][0]["source"] = "https://example.test/doc?token=secret"
errors, _ = validate_evidence.validate_manifest(data)
self.assertTrue(
any("密钥" in item["message"] for item in errors)
)
def test_placeholder_is_warning(self):
data = copy.deepcopy(self.valid_data)
data["product"]["summary"] = "TODO"
errors, warnings = validate_evidence.validate_manifest(data)
self.assertEqual(errors, [])
self.assertTrue(warnings)
def test_rejects_record_more_public_than_evidence(self):
data = copy.deepcopy(self.valid_data)
for record in data["evidence"]:
if record["id"] == "EVD-001":
record["disclosure"] = "restricted"
errors, _ = validate_evidence.validate_manifest(data)
self.assertTrue(
any("仅为 restricted" in item["message"] for item in errors)
)
def test_restricted_parameter_is_not_a_public_channel_warning(self):
data = copy.deepcopy(self.valid_data)
data["parameters"][0]["disclosure"] = "restricted"
for record in data["evidence"]:
if record["id"] in data["parameters"][0]["evidence_ids"]:
record["disclosure"] = "restricted"
data["claims"][0]["evidence_ids"] = []
data["claims"][0]["type"] = "goal"
errors, warnings = validate_evidence.validate_manifest(
data, channel="public"
)
self.assertEqual(errors, [])
self.assertEqual(warnings, [])
def test_public_channel_rejects_internal_evidence(self):
data = copy.deepcopy(self.valid_data)
claim = data["claims"][0]
claim["allowed_outputs"] = ["one-pager"]
evidence_id = claim["evidence_ids"][0]
for record in data["evidence"]:
if record["id"] == evidence_id:
record["disclosure"] = "internal"
for parameter in data["parameters"]:
if evidence_id in parameter.get("evidence_ids", []):
parameter["disclosure"] = "internal"
errors, _ = validate_evidence.validate_manifest(data, channel="public")
self.assertTrue(
any("不足以支撑 public" in item["message"] for item in errors)
)
errors, _ = validate_evidence.validate_manifest(data, channel="internal")
self.assertEqual(errors, [])
def test_cli_json_report(self):
with tempfile.TemporaryDirectory() as tmp:
manifest = Path(tmp) / "product-evidence.json"
manifest.write_text(
json.dumps(self.valid_data, ensure_ascii=False),
encoding="utf-8",
)
result = subprocess.run(
[sys.executable, str(SCRIPT), str(manifest), "--json"],
check=True,
capture_output=True,
text=True,
)
report = json.loads(result.stdout)
self.assertTrue(report["valid"])
if __name__ == "__main__":
unittest.main()