@givolt/i3-composer-agent 2.3.1 → 2.3.2

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 (116) hide show
  1. package/.agents/plugins/marketplace.json +1 -1
  2. package/.claude/commands/i3-prototype.md +5 -7
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/.claude-plugin/plugin.json +1 -1
  5. package/.codex-plugin/plugin.json +1 -1
  6. package/.gemini/commands/i3-prototype.toml +5 -7
  7. package/README.md +12 -7
  8. package/bin/i3-composer-agent.cjs +33 -2
  9. package/bin/install.cjs +14 -0
  10. package/bin/native-migration.cjs +27 -3
  11. package/commands/i3-prototype.toml +5 -7
  12. package/docs/codebuddy-setup.md +65 -2
  13. package/docs/codex-setup.md +4 -0
  14. package/docs/other-hosts.md +1 -1
  15. package/docs/ui-prototype-native/MAIN-SESSION-REPORT.md +66 -0
  16. package/docs/ui-prototype-native/USAGE.md +91 -0
  17. package/docs/workbuddy-setup.md +4 -0
  18. package/evals/cases/i3-frontend-ui-engineering.json +16 -0
  19. package/evals/cases/i3-ui-prototype.json +109 -0
  20. package/evals/cases/i3-using-agent-skills.json +28 -0
  21. package/evals/fixtures/i3-ui-prototype/admin-list/assets/reference.css +3 -0
  22. package/evals/fixtures/i3-ui-prototype/admin-list/assets/reference.js +8 -0
  23. package/evals/fixtures/i3-ui-prototype/admin-list/capability-definition.json +67 -0
  24. package/evals/fixtures/i3-ui-prototype/admin-list/index.html +89 -0
  25. package/evals/fixtures/i3-ui-prototype/admin-list/readme.md +52 -0
  26. package/evals/fixtures/i3-ui-prototype/admin-list/template.json +8 -0
  27. package/evals/zh-CN/cases/i3-frontend-ui-engineering.json +17 -0
  28. package/evals/zh-CN/cases/i3-ui-prototype.json +115 -0
  29. package/evals/zh-CN/cases/i3-using-agent-skills.json +30 -0
  30. package/package.json +31 -1
  31. package/plugin.json +1 -1
  32. package/scripts/design-provider-test.js +7 -0
  33. package/scripts/enterprise-template-test.js +27 -1
  34. package/scripts/lib/prototype-fixture.cjs +17 -0
  35. package/scripts/prototype-locations-test.js +58 -0
  36. package/scripts/ui-prototype-content-test.js +123 -0
  37. package/scripts/ui-prototype-dependencies-test.js +213 -0
  38. package/scripts/ui-prototype-devtools-test.js +53 -0
  39. package/scripts/ui-prototype-native-test.js +504 -0
  40. package/scripts/ui-prototype-template-test.js +261 -0
  41. package/scripts/ui-prototype-test.js +10 -0
  42. package/skills/i3-ui-prototype/SKILL.md +138 -19
  43. package/skills/i3-ui-prototype/assets/capabilities/management.json +294 -0
  44. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/acceptance.json +51 -0
  45. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/component-map.json +13 -0
  46. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/index.html +11 -0
  47. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/package-lock.json +2298 -0
  48. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/package.json +22 -0
  49. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/public/prototype-config.json +6 -0
  50. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/App.vue +136 -0
  51. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/components/DeviceEditor.vue +111 -0
  52. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/config.ts +19 -0
  53. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/content.ts +28 -0
  54. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/domain/devices.ts +50 -0
  55. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/domain/theme.ts +25 -0
  56. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/main.ts +18 -0
  57. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/style.css +50 -0
  58. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/useDevices.ts +66 -0
  59. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/tests/devices.test.ts +44 -0
  60. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/tests/theme.test.ts +26 -0
  61. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/tsconfig.json +15 -0
  62. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/vite.config.ts +4 -0
  63. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/profile.json +174 -0
  64. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/readme.md +61 -0
  65. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/index.html +11 -0
  66. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/package-lock.json +2298 -0
  67. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/package.json +22 -0
  68. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/public/prototype-config.json +21 -0
  69. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/App.vue +51 -0
  70. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/config.ts +18 -0
  71. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/domain/theme.ts +25 -0
  72. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/main.ts +18 -0
  73. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/style.css +13 -0
  74. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/tests/theme.test.ts +26 -0
  75. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/tsconfig.json +15 -0
  76. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/vite.config.ts +4 -0
  77. package/skills/i3-ui-prototype/assets/optional-skills/host-targets.json +737 -0
  78. package/skills/i3-ui-prototype/assets/optional-skills/sources.json +27 -0
  79. package/skills/i3-ui-prototype/assets/templates/catalog.js +149 -0
  80. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/capability-definition.json +294 -0
  81. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/index.html +507 -2
  82. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/offline-build.json +265 -0
  83. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/offline.js +8888 -0
  84. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/template.json +9 -0
  85. package/skills/i3-ui-prototype/assets/templates/gallery/index.html +4 -4
  86. package/skills/i3-ui-prototype/assets/templates/index.html +3 -0
  87. package/skills/i3-ui-prototype/references/artifact-contract.md +25 -0
  88. package/skills/i3-ui-prototype/references/content-template.md +76 -0
  89. package/skills/i3-ui-prototype/references/enterprise-admin.md +6 -0
  90. package/skills/i3-ui-prototype/references/entry-contract.md +71 -0
  91. package/skills/i3-ui-prototype/references/expert-adapter.md +33 -5
  92. package/skills/i3-ui-prototype/references/framework-contract.md +76 -0
  93. package/skills/i3-ui-prototype/references/interaction-acceptance.md +165 -0
  94. package/skills/i3-ui-prototype/references/legacy-native.md +58 -0
  95. package/skills/i3-ui-prototype/references/optional-skills.md +59 -0
  96. package/skills/i3-ui-prototype/references/template-adapters.md +1 -1
  97. package/skills/i3-ui-prototype/references/workflow-contract.md +58 -4
  98. package/skills/i3-ui-prototype/scripts/adapters/easel-html.cjs +1 -1
  99. package/skills/i3-ui-prototype/scripts/content-template.cjs +98 -0
  100. package/skills/i3-ui-prototype/scripts/enterprise-template.cjs +13 -6
  101. package/skills/i3-ui-prototype/scripts/manage-baseline.cjs +1 -1
  102. package/skills/i3-ui-prototype/scripts/native-browser.cjs +785 -0
  103. package/skills/i3-ui-prototype/scripts/native-build.cjs +91 -0
  104. package/skills/i3-ui-prototype/scripts/native-delivery.cjs +91 -0
  105. package/skills/i3-ui-prototype/scripts/native-devtools.cjs +91 -0
  106. package/skills/i3-ui-prototype/scripts/native-files.cjs +35 -0
  107. package/skills/i3-ui-prototype/scripts/native-pilot-lib.cjs +244 -0
  108. package/skills/i3-ui-prototype/scripts/native-pilot.cjs +34 -0
  109. package/skills/i3-ui-prototype/scripts/optional-skill-install.cjs +203 -0
  110. package/skills/i3-ui-prototype/scripts/prototype-dependencies.cjs +81 -0
  111. package/skills/i3-ui-prototype/scripts/prototype-lib.cjs +26 -6
  112. package/skills/i3-ui-prototype/scripts/prototype-locations.cjs +65 -0
  113. package/skills/i3-ui-prototype/scripts/sync-template.cjs +19 -0
  114. package/skills/i3-ui-prototype/scripts/template-contract.cjs +398 -0
  115. package/skills/i3-ui-prototype/scripts/validate-result.cjs +117 -0
  116. package/skills/i3-using-agent-skills/SKILL.md +2 -5
@@ -17,6 +17,26 @@
17
17
  {
18
18
  "prompt": "根据评审反馈迭代交互原型,保留已确认的原型基线。",
19
19
  "top_k": 1
20
+ },
21
+ {
22
+ "prompt": "参考这份可编辑 HTML 内容模板,用 Vue 3 和 antDv-next 生成设备管理交互原型。",
23
+ "top_k": 1
24
+ },
25
+ {
26
+ "prompt": "先分析内容模板与业务需求如何形成交互原型方案,不修改文件。",
27
+ "top_k": 1
28
+ },
29
+ {
30
+ "prompt": "根据反馈修改交互原型,保留我手改的代码并检查抽屉退出动画。",
31
+ "top_k": 1
32
+ },
33
+ {
34
+ "prompt": "请当前会话直接使用原型技能检查待评审候选,不启动其他智能体。",
35
+ "top_k": 1
36
+ },
37
+ {
38
+ "prompt": "请主会话根据 HTML 内容参考和业务说明分析交互原型方案,保持只读。",
39
+ "top_k": 1
20
40
  }
21
41
  ],
22
42
  "negative": [
@@ -27,6 +47,10 @@
27
47
  {
28
48
  "prompt": "产品页面有一千条数据渲染很慢,请定位性能瓶颈并优化。",
29
49
  "owner": "i3-performance-optimization"
50
+ },
51
+ {
52
+ "prompt": "昨天通过的测试现在失败,请复现并定位根因。",
53
+ "owner": "i3-debugging-and-error-recovery"
30
54
  }
31
55
  ]
32
56
  },
@@ -45,6 +69,97 @@
45
69
  "The prototype remains pending human review and no approved baseline is invented",
46
70
  "人类可读的追问、说明、进度、报告与生成文档使用简体中文;保留代码、路径、工具参数、JSON 协议及机器状态的原始格式。"
47
71
  ]
72
+ },
73
+ {
74
+ "id": 1002,
75
+ "kind": "dialogue",
76
+ "prompt": "请当前会话直接使用 i3-ui-prototype 技能,分析可编辑 HTML 内容模板和通用能力清单如何生成 Vue 3+antDv-next 设备列表原型。仅分析:不改文件、不安装升级、不持久化设计结果、不启动另一个智能体。",
77
+ "expected_output": "A read-only prototype analysis that keeps direct Skill execution valid and states the framework, content, capability and acceptance boundaries",
78
+ "expectations": [
79
+ "The current executor runs the shared preflight without redispatching to an Agent, command or meta-skill",
80
+ "The response preserves analysis-only scope and does not initialize, install, persist design output or update project execution state",
81
+ "Editable content reference, reusable capabilities and target-framework behavior are explained as separate inputs",
82
+ "Unavailable provider, host or browser capabilities remain explicitly unverified rather than reported as executed",
83
+ "The analysis deliverable is the conversation; it does not invent prototype completion or human approval",
84
+ "人类可读的追问、说明、进度、报告与生成文档使用简体中文;保留代码、路径、工具参数、JSON 协议及机器状态的原始格式。"
85
+ ]
86
+ },
87
+ {
88
+ "id": 1003,
89
+ "prompt": "请使用 admin-list 可编辑内容模板,创建独立的 Vue 3+antDv-next 原生组件设备列表原型,本例明确选择 template 模式。要求:固定 21 条设备,每页 10 条;筛选或翻页清空当前页选择;批量状态变更仅影响选中记录;编辑抽屉校验必填名称,模拟保存失败时保留输入,重试后更新正确行。有真实浏览器时验证当前构建的抽屉退出动效和遮罩模糊。保留已有原型,交付待人工评审候选。",
90
+ "expected_output": "An isolated native component prototype with fixed capability inputs, reproducible local data, current-build verification or a truthful verification gap",
91
+ "files": [
92
+ "i3-ui-prototype"
93
+ ],
94
+ "expectations": [
95
+ "The instance uses the native framework runtime and fixed template, capability, scope and framework inputs rather than converting an existing legacy instance",
96
+ "Real antDv-next components implement filtering, current-page selection, pagination and the edit flow with deterministic isolated mock state",
97
+ "Required capability bindings remain independently checkable and are not removed or arbitrarily marked inapplicable to make checks pass",
98
+ "The executor distinguishes source inspection, successful build and real browser execution; unavailable checks remain unverified",
99
+ "Browser evidence, when present, refers to the current candidate and actual build, with drawer motion and blur verified separately from static screenshots",
100
+ "Template mode is explicit and does not claim UIUX use; existing prototypes remain unchanged and no human baseline approval is invented",
101
+ "人类可读的追问、说明、进度、报告与生成文档使用简体中文;保留代码、路径、工具参数、JSON 协议及机器状态的原始格式。"
102
+ ]
103
+ },
104
+ {
105
+ "id": 1004,
106
+ "kind": "dialogue",
107
+ "prompt": "请只读检查这份原型验收声明:测试后源码已改,服务仍展示上一版构建,证据只有一张截图和手动填 passed 的报告。同事建议删除分页能力并标为不适用,以便批准候选。请说明正确的后续动作;不改文件、不执行新测试、不确认基线。",
108
+ "expected_output": "A read-only rejection of stale evidence and unsupported exemption, with concrete required revalidation and pending human review",
109
+ "expectations": [
110
+ "The changed candidate and previous build invalidate the supplied acceptance claim",
111
+ "A screenshot or manually passed report is not treated as proof of current-build browser execution or drawer animation",
112
+ "Pagination remains required unless an explicit requirement or scope-change source justifies an exemption",
113
+ "The response proposes rebuilding and revalidating against fixed inputs without claiming those actions already occurred",
114
+ "No files are changed, no tests are pretended, and human baseline approval remains separate",
115
+ "人类可读的追问、说明、进度、报告与生成文档使用简体中文;保留代码、路径、工具参数、JSON 协议及机器状态的原始格式。"
116
+ ]
117
+ },
118
+ {
119
+ "id": 1005,
120
+ "kind": "dialogue",
121
+ "prompt": "请在主会话中分析可编辑 admin-list HTML 参考和业务说明,形成 Vue 3+antDv-next 评审原型方案。保持只读。宿主没有原型专责角色,也没有父子 JSON 传输机制。请交付有用的分析和后续步骤,不安装工具,不执行创建阶段的检查。",
122
+ "expected_output": "A complete read-only main-session analysis with useful coverage and a truthful acceptance plan",
123
+ "expectations": [
124
+ "The main session directly applies the shared prototype preflight without requiring or simulating a dedicated Agent",
125
+ "The analysis delivers requirement and bound-capability coverage, template and framework selection, acceptance planning and concrete gaps",
126
+ "No initialization, installation, persisted design, project state update, build, browser run or creation-stage UIUX adoption gate occurs",
127
+ "No parent-child JSON, PROTOTYPE_RESULT_V1 marker or validate-result execution is made a prerequisite for the natural-language deliverable",
128
+ "Unavailable capabilities and fixed-parameter conflicts remain visible; analysis completion is distinct from implemented coverage and human approval",
129
+ "人类可读的追问、说明、进度、报告与生成文档使用简体中文;保留代码、路径、工具参数、JSON 协议及机器状态的原始格式。"
130
+ ]
131
+ },
132
+ {
133
+ "id": 1006,
134
+ "prompt": "请根据可编辑 admin-list 参考创建独立的 Vue 3+antDv-next 原生组件设备列表评审候选,明确使用 template 模式。我授权页面内四种主题:明亮、暗黑、清新、科技,首次默认明亮。请在我的 HTML 工作副本中启用并固定主题需求,切换时保留业务状态,保留全部已有原型和手改内容。不要再次询问是否需要主题。保持固定必需能力,指定浏览器工具可用时验证当前构建。",
135
+ "expected_output": "A main-session native review candidate with explicitly fixed four-theme inputs, preserved state and truthful current-build evidence or gaps",
136
+ "files": [
137
+ "i3-ui-prototype"
138
+ ],
139
+ "expectations": [
140
+ "The main session uses the prototype workflow and supporting frontend and version-matched official framework knowledge for actual implementation, without requiring a dedicated persona",
141
+ "The authorized theme capability is bound in the user HTML working copy and fixed with its requirement source rather than left disabled or sent for repeat confirmation",
142
+ "The generated framework implements the four exact theme names, defaults to 明亮, and verifies that business state survives theme changes",
143
+ "Existing or unrequested-theme instances, user edits and approved baselines remain unchanged",
144
+ "Template mode does not claim UIUX use; fixed required capabilities and applicable acceptance scenarios are retained",
145
+ "Build, actual browser Skill and DevTools observations, supplemental regression and current delivery evidence are distinguished; unavailable checks remain incomplete",
146
+ "The final natural-language delivery identifies the candidate, current evidence and gaps, and leaves human review pending without requiring a parent-child result envelope",
147
+ "人类可读的追问、说明、进度、报告与生成文档使用简体中文;保留代码、路径、工具参数、JSON 协议及机器状态的原始格式。"
148
+ ]
149
+ },
150
+ {
151
+ "id": 1007,
152
+ "kind": "dialogue",
153
+ "prompt": "请比较三种请求的处理方式:只读分析原型方案;为评审修改原型抽屉交互;把已认可抽屉实现为可复用生产组件。说明各自适用的技能和证据,不执行这些请求。",
154
+ "expected_output": "Operation-specific support and a clear boundary between prototype work and production frontend engineering",
155
+ "expectations": [
156
+ "Read-only prototype analysis uses only material needed for coverage and selection and plans later verification without executing it",
157
+ "Prototype interaction revision stays within prototype workflow ownership and uses frontend, official framework and browser capabilities when the revision requires them",
158
+ "Professional UIUX lookup, adoption and effect verification remain distinct and apply to professional design work, with no silent downgrade to template mode",
159
+ "Approved production component implementation follows the frontend implementation method and the existing engineering workflow rather than creating a new review prototype",
160
+ "No claim of completed runtime checks or human baseline approval is made",
161
+ "人类可读的追问、说明、进度、报告与生成文档使用简体中文;保留代码、路径、工具参数、JSON 协议及机器状态的原始格式。"
162
+ ]
48
163
  }
49
164
  ]
50
165
  }
@@ -23,6 +23,10 @@
23
23
  {
24
24
  "prompt": "为项目配置 CI/CD 流水线,自动运行构建和测试。",
25
25
  "owner": "i3-ci-cd-and-automation"
26
+ },
27
+ {
28
+ "prompt": "选择 UI 原型模板,制作一个可操作的申请审批交互原型供评审。",
29
+ "owner": "i3-ui-prototype"
26
30
  }
27
31
  ]
28
32
  },
@@ -40,6 +44,32 @@
40
44
  "Core operating behaviors (assumptions surfaced) are respected",
41
45
  "人类可读的追问、说明、进度、报告与生成文档使用简体中文;保留代码、路径、工具参数、JSON 协议及机器状态的原始格式。"
42
46
  ]
47
+ },
48
+ {
49
+ "id": 1002,
50
+ "kind": "dialogue",
51
+ "prompt": "用户希望根据可编辑 HTML 参考和简短业务说明制作设备管理评审原型。请在当前会话选择适用技能并说明下一步;不创建文件,不启动独立任务。",
52
+ "expected_output": "Ordinary routing to i3-ui-prototype in the main session, with operation and scope decided by the working skill",
53
+ "expectations": [
54
+ "The meta-skill selects the prototype workflow for reviewable prototype work",
55
+ "The main session uses the working skill without requiring a dedicated persona, persistent routing rule or parent-child result protocol",
56
+ "Detailed operation, framework and acceptance decisions remain in the prototype workflow rather than being copied into the meta-skill",
57
+ "The requested read-only routing explanation does not initialize a prototype or claim implementation",
58
+ "人类可读的追问、说明、进度、报告与生成文档使用简体中文;保留代码、路径、工具参数、JSON 协议及机器状态的原始格式。"
59
+ ]
60
+ },
61
+ {
62
+ "id": 1003,
63
+ "kind": "dialogue",
64
+ "prompt": "分别收到两个任务:实现已认可的生产 Vue 表单组件并支持键盘操作;对照已认可基线检查评审原型。请分别路由并说明原工程工作流如何继续适用,不执行这两个任务。",
65
+ "expected_output": "Separate routing for production engineering and prototype review without changing the general lifecycle",
66
+ "expectations": [
67
+ "Production UI implementation follows the engineering workflow with the frontend implementation method",
68
+ "Prototype fidelity inspection selects the prototype workflow",
69
+ "The prototype route does not replace generic planning, implementation or review methods or impose prototype-specific delegation on them",
70
+ "The response does not invent runtime verification or human approval",
71
+ "人类可读的追问、说明、进度、报告与生成文档使用简体中文;保留代码、路径、工具参数、JSON 协议及机器状态的原始格式。"
72
+ ]
43
73
  }
44
74
  ]
45
75
  }
package/package.json CHANGED
@@ -100,6 +100,12 @@
100
100
  "evals/fixtures/i3-test-driven-development/package.json",
101
101
  "evals/fixtures/i3-test-driven-development/src/split.js",
102
102
  "evals/fixtures/i3-test-driven-development/test/split.test.js",
103
+ "evals/fixtures/i3-ui-prototype/admin-list/assets/reference.css",
104
+ "evals/fixtures/i3-ui-prototype/admin-list/assets/reference.js",
105
+ "evals/fixtures/i3-ui-prototype/admin-list/capability-definition.json",
106
+ "evals/fixtures/i3-ui-prototype/admin-list/index.html",
107
+ "evals/fixtures/i3-ui-prototype/admin-list/readme.md",
108
+ "evals/fixtures/i3-ui-prototype/admin-list/template.json",
103
109
  "evals/fixtures/i3-ui-prototype/business-brief.md",
104
110
  "evals/fixtures/i3-using-agent-skills/incident.md",
105
111
  "evals/fixtures/incremental-implementation-pressure/draft-export.js",
@@ -163,6 +169,8 @@
163
169
  "docs/getting-started.md",
164
170
  "docs/opencode-setup.md",
165
171
  "docs/other-hosts.md",
172
+ "docs/ui-prototype-native/MAIN-SESSION-REPORT.md",
173
+ "docs/ui-prototype-native/USAGE.md",
166
174
  "docs/windsurf-setup.md",
167
175
  "docs/workbuddy-setup.md",
168
176
  "plugin.json",
@@ -172,12 +180,19 @@
172
180
  "scripts/design-provider-test.js",
173
181
  "scripts/enterprise-template-test.js",
174
182
  "scripts/floor-guard-reference-test.js",
183
+ "scripts/lib/prototype-fixture.cjs",
175
184
  "scripts/lib/skill-lint-test.js",
176
185
  "scripts/lib/skill-lint.js",
186
+ "scripts/prototype-locations-test.js",
177
187
  "scripts/run-evals-test.js",
178
188
  "scripts/run-evals.js",
179
189
  "scripts/run-regressions.js",
180
190
  "scripts/template-adapter-test.js",
191
+ "scripts/ui-prototype-content-test.js",
192
+ "scripts/ui-prototype-dependencies-test.js",
193
+ "scripts/ui-prototype-devtools-test.js",
194
+ "scripts/ui-prototype-native-test.js",
195
+ "scripts/ui-prototype-template-test.js",
181
196
  "scripts/ui-prototype-test.js",
182
197
  "scripts/validate-artifact-paths-test.js",
183
198
  "scripts/validate-artifact-paths.js",
@@ -287,6 +302,12 @@
287
302
  "evals/fixtures/i3-test-driven-development/package.json",
288
303
  "evals/fixtures/i3-test-driven-development/src/split.js",
289
304
  "evals/fixtures/i3-test-driven-development/test/split.test.js",
305
+ "evals/fixtures/i3-ui-prototype/admin-list/assets/reference.css",
306
+ "evals/fixtures/i3-ui-prototype/admin-list/assets/reference.js",
307
+ "evals/fixtures/i3-ui-prototype/admin-list/capability-definition.json",
308
+ "evals/fixtures/i3-ui-prototype/admin-list/index.html",
309
+ "evals/fixtures/i3-ui-prototype/admin-list/readme.md",
310
+ "evals/fixtures/i3-ui-prototype/admin-list/template.json",
290
311
  "evals/fixtures/i3-ui-prototype/business-brief.md",
291
312
  "evals/fixtures/i3-using-agent-skills/incident.md",
292
313
  "evals/fixtures/incremental-implementation-pressure/draft-export.js",
@@ -350,6 +371,8 @@
350
371
  "docs/getting-started.md",
351
372
  "docs/opencode-setup.md",
352
373
  "docs/other-hosts.md",
374
+ "docs/ui-prototype-native/MAIN-SESSION-REPORT.md",
375
+ "docs/ui-prototype-native/USAGE.md",
353
376
  "docs/windsurf-setup.md",
354
377
  "docs/workbuddy-setup.md",
355
378
  "plugin.json",
@@ -360,12 +383,19 @@
360
383
  "scripts/design-provider-test.js",
361
384
  "scripts/enterprise-template-test.js",
362
385
  "scripts/floor-guard-reference-test.js",
386
+ "scripts/lib/prototype-fixture.cjs",
363
387
  "scripts/lib/skill-lint-test.js",
364
388
  "scripts/lib/skill-lint.js",
389
+ "scripts/prototype-locations-test.js",
365
390
  "scripts/run-evals-test.js",
366
391
  "scripts/run-evals.js",
367
392
  "scripts/run-regressions.js",
368
393
  "scripts/template-adapter-test.js",
394
+ "scripts/ui-prototype-content-test.js",
395
+ "scripts/ui-prototype-dependencies-test.js",
396
+ "scripts/ui-prototype-devtools-test.js",
397
+ "scripts/ui-prototype-native-test.js",
398
+ "scripts/ui-prototype-template-test.js",
369
399
  "scripts/ui-prototype-test.js",
370
400
  "scripts/validate-artifact-paths-test.js",
371
401
  "scripts/validate-artifact-paths.js",
@@ -378,7 +408,7 @@
378
408
  "scripts/validate-versions.js"
379
409
  ]
380
410
  },
381
- "version": "2.3.1",
411
+ "version": "2.3.2",
382
412
  "repository": {
383
413
  "type": "git",
384
414
  "url": "https://github.com/49780522/i3-composer-agent.git"
package/plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i3-composer-agent",
3
- "version": "2.3.1",
3
+ "version": "2.3.2",
4
4
  "description": "覆盖需求、计划、开发、测试、审查与发布的软件工程技能,默认使用简体中文。",
5
5
  "homepage": "https://github.com/49780522/i3-composer-agent",
6
6
  "repository": "https://github.com/49780522/i3-composer-agent"
@@ -66,3 +66,10 @@ test('all unusable providers preserve their individual failure reasons without a
66
66
  assert.ok(!result.choices.some(choice => choice.id === 'install'));
67
67
  assert.equal(fs.existsSync(path.join(home, 'prototypes')), false);
68
68
  });
69
+ test('a business package version cannot masquerade as the later installed UIUX version', t => {
70
+ const home = fixture(t), root = provider(home, 'manual-provider');
71
+ fs.writeFileSync(path.join(home, 'package.json'), JSON.stringify({ name: 'human-business-project', version: '99.88.77' }));
72
+ assert.equal(lib.checkDesignProvider({ provider: root }).version, 'unversioned');
73
+ fs.writeFileSync(path.join(root, 'UPSTREAM-ORIGIN.json'), JSON.stringify({ packageName: 'ui-ux-pro-max-cli', version: '2.15.0' }));
74
+ assert.equal(lib.checkDesignProvider({ provider: root }).version, '2.15.0');
75
+ });
@@ -104,11 +104,37 @@ test('generated enterprise instances isolate their local demo storage and retain
104
104
  const source = fs.readFileSync(path.join(dir, 'app/src/shared/session.js'), 'utf8');
105
105
  assert.match(source, /i3-enterprise-enterprise-demo-/);
106
106
  assert.doesNotMatch(source, /i3-prototype-antdv-/);
107
- assert.match(fs.readFileSync(path.join(dir, 'app/index.html'), 'utf8'), /enterprise-bootstrap\.mjs/);
107
+ assert.match(fs.readFileSync(path.join(dir, 'app/index.html'), 'utf8'), /src="offline\.js"/);
108
+ assert.ok(fs.existsSync(path.join(dir, 'app/enterprise-bootstrap.mjs')), 'Retain the historical module bootstrap for compatibility');
108
109
  assert.match(fs.readFileSync(path.join(dir, 'app/index.html'), 'utf8'), /tokens\.css/);
109
110
  const other = initialize(t);
110
111
  assert.notEqual(read(dir, 'prototype.json').runtime.storagePrefix, read(other, 'prototype.json').runtime.storagePrefix);
111
112
  });
113
+ test('offline entry carries every editable HTML fragment and routes without changing the reference', t => {
114
+ const root = path.join(skill, 'assets/templates/enterprise-admin');
115
+ const build = read(root, 'offline-build.json');
116
+ for (const [file, expected] of Object.entries(build.files)) {
117
+ assert.equal(crypto.createHash('sha256').update(fs.readFileSync(path.join(root, file))).digest('hex'), expected, file);
118
+ }
119
+ const html = fs.readFileSync(path.join(root, 'index.html'), 'utf8');
120
+ assert.equal([...html.matchAll(/<template data-source=/g)].length, build.htmlFragmentCount);
121
+ const routes = JSON.parse(html.match(/id="prototype-offline-routes">([\s\S]*?)<\/script>/)[1]);
122
+ assert.deepEqual(routes, read(root, 'reference/routes.json'));
123
+ for (const component of new Set(routes.map(route => route.component).filter(Boolean))) {
124
+ const source = fs.readFileSync(path.resolve(root, 'reference', component), 'utf8');
125
+ const fragment = source.match(/template: new URL\('\.\/([^']+\.html)'/)[1];
126
+ assert.ok(html.includes(`data-source="reference/${path.posix.dirname(component).replace(/^\.\//, '')}/${fragment}"`), component);
127
+ }
128
+ assert.doesNotMatch(html, /type="module"/);
129
+ assert.doesNotMatch(fs.readFileSync(path.join(root, 'offline.js'), 'utf8'), /\bfetch\s*\(|\bimport\s*\(|import\.meta/);
130
+ const dir = initialize(t, { title: '离线配置 <安全标题>' });
131
+ const instance = fs.readFileSync(path.join(dir, 'app/index.html'), 'utf8');
132
+ assert.equal(JSON.parse(instance.match(/id="prototype-offline-config">([\s\S]*?)<\/script>/)[1]).title, '离线配置 <安全标题>');
133
+ assert.equal(read(dir, 'prototype.json').runtime.offlineVersion, 1);
134
+ assert.equal(lib.checkPrototype(dir).status, 'passed');
135
+ fs.unlinkSync(path.join(dir, 'app/offline.js'));
136
+ assert.equal(lib.checkPrototype(dir).status, 'failed', 'A new offline instance must require its runtime');
137
+ });
112
138
  test('missing enterprise runtime imports are rejected while declarative data-bind attributes remain valid', t => {
113
139
  const dir = initialize(t);
114
140
  assert.equal(lib.checkPrototype(dir).status, 'passed');
@@ -0,0 +1,17 @@
1
+ 'use strict';
2
+ // Historical admin-list input for regression tests only; no public template entry.
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ function prepareTemplate(contract, { project, id = 'admin-list' }) {
6
+ if (!/^[a-z][a-z0-9-]{0,63}$/.test(id)) throw new Error('Invalid identifier');
7
+ const directory = path.resolve(project, 'ui-templates', id);
8
+ if (fs.existsSync(path.join(project, 'ui-templates')) && fs.lstatSync(path.join(project, 'ui-templates')).isSymbolicLink()) throw new Error('Refusing symlink');
9
+ if (fs.existsSync(directory)) return { directory, created: false, index: contract.loadTemplate(directory).index };
10
+ fs.cpSync(path.resolve(__dirname, '../../evals/fixtures/i3-ui-prototype/admin-list'), directory, { recursive: true });
11
+ const file = path.join(directory, 'template.json');
12
+ const manifest = JSON.parse(fs.readFileSync(file));
13
+ manifest.id = id;
14
+ fs.writeFileSync(file, JSON.stringify(manifest, null, 2) + '\n');
15
+ return { directory, created: true, index: contract.syncTemplate(directory) };
16
+ }
17
+ module.exports = { prepareTemplate };
@@ -0,0 +1,58 @@
1
+ 'use strict';
2
+ const test = require('node:test');
3
+ const assert = require('node:assert/strict');
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+ const os = require('node:os');
7
+ const installation = path.join(__dirname, 'lib/installation.js');
8
+ const skills = fs.existsSync(installation) ? require(installation).loadInstallation().skillsDirectory : path.resolve(__dirname, '../skills');
9
+ const locations = require(path.join(skills, 'i3-ui-prototype/scripts/prototype-locations.cjs'));
10
+ function workspace(t) {
11
+ const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'i3-tool-locations-')));
12
+ t.after(() => fs.rmSync(root, { recursive: true, force: true }));
13
+ const project = path.join(root, 'business'), home = path.join(root, 'home');
14
+ fs.mkdirSync(project); fs.mkdirSync(home);
15
+ return { project, home, env: {} };
16
+ }
17
+ test('all registered project and global targets keep tools under their host base without writes', t => {
18
+ const options = workspace(t);
19
+ for (const agent of locations.registry.agents) {
20
+ const local = locations.resolveLocations({ ...options, host: agent.id });
21
+ assert.equal(local.skillsDirectory, path.resolve(options.project, agent.projectSkillsDir));
22
+ const toolBase = path.dirname(local.skillsDirectory) === options.project ? path.join(options.project, `.${agent.id}`) : path.dirname(local.skillsDirectory);
23
+ assert.equal(local.toolsDirectory, path.join(toolBase, 'tools/i3-prototype'));
24
+ assert.notEqual(local.toolsDirectory, path.join(options.project, 'tools/i3-prototype'));
25
+ if (agent.globalSkillsDir) {
26
+ const global = locations.resolveLocations({ ...options, host: agent.id, scope: 'global' });
27
+ assert.equal(global.toolsDirectory, path.join(path.dirname(global.skillsDirectory), 'tools/i3-prototype'));
28
+ assert.ok(global.base.startsWith(options.home + path.sep));
29
+ } else assert.throws(() => locations.resolveLocations({ ...options, host: agent.id, scope: 'global' }), /project installation only/);
30
+ }
31
+ assert.deepEqual(fs.readdirSync(options.project), []);
32
+ assert.deepEqual(fs.readdirSync(options.home), []);
33
+ });
34
+ test('CodeBuddy and WorkBuddy share project tools but use separate global roots; Codex follows agents', t => {
35
+ const options = workspace(t), get = (host, scope = 'project') => locations.resolveLocations({ ...options, host, scope });
36
+ assert.equal(get('codebuddy').toolsDirectory, get('workbuddy').toolsDirectory);
37
+ assert.notEqual(get('codebuddy', 'global').toolsDirectory, get('workbuddy', 'global').toolsDirectory);
38
+ assert.equal(get('codex').base, path.join(options.project, '.agents'));
39
+ assert.equal(get('codex', 'global').base, path.join(options.home, '.agents'));
40
+ });
41
+ test('configuration roots and aliases follow installer mapping and explicit global checks exclude business paths', t => {
42
+ const options = workspace(t);
43
+ const context = locations.resolveLocations({ ...options, host: 'claude', scope: 'global', env: { CLAUDE_CONFIG_DIR: path.join(options.home, 'custom claude') } });
44
+ assert.equal(context.base, path.join(options.home, 'custom claude'));
45
+ assert.equal(context.host, 'claude-code');
46
+ const opencode = locations.resolveLocations({ ...options, host: 'opencode', env: { XDG_CONFIG_HOME: path.join(options.home, 'config') }, scope: 'global' });
47
+ assert.equal(opencode.base, path.join(options.home, 'config/opencode'));
48
+ assert.ok(locations.toolCandidates(context).every(item => !item.root.startsWith(options.project + path.sep)));
49
+ assert.throws(() => locations.resolveLocations({ ...options, host: 'unknown' }), /Unknown host/);
50
+ assert.throws(() => locations.resolveLocations({ ...options, scope: 'all' }), /scope/);
51
+ });
52
+ test('new project and global tools precede both legacy layouts', t => {
53
+ const options = workspace(t), context = locations.resolveLocations({ ...options, host: 'codex' });
54
+ const paths = locations.toolCandidates(context);
55
+ assert.deepEqual(paths.map(item => item.source), ['project-tool', 'global-tool', 'legacy-project-tool', 'legacy-project-tool']);
56
+ assert.ok(paths[0].root.includes('/.agents/tools/i3-prototype/antdv-cli/'));
57
+ assert.ok(paths.at(-1).root.includes('/.prototype-tools/node_modules/'));
58
+ });
@@ -0,0 +1,123 @@
1
+ 'use strict';
2
+ const test = require('node:test');
3
+ const assert = require('node:assert/strict');
4
+ const fs = require('node:fs');
5
+ const os = require('node:os');
6
+ const path = require('node:path');
7
+ const crypto = require('node:crypto');
8
+ const installation = path.join(__dirname, 'lib/installation.js');
9
+ const skills = fs.existsSync(installation) ? require(installation).loadInstallation().skillsDirectory : path.resolve(__dirname, '../skills');
10
+ const root = path.join(skills, 'i3-ui-prototype');
11
+ const contract = require(path.join(root, 'scripts/template-contract.cjs'));
12
+ const native = require(path.join(root, 'scripts/native-pilot-lib.cjs'));
13
+ const read = (directory, file) => JSON.parse(fs.readFileSync(path.join(directory, file), 'utf8'));
14
+ const write = (directory, file, data) => fs.writeFileSync(path.join(directory, file), JSON.stringify(data, null, 2));
15
+ const files = directory => Object.fromEntries(fs.readdirSync(directory, { recursive: true }).filter(file => fs.statSync(path.join(directory, file)).isFile()).sort().map(file => [file, crypto.createHash('sha256').update(fs.readFileSync(path.join(directory, file))).digest('hex')]));
16
+ function fixture(t, { prepare = true } = {}) {
17
+ const project = fs.mkdtempSync(path.join(os.tmpdir(), 'i3-full-content-'));
18
+ t.after(() => fs.rmSync(project, { recursive: true, force: true }));
19
+ const template = prepare ? contract.prepareTemplate({ project }) : null;
20
+ const scope = { schemaVersion: 1, title: '资产审核', pages: [{ id: 'assets', title: '资产台账', referencePage: 'models-page',
21
+ regions: [{ id: 'list', mode: 'management-list' }, { id: 'editor', mode: 'edit-form' }] }],
22
+ requirements: [{ id: 'ASSET-01', text: '查询、选择和修改资产', pages: ['assets'], source: { type: 'prd', reference: 'PRD.md#ASSET-01', statement: '按资产字段实现清单与编辑;使用模板通用能力约定。' } }], exemptions: [] };
23
+ write(project, 'scope.json', scope);
24
+ const create = options => native.createPrototype({ project, id: 'assets', expertMode: 'template', scope: path.join(project, 'scope.json'), ...(template ? { templateRoot: template.directory } : {}), ...options });
25
+ return { project, template, scope, create };
26
+ }
27
+ test('explicit legacy preparation uses the complete offline reference and never overwrites a working copy', t => {
28
+ const { project, template } = fixture(t);
29
+ const snapshot = contract.loadTemplate(template.directory);
30
+ assert.equal(snapshot.kind, 'content-reference'); assert.equal(snapshot.id, 'enterprise-admin');
31
+ assert.equal(snapshot.index.pages.length, 35); assert.equal(snapshot.index.routes.length, 44); assert.equal(snapshot.index.htmlFragments.length, 93);
32
+ assert.deepEqual(Object.keys(snapshot.index.extractor.files).sort(), ['content-template.cjs', 'template-contract.cjs']);
33
+ const file = path.join(template.directory, 'index.html');
34
+ fs.writeFileSync(file, fs.readFileSync(file, 'utf8').replace('<h1>模型管理</h1>', '<h1>资产参考内容</h1>'));
35
+ assert.throws(() => contract.loadTemplate(template.directory), /stale|不同步/);
36
+ const index = contract.syncTemplate(template.directory);
37
+ assert.equal(index.pages.find(page => page.id === 'models-page').title, '资产参考内容');
38
+ const content = require(path.join(root, 'scripts/content-template.cjs')).pageContent(contract.loadTemplate(template.directory), 'models-page');
39
+ assert.ok(content.html.includes('<h1>资产参考内容</h1>')); assert.equal(content.readOnly, true);
40
+ assert.equal(contract.prepareTemplate({ project }).created, false);
41
+ assert.ok(fs.readFileSync(file, 'utf8').includes('资产参考内容'));
42
+ });
43
+ test('default creation reads the released template without preparing a business template directory or modifying Skill assets', t => {
44
+ const { project, create } = fixture(t, { prepare: false });
45
+ const builtin = path.join(root, 'assets/templates/enterprise-admin');
46
+ const before = files(builtin);
47
+ const created = create();
48
+ assert.equal(fs.existsSync(path.join(project, 'ui-templates')), false);
49
+ assert.equal(fs.existsSync(path.join(project, '.prototype-preparation')), false);
50
+ assert.deepEqual(files(builtin), before);
51
+ assert.equal(native.inspect(created.directory).status, 'passed');
52
+ const snapshot = read(created.directory, 'inputs/template-snapshot.json');
53
+ assert.equal(snapshot.id, 'enterprise-admin'); assert.equal(snapshot.index.pages.length, 35);
54
+ assert.deepEqual(snapshot, contract.loadBundledTemplate());
55
+ assert.deepEqual(read(created.directory, 'inputs/scope.json'), read(project, 'scope.json'));
56
+ });
57
+ test('an existing edited template copy is preserved and used only when explicitly selected', t => {
58
+ const { project, template, create } = fixture(t);
59
+ const entry = path.join(template.directory, 'index.html');
60
+ fs.writeFileSync(entry, fs.readFileSync(entry, 'utf8').replace('<h1>模型管理</h1>', '<h1>用户维护的旧模板</h1>'));
61
+ contract.syncTemplate(template.directory);
62
+ const before = files(template.directory);
63
+ const current = create({ id: 'current', templateRoot: undefined });
64
+ const legacy = create({ id: 'legacy' });
65
+ assert.notEqual(read(current.directory, 'inputs/template-snapshot.json').digest, read(legacy.directory, 'inputs/template-snapshot.json').digest);
66
+ assert.equal(read(legacy.directory, 'inputs/template-snapshot.json').index.pages.find(page => page.id === 'models-page').title, '用户维护的旧模板');
67
+ assert.deepEqual(files(template.directory), before);
68
+ });
69
+ test('new business generation requires a sourced scope and does not silently generate a device list', t => {
70
+ const { project, create } = fixture(t);
71
+ assert.throws(() => create({ scope: undefined }), /scope|需求/i);
72
+ assert.equal(fs.existsSync(path.join(project, 'prototypes')), false);
73
+ const created = create(), meta = read(created.directory, 'prototype.json');
74
+ assert.equal(meta.runtime.contentReference, true); assert.equal(meta.business.status, 'needs-adaptation');
75
+ assert.equal(native.inspect(created.directory).status, 'passed');
76
+ assert.match(native.inspect(created.directory, { acceptance: true }).errors.join('; '), /implementation pending|scaffold/);
77
+ const config = read(created.directory, 'app/public/prototype-config.json');
78
+ assert.deepEqual(config.pages, [{ id: 'assets', title: '资产台账' }]);
79
+ assert.deepEqual(config.theme, { defaultTheme: 'light', presets: ['light', 'dark', 'fresh', 'tech'] });
80
+ assert.equal(config.content, undefined); assert.equal(read(created.directory, 'mocks/contract.json').count, undefined);
81
+ assert.equal(fs.existsSync(path.join(created.directory, 'app/src/domain/devices.ts')), false);
82
+ const state = native.state(created.directory, {});
83
+ assert.equal(state.nextAction, 'implement-business'); assert.equal(state.commands.browser, undefined); assert.ok(state.commands.recordRun);
84
+ });
85
+ test('the complete content reference yields fixed common capabilities for the selected business regions', t => {
86
+ const { create } = fixture(t), directory = create().directory;
87
+ const fixed = read(directory, 'inputs/capabilities.json'), acceptance = read(directory, 'inputs/acceptance.json');
88
+ for (const id of ['filter', 'selection', 'pagination', 'bulk', 'feedback']) assert.ok(fixed.bindings.some(binding => binding.id === `assets/list/${id}`));
89
+ assert.ok(fixed.bindings.some(binding => binding.id === 'assets/editor/edit'));
90
+ assert.ok(fixed.bindings.some(binding => binding.id === 'app-shell/appearance/theme'));
91
+ assert.ok(acceptance.requiredScenarios.includes('assets/list/pagination/pages'));
92
+ assert.ok(acceptance.requiredScenarios.includes('app-shell/appearance/theme/persistence'));
93
+ assert.equal(acceptance.requiredScenarios.some(id => id.startsWith('devices/')), false);
94
+ fixed.bindings = fixed.bindings.filter(binding => binding.capabilityId !== 'selection'); write(directory, 'inputs/capabilities.json', fixed);
95
+ const candidate = native.manifest(directory);
96
+ assert.throws(() => native.revise(directory, { revision: 'r002', expectedRevision: candidate.revision, expectedDigest: candidate.digest, acceptInputs: true }), /binding|capabilit|合同/i);
97
+ });
98
+ test('business page and requirement references must remain explicit and unambiguous', t => {
99
+ const { project, scope, create } = fixture(t);
100
+ for (const modify of [
101
+ value => { value.pages[0].referencePage = 'missing-page'; },
102
+ value => { value.pages.push(structuredClone(value.pages[0])); },
103
+ value => { value.requirements.push(structuredClone(value.requirements[0])); },
104
+ value => { value.requirements[0].source = {}; },
105
+ value => { value.requirements[0].pages = ['other-business']; },
106
+ value => { value.pages[0].regions[0].mode = 'unknown-mode'; },
107
+ ]) {
108
+ const broken = structuredClone(scope); modify(broken); write(project, 'scope.json', broken);
109
+ assert.throws(() => create()); assert.equal(fs.existsSync(path.join(project, 'prototypes')), false);
110
+ }
111
+ });
112
+ test('offline reference synchronization rejects missing runtime resources and module loading', t => {
113
+ const { template } = fixture(t), file = path.join(template.directory, 'index.html'), html = fs.readFileSync(file, 'utf8');
114
+ fs.writeFileSync(file, html.replace('src="offline.js"', 'src="missing.js"')); assert.throws(() => contract.syncTemplate(template.directory), /resource missing/);
115
+ fs.writeFileSync(file, html.replace('<script src="offline.js"', '<script type="module" src="offline.js"')); assert.throws(() => contract.syncTemplate(template.directory), /modules/);
116
+ fs.writeFileSync(file, html); fs.appendFileSync(path.join(template.directory, 'offline.js'), '\nfetch("parts.html")'); assert.throws(() => contract.syncTemplate(template.directory), /Offline runtime/);
117
+ });
118
+ test('the historical device regression runner refuses a business contract before writing evidence', async t => {
119
+ const { create } = fixture(t), directory = create().directory;
120
+ const before = fs.readdirSync(path.join(directory, 'qa'));
121
+ await assert.rejects(require(path.join(root, 'scripts/native-browser.cjs')).main(directory), /historical device-list/);
122
+ assert.deepEqual(fs.readdirSync(path.join(directory, 'qa')), before);
123
+ });