@remixmate/cli 0.9.49 → 0.9.51

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.
Files changed (29) hide show
  1. package/README.md +14 -26
  2. package/dist/manifest.json +18 -8
  3. package/package.json +9 -13
  4. package/skills/doc-parse/SKILL.md +6 -6
  5. package/skills/doc-parse/scripts/extract_via_render.py +3 -2
  6. package/skills/doc-parse/skill.json +1 -1
  7. package/skills/gen-script/SKILL.md +13 -11
  8. package/skills/gen-script/scripts/gen_script.py +106 -25
  9. package/skills/gen-script/skill.json +14 -4
  10. package/skills/gen-script/version.json +1 -1
  11. package/skills/prepare-video-assets/SKILL.md +3 -3
  12. package/skills/prepare-video-assets/skill.json +1 -1
  13. package/skills/prepare-video-assets/version.json +1 -1
  14. package/skills/render-video/scripts/render_video.py +77 -44
  15. package/skills/render-video/version.json +1 -1
  16. package/skills/template-registry/README.md +3 -14
  17. package/skills/template-registry/SKILL.md +12 -49
  18. package/skills/template-registry/scripts/countdown_spec.py +56 -6
  19. package/skills/template-registry/scripts/registry_loader.py +3 -4
  20. package/skills/template-registry/version.json +1 -1
  21. package/skills/template-registry/video_dsl/README.md +10 -53
  22. package/skills/template-registry/video_dsl/runtime/__init__.py +0 -1
  23. package/skills/template-registry/video_dsl/runtime/dsl_validator.py +44 -35
  24. package/skills/template-registry/video_dsl/schema/video-dsl-v1alpha1.json +1 -1
  25. package/skills/template-registry/scripts/check_contracts.py +0 -179
  26. package/skills/template-registry/scripts/sync_registry.py +0 -44
  27. package/skills/template-registry/scripts/template_paths.py +0 -72
  28. package/skills/template-registry/scripts/verify_props_contract.py +0 -273
  29. package/skills/template-registry/video_dsl/runtime/template_binder.py +0 -87
@@ -1,72 +0,0 @@
1
- """Centralized monorepo path resolution for template-library artifacts.
2
-
3
- Why this module exists
4
- ----------------------
5
- Before this file landed, every caller that needed to reach into the
6
- monorepo's ``template-library/`` from inside ``apps/ab-skill/`` had to count
7
- ``..`` segments by hand. The depth differs per call site:
8
-
9
- skills/template-registry/scripts/registry_loader.py → 5 × ".."
10
- skills/template-registry/scripts/check_contracts.py → 3 × ".."
11
- skills/template-registry/video_dsl/runtime/template_binder → 6 × ".."
12
- skills/gen-script/scripts/gen_script.py → 5 × ".."
13
- skills/render-video/scripts/render_video.py → 4 × ".."
14
-
15
- Five copies, five chances for an off-by-one bug, and any future directory
16
- move silently breaks one site without touching the others. This module
17
- computes the monorepo root **once** from its own location and exposes
18
- helpers so callers no longer count.
19
-
20
- All sibling skills (including the ``list_templates.py`` CLI entry, via
21
- ``registry_loader``) go through these helpers, so the monorepo-root
22
- computation lives in exactly one place.
23
- """
24
- from __future__ import annotations
25
-
26
- import os
27
- from typing import Optional
28
-
29
- __all__ = ["monorepo_root", "monorepo_registry_path", "monorepo_template_src_dir"]
30
-
31
- # This file lives at:
32
- # <repo>/apps/ab-skill/skills/template-registry/scripts/template_paths.py
33
- # So the monorepo root is 5 levels up.
34
- _HERE = os.path.dirname(os.path.abspath(__file__))
35
- _REPO_ROOT = os.path.normpath(os.path.join(_HERE, "..", "..", "..", "..", ".."))
36
-
37
-
38
- def monorepo_root() -> str:
39
- """Absolute path to the ab-platform monorepo root.
40
-
41
- Always returns a string; the caller is responsible for checking
42
- ``os.path.isdir(...)`` if the root may genuinely be absent (e.g. when
43
- the skill is consumed standalone via the npm package).
44
- """
45
- return _REPO_ROOT
46
-
47
-
48
- def monorepo_registry_path() -> str:
49
- """Path to ``template-library/packages/metadata/registry.json``.
50
-
51
- Always returns the computed path even when the file does not exist —
52
- so callers can keep their existing ``os.path.exists(...)`` / try-load
53
- branches unchanged. Mirrors the historical behavior of the 4 in-tree
54
- helpers it replaces.
55
- """
56
- return os.path.join(_REPO_ROOT, "template-library", "packages", "metadata", "registry.json")
57
-
58
-
59
- def monorepo_template_src_dir(template_id: str) -> Optional[str]:
60
- """Locate a template's source directory under ``template-library``.
61
-
62
- Returns the absolute path **only when the directory exists**; returns
63
- ``None`` for the standalone / npm-consumer case where there is no
64
- monorepo to reach into. Matches ``gen_script._locate_template_dir``'s
65
- historical contract (callers test the result for None).
66
- """
67
- if not template_id:
68
- return None
69
- tpl_dir = os.path.join(
70
- _REPO_ROOT, "template-library", "packages", "templates", "src", template_id
71
- )
72
- return tpl_dir if os.path.isdir(tpl_dir) else None
@@ -1,273 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- 脆弱点 6②:propExtractors / requiredProps ↔ 组件实际消费 props 的**双向对账**。
4
-
5
- 不同于纯静态「consumed - providable」对账(噪声过大:分不清可选/必填、textLayers
6
- 派生、同名变量污染),本校验用**三个无误报的稳健信号**做对账,且复用 render-video
7
- 的真实 `build_binding`(而非重写绑定逻辑),保证与运行时一致:
8
-
9
- A. requiredProps ⊆ produced
10
- 用真实 dsl-example.json 跑 build_binding,断言每个 slot 声明的 requiredProps
11
- 都真的进了 props。缺失 = 渲染前会回退通用布局(脆弱点 2 的 slideId 事故类)。
12
- —— ERROR
13
-
14
- B. requiredProps ⊆ typeFields
15
- slot 声明 required 的 prop,组件 props 类型(entry.props as <T>,<T> 定义在
16
- types.ts)里必须有该字段;否则 = 契约自相矛盾(声明必填却无处接收)。
17
- —— ERROR
18
-
19
- C. propExtractors keys ⊆ typeFields
20
- 提取器产出的每个 prop,目标组件类型必须声明;否则 = 死字段 / 漂移(提取器辛苦
21
- 算出来却没人读,或类型重命名后提取器没跟)。
22
- —— WARN
23
-
24
- 为什么这三个无误报:它们只用「类型声明(含 ?可选信息)」+「requiredProps 显式契约」
25
- +「真实绑定产出」,完全不猜组件源码里的 `props.x` 成员访问(那才是噪声源,且 tsc
26
- 已覆盖「读了类型里没有的字段」)。
27
-
28
- 与 test-template-pipeline.py 的分工:后者通过 ab-api registry 跑 build_binding,已
29
- 覆盖 A(missingRequiredProps),但**离线 CI 跑不了**(registry_loader 需 ab-api);
30
- 本模块直接从磁盘读 template.json + types.ts,**无需 ab-api**,且额外提供类型级的
31
- B/C 对账(别处都没有)。由 scripts/test-props-contract.py 作为 CI 入口驱动。
32
-
33
- 库接口:
34
- verify_template(template_dir) -> {name, errors[], warnings[], info{}}
35
- """
36
-
37
- from __future__ import annotations
38
-
39
- import json
40
- import os
41
- import re
42
- import sys
43
-
44
- # 复用真实绑定逻辑(同目录 sibling 模块)。registry_loader 无 import 期副作用,
45
- # 不触发 ab-api,故可安全 import。
46
- sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
47
- from match_template import build_binding # noqa: E402
48
-
49
-
50
- CAST_RE = re.compile(r"entry\.props\s+as\s+([A-Za-z_$][\w$]*)")
51
-
52
-
53
- def parse_interface_fields(types_src: str) -> dict:
54
- """从 types.ts 解析 `export interface Name { ... }` → {Name: {field: optional_bool}}.
55
-
56
- 只解析顶层 interface 的顶层成员(足够覆盖模板 props 类型——它们都是扁平接口)。
57
- 嵌套对象类型按整体跳过(不影响顶层字段名收集)。
58
- """
59
- result: dict = {}
60
- i = 0
61
- n = len(types_src)
62
- iface_re = re.compile(r"export\s+interface\s+([A-Za-z_$][\w$]*)\s*\{")
63
- for m in iface_re.finditer(types_src):
64
- name = m.group(1)
65
- # 从 `{` 起做花括号配平,截取 body
66
- start = m.end() - 1
67
- depth = 0
68
- j = start
69
- while j < n:
70
- c = types_src[j]
71
- if c == "{":
72
- depth += 1
73
- elif c == "}":
74
- depth -= 1
75
- if depth == 0:
76
- break
77
- j += 1
78
- body = types_src[start + 1 : j]
79
- result[name] = _parse_members(body)
80
- return result
81
-
82
-
83
- def _parse_members(body: str) -> dict:
84
- """解析接口 body 顶层成员名 + 是否可选。嵌套 { } 整体跳过。"""
85
- fields: dict = {}
86
- i = 0
87
- n = len(body)
88
- # 移除注释,避免把注释里的标识符当成员
89
- body = re.sub(r"/\*.*?\*/", "", body, flags=re.S)
90
- body = re.sub(r"//[^\n]*", "", body)
91
- n = len(body)
92
- # 按顶层切分:在 depth==0 处遇到 `;` 或 `\n` 视为成员边界
93
- member_re = re.compile(r"([A-Za-z_$][\w$]*)\s*(\??)\s*:")
94
- depth = 0
95
- seg_start = 0
96
- segments = []
97
- for k, c in enumerate(body):
98
- if c in "{([":
99
- depth += 1
100
- elif c in "})]":
101
- depth -= 1
102
- elif c in ";\n" and depth == 0:
103
- segments.append(body[seg_start:k])
104
- seg_start = k + 1
105
- segments.append(body[seg_start:])
106
- for seg in segments:
107
- seg = seg.strip()
108
- if not seg:
109
- continue
110
- mm = member_re.match(seg)
111
- if mm:
112
- fields[mm.group(1)] = mm.group(2) == "?"
113
- return fields
114
-
115
-
116
- def resolve_slot_def(slot_mapping: dict, purpose: str) -> dict:
117
- slot_def = slot_mapping.get(purpose) or slot_mapping.get("default") or {}
118
- visited = set()
119
- while isinstance(slot_def, dict) and "$ref" in slot_def:
120
- ref = slot_def["$ref"]
121
- if ref in visited:
122
- break
123
- visited.add(ref)
124
- slot_def = slot_mapping.get(ref) or {}
125
- return slot_def
126
-
127
-
128
- def verify_template(tdir: str) -> dict:
129
- """对单个模板目录做 A/B/C 对账,返回 {errors:[], warnings:[], info:{}}。"""
130
- name = os.path.basename(tdir.rstrip("/"))
131
- errors: list = []
132
- warnings: list = []
133
- info: dict = {}
134
-
135
- tpl_path = os.path.join(tdir, "template.json")
136
- with open(tpl_path, "r", encoding="utf-8") as f:
137
- tpl = json.load(f)
138
-
139
- slot_mapping = tpl.get("slotMapping", {}) or {}
140
- compositions = tpl.get("compositions", []) or []
141
-
142
- # compositionId → componentFile
143
- comp_file = {c.get("compositionId"): c.get("componentFile") for c in compositions}
144
-
145
- # componentFile → cast 类型名(entry.props as <T>)
146
- file_cast: dict = {}
147
- for cf in set(comp_file.values()):
148
- if not cf:
149
- continue
150
- src_path = os.path.normpath(os.path.join(tdir, cf))
151
- if not os.path.exists(src_path):
152
- warnings.append(f"组件文件不存在:{cf}")
153
- continue
154
- with open(src_path, "r", encoding="utf-8") as f:
155
- src = f.read()
156
- casts = CAST_RE.findall(src)
157
- file_cast[cf] = casts[0] if casts else None
158
- if not casts:
159
- warnings.append(f"{cf} 未找到 `entry.props as <Type>`,跳过类型对账")
160
-
161
- # 收集 types.ts(含同目录所有 .ts 里的 interface,稳妥起见全扫)
162
- type_fields: dict = {}
163
- for root, _dirs, files in os.walk(tdir):
164
- for fn in files:
165
- if fn.endswith(".ts") and not fn.endswith(".d.ts"):
166
- try:
167
- with open(os.path.join(root, fn), "r", encoding="utf-8") as f:
168
- type_fields.update(parse_interface_fields(f.read()))
169
- except Exception:
170
- pass
171
-
172
- # compositionId → 类型字段集合
173
- def fields_for_comp(comp_id: str):
174
- cf = comp_file.get(comp_id)
175
- cast = file_cast.get(cf)
176
- if cast and cast in type_fields:
177
- return set(type_fields[cast].keys()), cast
178
- return None, cast
179
-
180
- # ---- B & C:基于 slot 声明(不依赖 dsl-example)----
181
- seen_slots = set()
182
- for purpose in list(slot_mapping.keys()):
183
- sd = resolve_slot_def(slot_mapping, purpose)
184
- comp_id = sd.get("compositionId")
185
- if not comp_id or comp_id in seen_slots:
186
- continue
187
- seen_slots.add(comp_id)
188
- tfields, cast = fields_for_comp(comp_id)
189
- required = sd.get("requiredProps") or []
190
- extractor_keys = list((sd.get("propExtractors") or {}).keys())
191
-
192
- if tfields is None:
193
- if required or extractor_keys:
194
- warnings.append(
195
- f"slot→{comp_id}: 无法解析组件 props 类型({cast}),B/C 跳过"
196
- )
197
- continue
198
-
199
- # B: requiredProps ⊆ typeFields
200
- for rp in required:
201
- if rp not in tfields:
202
- errors.append(
203
- f"[B] slot→{comp_id}: requiredProps 含 '{rp}',但组件类型 {cast} 未声明该字段"
204
- )
205
- # C: propExtractors keys ⊆ typeFields
206
- for ek in extractor_keys:
207
- if ek not in tfields:
208
- warnings.append(
209
- f"[C] slot→{comp_id}: propExtractors 产出 '{ek}',但组件类型 {cast} 未声明 → 死字段/漂移"
210
- )
211
-
212
- # ---- A:用真实 dsl-example 跑 build_binding,requiredProps ⊆ produced ----
213
- ex_path = os.path.join(tdir, "dsl-example.json")
214
- if os.path.exists(ex_path):
215
- with open(ex_path, "r", encoding="utf-8") as f:
216
- dsl = json.load(f)
217
- binding = build_binding(tpl, dsl)
218
- miss_rows = [b for b in binding.get("bindings", []) if b.get("missingRequiredProps")]
219
- for b in miss_rows:
220
- errors.append(
221
- f"[A] scene={b['sceneId']} slot={b['slotId']}: "
222
- f"requiredProps 缺失 {b['missingRequiredProps']}(dsl-example 跑真实绑定后仍未产出)"
223
- )
224
- info["scenes_bound"] = len(binding.get("bindings", []))
225
- else:
226
- warnings.append("缺 dsl-example.json,A(运行时绑定对账)跳过")
227
-
228
- return {"name": name, "errors": errors, "warnings": warnings, "info": info}
229
-
230
-
231
- def verify_all(base: str) -> int:
232
- """对 base/*/template.json 逐个对账,打印报告,返回 error 总数。"""
233
- dirs = sorted(
234
- os.path.join(base, d)
235
- for d in os.listdir(base)
236
- if os.path.isdir(os.path.join(base, d))
237
- and os.path.exists(os.path.join(base, d, "template.json"))
238
- )
239
- if not dirs:
240
- print(f"未发现模板({base}/*/template.json)", file=sys.stderr)
241
- return 0
242
-
243
- total_err = 0
244
- total_warn = 0
245
- for tdir in dirs:
246
- r = verify_template(tdir)
247
- status = "FAIL" if r["errors"] else ("WARN" if r["warnings"] else "PASS")
248
- scenes = r["info"].get("scenes_bound", "-")
249
- print(f"\n=== {r['name']} [{status}] (scenes_bound={scenes}) ===")
250
- for e in r["errors"]:
251
- print(f" ERROR {e}")
252
- for w in r["warnings"]:
253
- print(f" WARN {w}")
254
- if not r["errors"] and not r["warnings"]:
255
- print(" ✓ A/B/C 全部通过")
256
- total_err += len(r["errors"])
257
- total_warn += len(r["warnings"])
258
-
259
- print(f"\n—— 汇总:{len(dirs)} 模板,{total_err} error,{total_warn} warning ——")
260
- return total_err
261
-
262
-
263
- if __name__ == "__main__":
264
- import argparse
265
-
266
- ap = argparse.ArgumentParser(description="脆弱点 6② props 双向对账(A/B/C)")
267
- ap.add_argument("--templates-dir", required=True, help="template-library/packages/templates/src")
268
- args = ap.parse_args()
269
- base = os.path.abspath(args.templates_dir)
270
- if not os.path.isdir(base):
271
- print(f"templates-dir 不存在: {base}", file=sys.stderr)
272
- raise SystemExit(2)
273
- raise SystemExit(1 if verify_all(base) else 0)
@@ -1,87 +0,0 @@
1
- """
2
- 模板 registry 摘要导出器 - 把 ``@ab-templates/metadata`` 的 registry.json
3
- 浓缩为 LLM prompt / 离线脚本使用的精简版本并写盘。
4
-
5
- 历史:本模块之前还托管了 ``bind_template`` / ``load_template`` / ``extract_props``
6
- 等 DSL→TemplateBinding 接口;那条路径已被 ``scripts/match_template.py`` 接管
7
- (由 render-video 直接 import),留在这里的副本无任何外部调用方且与
8
- match_template 的算法已经漂移。删除这些死接口后,本文件只保留 ``sync_registry``
9
- 所需的最小依赖链 (``_load_registry`` → ``load_all_templates`` → ``build_registry``)。
10
-
11
- monorepo 路径解析统一走 ``scripts/template_paths``,避免本文件再自己数 ``..``。
12
- """
13
-
14
- from __future__ import annotations
15
-
16
- import json
17
- import os
18
- import sys
19
- from typing import Optional
20
-
21
- # template_paths 住在 ../../scripts/,把它挂上 sys.path 后才能 import。
22
- _SCRIPTS_DIR = os.path.abspath(
23
- os.path.join(os.path.dirname(__file__), "..", "..", "scripts")
24
- )
25
- if _SCRIPTS_DIR not in sys.path:
26
- sys.path.insert(0, _SCRIPTS_DIR)
27
-
28
- from template_paths import monorepo_registry_path # noqa: E402
29
-
30
- _registry_cache: Optional[dict] = None
31
-
32
-
33
- def _load_registry(registry_path: Optional[str] = None) -> dict:
34
- """Load and cache the template registry from ``@ab-templates/metadata``.
35
-
36
- File-only: this helper never goes to HTTP (that's ``registry_loader`` 的
37
- 职责);``sync_registry`` 的输出只是给 LLM prompt 用,对一致性要求弱,所以
38
- 本地读盘即可。
39
- """
40
- global _registry_cache
41
- if _registry_cache is not None:
42
- return _registry_cache
43
-
44
- path = registry_path or monorepo_registry_path()
45
- if not os.path.isfile(path):
46
- return {"templates": []}
47
-
48
- with open(path, "r", encoding="utf-8") as f:
49
- _registry_cache = json.load(f)
50
- return _registry_cache
51
-
52
-
53
- def load_all_templates(registry_path: Optional[str] = None) -> list[dict]:
54
- """Return every template definition from the metadata registry."""
55
- registry = _load_registry(registry_path)
56
- return registry.get("templates", [])
57
-
58
-
59
- def build_registry(registry_path: Optional[str] = None) -> dict:
60
- """Return the registry in the legacy summary format (for LLM prompts etc.)."""
61
- templates = load_all_templates(registry_path)
62
- matching_fields = [
63
- "templateId", "name", "description", "supportedAspectRatios",
64
- "supportedDurations", "styleTags", "scenePatterns", "assetRequirements",
65
- ]
66
- summary = []
67
- for tpl in templates:
68
- summary.append({k: tpl[k] for k in matching_fields if k in tpl})
69
- return {
70
- "version": "v1alpha1",
71
- "description": "视频模板注册表 - 来自 @ab-templates/metadata",
72
- "templates": summary,
73
- }
74
-
75
-
76
- def sync_registry(output_path: Optional[str] = None, registry_path: Optional[str] = None) -> str:
77
- """Write a local registry-cache.json from ``@ab-templates/metadata``.
78
-
79
- Returns the absolute path of the written file.
80
- """
81
- registry = build_registry(registry_path)
82
- out = output_path or os.path.join(os.path.dirname(__file__), "..", "registry-cache.json")
83
- out = os.path.abspath(out)
84
- with open(out, "w", encoding="utf-8") as f:
85
- json.dump(registry, f, ensure_ascii=False, indent=2)
86
- f.write("\n")
87
- return out