@givolt/i3-composer-agent 2.3.1 → 2.3.3

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 (173) 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 +16 -7
  8. package/bin/agent-targets.json +4 -2
  9. package/bin/i3-composer-agent.cjs +74 -7
  10. package/bin/install.cjs +16 -1
  11. package/bin/native-layout.cjs +1 -1
  12. package/bin/native-migration.cjs +27 -3
  13. package/bin/tool-selector.cjs +24 -7
  14. package/bin/wizard.cjs +89 -15
  15. package/commands/i3-prototype.toml +5 -7
  16. package/docs/codebuddy-setup.md +91 -3
  17. package/docs/codex-setup.md +6 -0
  18. package/docs/other-hosts.md +1 -1
  19. package/docs/ui-prototype-native/MAIN-SESSION-REPORT.md +66 -0
  20. package/docs/ui-prototype-native/USAGE.md +93 -0
  21. package/docs/workbuddy-setup.md +6 -0
  22. package/evals/cases/i3-frontend-ui-engineering.json +16 -0
  23. package/evals/cases/i3-ui-prototype.json +109 -0
  24. package/evals/cases/i3-using-agent-skills.json +28 -0
  25. package/evals/fixtures/i3-ui-prototype/admin-list/assets/reference.css +3 -0
  26. package/evals/fixtures/i3-ui-prototype/admin-list/assets/reference.js +8 -0
  27. package/evals/fixtures/i3-ui-prototype/admin-list/capability-definition.json +67 -0
  28. package/evals/fixtures/i3-ui-prototype/admin-list/index.html +89 -0
  29. package/evals/fixtures/i3-ui-prototype/admin-list/readme.md +52 -0
  30. package/evals/fixtures/i3-ui-prototype/admin-list/template.json +8 -0
  31. package/evals/zh-CN/cases/i3-frontend-ui-engineering.json +17 -0
  32. package/evals/zh-CN/cases/i3-ui-prototype.json +115 -0
  33. package/evals/zh-CN/cases/i3-using-agent-skills.json +30 -0
  34. package/hooks/codebuddy-upstream.cjs +93 -0
  35. package/hooks/i3-prototype.cjs +12 -0
  36. package/hooks/manage.cjs +162 -0
  37. package/package.json +61 -1
  38. package/plugin.json +1 -1
  39. package/scripts/design-provider-test.js +7 -0
  40. package/scripts/enterprise-template-test.js +27 -1
  41. package/scripts/lib/prototype-fixture.cjs +17 -0
  42. package/scripts/prototype-cache-test.js +55 -0
  43. package/scripts/prototype-generation-entry-test.js +6 -0
  44. package/scripts/prototype-generation-evidence-test.js +10 -0
  45. package/scripts/prototype-generation-native-test.js +78 -0
  46. package/scripts/prototype-generation-test.js +99 -0
  47. package/scripts/prototype-inventory-test.js +43 -0
  48. package/scripts/prototype-locations-test.js +79 -0
  49. package/scripts/prototype-project-test.js +183 -0
  50. package/scripts/ui-prototype-codebuddy-hook-test.js +106 -0
  51. package/scripts/ui-prototype-content-test.js +123 -0
  52. package/scripts/ui-prototype-dependencies-test.js +213 -0
  53. package/scripts/ui-prototype-devtools-test.js +53 -0
  54. package/scripts/ui-prototype-execution-evidence-test.js +62 -0
  55. package/scripts/ui-prototype-execution-gates-test.js +66 -0
  56. package/scripts/ui-prototype-execution-policy-test.js +42 -0
  57. package/scripts/ui-prototype-execution-runtime-test.js +27 -0
  58. package/scripts/ui-prototype-ide-source-test.js +73 -0
  59. package/scripts/ui-prototype-native-test.js +504 -0
  60. package/scripts/ui-prototype-registration-test.js +53 -0
  61. package/scripts/ui-prototype-template-test.js +261 -0
  62. package/scripts/ui-prototype-test.js +10 -0
  63. package/scripts/ui-prototype-transcript-auto-test.js +67 -0
  64. package/skills/i3-frontend-ui-engineering/SKILL.md +1 -1
  65. package/skills/i3-incremental-implementation/SKILL.md +1 -1
  66. package/skills/i3-ui-prototype/SKILL.md +158 -19
  67. package/skills/i3-ui-prototype/assets/capabilities/generation.json +833 -0
  68. package/skills/i3-ui-prototype/assets/capabilities/management.json +294 -0
  69. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/acceptance.json +51 -0
  70. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/component-map.json +13 -0
  71. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/generation-component-map.json +300 -0
  72. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/index.html +11 -0
  73. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/package-lock.json +2298 -0
  74. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/package.json +22 -0
  75. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/public/prototype-config.json +6 -0
  76. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/App.vue +136 -0
  77. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/components/DeviceEditor.vue +111 -0
  78. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/config.ts +19 -0
  79. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/content.ts +28 -0
  80. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/domain/devices.ts +50 -0
  81. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/domain/theme.ts +25 -0
  82. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/main.ts +18 -0
  83. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/style.css +50 -0
  84. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/useDevices.ts +66 -0
  85. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/tests/devices.test.ts +44 -0
  86. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/tests/theme.test.ts +26 -0
  87. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/tsconfig.json +15 -0
  88. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/vite.config.ts +4 -0
  89. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/profile.json +174 -0
  90. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/readme.md +61 -0
  91. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/index.html +11 -0
  92. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/package-lock.json +2298 -0
  93. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/package.json +22 -0
  94. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/public/prototype-config.json +21 -0
  95. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/App.vue +62 -0
  96. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/PrototypeShell.vue +59 -0
  97. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/config.ts +23 -0
  98. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/domain/presentation.ts +29 -0
  99. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/domain/theme.ts +25 -0
  100. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/main.ts +18 -0
  101. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/style.css +26 -0
  102. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/tests/presentation.test.ts +23 -0
  103. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/tests/theme.test.ts +26 -0
  104. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/tsconfig.json +15 -0
  105. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/vite.config.ts +4 -0
  106. package/skills/i3-ui-prototype/assets/hosts/codebuddy/profile.json +13 -0
  107. package/skills/i3-ui-prototype/assets/optional-skills/host-targets.json +739 -0
  108. package/skills/i3-ui-prototype/assets/optional-skills/sources.json +27 -0
  109. package/skills/i3-ui-prototype/assets/templates/catalog.js +149 -0
  110. package/skills/i3-ui-prototype/assets/templates/catalog.json +2 -1
  111. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/capability-definition.json +294 -0
  112. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/component-inventory.json +707 -0
  113. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/generation.json +49 -0
  114. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/index.html +507 -2
  115. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/offline-build.json +265 -0
  116. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/offline.js +8888 -0
  117. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/template-inventory.json +28791 -0
  118. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/template.json +9 -0
  119. package/skills/i3-ui-prototype/assets/templates/gallery/index.html +5 -5
  120. package/skills/i3-ui-prototype/assets/templates/generation-options.js +42 -0
  121. package/skills/i3-ui-prototype/assets/templates/index.html +3 -0
  122. package/skills/i3-ui-prototype/assets/templates/presentation.html +17 -0
  123. package/skills/i3-ui-prototype/references/artifact-contract.md +26 -1
  124. package/skills/i3-ui-prototype/references/content-template.md +81 -0
  125. package/skills/i3-ui-prototype/references/enterprise-admin.md +6 -0
  126. package/skills/i3-ui-prototype/references/entry-contract.md +71 -0
  127. package/skills/i3-ui-prototype/references/execution-evidence.md +66 -0
  128. package/skills/i3-ui-prototype/references/expert-adapter.md +33 -5
  129. package/skills/i3-ui-prototype/references/framework-contract.md +76 -0
  130. package/skills/i3-ui-prototype/references/generation-contract.md +122 -0
  131. package/skills/i3-ui-prototype/references/interaction-acceptance.md +169 -0
  132. package/skills/i3-ui-prototype/references/legacy-native.md +58 -0
  133. package/skills/i3-ui-prototype/references/optional-skills.md +63 -0
  134. package/skills/i3-ui-prototype/references/project-layout.md +92 -0
  135. package/skills/i3-ui-prototype/references/template-adapters.md +7 -1
  136. package/skills/i3-ui-prototype/references/workflow-contract.md +62 -4
  137. package/skills/i3-ui-prototype/scripts/adapters/easel-html.cjs +1 -1
  138. package/skills/i3-ui-prototype/scripts/browser-workspace.cjs +72 -0
  139. package/skills/i3-ui-prototype/scripts/codebuddy-context.cjs +75 -0
  140. package/skills/i3-ui-prototype/scripts/codebuddy-hook.cjs +157 -0
  141. package/skills/i3-ui-prototype/scripts/codebuddy-ide-source.cjs +69 -0
  142. package/skills/i3-ui-prototype/scripts/codebuddy-registration.cjs +123 -0
  143. package/skills/i3-ui-prototype/scripts/content-template.cjs +98 -0
  144. package/skills/i3-ui-prototype/scripts/enterprise-template.cjs +13 -6
  145. package/skills/i3-ui-prototype/scripts/execution-assertions.cjs +43 -0
  146. package/skills/i3-ui-prototype/scripts/execution-evidence.cjs +146 -0
  147. package/skills/i3-ui-prototype/scripts/execution-gates.cjs +147 -0
  148. package/skills/i3-ui-prototype/scripts/execution-policy.cjs +41 -0
  149. package/skills/i3-ui-prototype/scripts/execution-runtime.cjs +60 -0
  150. package/skills/i3-ui-prototype/scripts/generation-assets.cjs +18 -0
  151. package/skills/i3-ui-prototype/scripts/generation-contract.cjs +214 -0
  152. package/skills/i3-ui-prototype/scripts/generation-evidence.cjs +50 -0
  153. package/skills/i3-ui-prototype/scripts/generation-replan.cjs +34 -0
  154. package/skills/i3-ui-prototype/scripts/hosts/codebuddy.cjs +16 -0
  155. package/skills/i3-ui-prototype/scripts/manage-baseline.cjs +1 -1
  156. package/skills/i3-ui-prototype/scripts/native-browser.cjs +785 -0
  157. package/skills/i3-ui-prototype/scripts/native-build.cjs +91 -0
  158. package/skills/i3-ui-prototype/scripts/native-delivery.cjs +103 -0
  159. package/skills/i3-ui-prototype/scripts/native-devtools.cjs +92 -0
  160. package/skills/i3-ui-prototype/scripts/native-files.cjs +35 -0
  161. package/skills/i3-ui-prototype/scripts/native-pilot-lib.cjs +292 -0
  162. package/skills/i3-ui-prototype/scripts/native-pilot.cjs +50 -0
  163. package/skills/i3-ui-prototype/scripts/optional-skill-install.cjs +203 -0
  164. package/skills/i3-ui-prototype/scripts/project-registry.cjs +126 -0
  165. package/skills/i3-ui-prototype/scripts/prototype-dependencies.cjs +81 -0
  166. package/skills/i3-ui-prototype/scripts/prototype-hooks.cjs +87 -0
  167. package/skills/i3-ui-prototype/scripts/prototype-lib.cjs +31 -13
  168. package/skills/i3-ui-prototype/scripts/prototype-locations.cjs +69 -0
  169. package/skills/i3-ui-prototype/scripts/sync-template.cjs +19 -0
  170. package/skills/i3-ui-prototype/scripts/template-contract.cjs +407 -0
  171. package/skills/i3-ui-prototype/scripts/template-inventory.cjs +78 -0
  172. package/skills/i3-ui-prototype/scripts/validate-result.cjs +117 -0
  173. package/skills/i3-using-agent-skills/SKILL.md +2 -5
@@ -0,0 +1,261 @@
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 legacyFixture = require('./lib/prototype-fixture.cjs');
8
+ const installationFile = path.join(__dirname, 'lib/installation.js');
9
+ const skillsDirectory = fs.existsSync(installationFile) ? require(installationFile).loadInstallation().skillsDirectory : path.resolve(__dirname, '../skills');
10
+ const contract = require(path.join(skillsDirectory, 'i3-ui-prototype/scripts/template-contract.cjs'));
11
+
12
+ function fixture(t) {
13
+ const project = fs.mkdtempSync(path.join(os.tmpdir(), 'i3-ui-template-'));
14
+ t.after(() => fs.rmSync(project, { recursive: true, force: true }));
15
+ return legacyFixture.prepareTemplate(contract, { project });
16
+ }
17
+ function edit(directory, change) {
18
+ const file = path.join(directory, 'index.html');
19
+ fs.writeFileSync(file, change(fs.readFileSync(file, 'utf8')));
20
+ }
21
+ function parameters(index) {
22
+ return { definition: index.definition, bindings: index.bindings, regions: index.regions, scope: { requirements: [], exemptions: [] }, decisions: [] };
23
+ }
24
+ test('theme remains optional and its explicit preset binding rejects invalid defaults and identifiers', t => {
25
+ const { directory, index }=fixture(t);
26
+ assert.equal(index.bindings.some(binding=>binding.capabilityId==='theme'),false);
27
+ edit(directory,html=>html.replace(/<!-- OPTIONAL_FOUR_THEMES:[\s\S]*?(<aside[\s\S]*?<\/aside>)\s*-->/,'$1'));
28
+ const themed=contract.syncTemplate(directory);
29
+ assert.equal(themed.bindings.filter(binding=>binding.capabilityId==='theme').length,1,'Uncommenting the shipped opt-in snippet must create exactly one binding');
30
+ assert.equal(contract.projectAdminListContent(contract.loadTemplate(directory)).pageTitle,'设备管理','The opt-in preserves normal HTML content projection');
31
+ const source={type:'user',reference:'theme-request',statement:'明亮、暗黑、清新、科技,默认明亮'};
32
+ const inputs=parameters(themed);inputs.scope.requirements=[{id:'THEME-1',text:source.statement,source,capabilities:[{pageId:'devices',regionId:'device-list',capabilityId:'theme'}]}];
33
+ assert.equal(contract.checkCapabilities(inputs).status,'passed');
34
+ assert.equal(contract.checkCapabilities(inputs).requiredScenarios.filter(s=>s.id.includes('/theme/')).length,3);
35
+ for(const values of [{},{defaultTheme:'dark',presets:['light','dark','fresh','tech']},{defaultTheme:'light',presets:['light','dark','fresh',4]},{defaultTheme:'light',presets:['light','dark','fresh','fresh']}]){
36
+ const broken=structuredClone(inputs);broken.bindings.find(binding=>binding.capabilityId==='theme').parameters=values;
37
+ assert.equal(contract.checkCapabilities(broken).status,'failed',JSON.stringify(values));
38
+ }
39
+ });
40
+ test('an enabled theme cannot exempt its required acceptance while retaining the binding', t => {
41
+ const {directory}=fixture(t);
42
+ edit(directory,html=>html.replace('<nav class="pagination"',`<aside data-capability-id="theme" data-capability-parameters='{"defaultTheme":"light","presets":["light","dark","fresh","tech"]}'>可选主题</aside><nav class="pagination"`));
43
+ const index=contract.syncTemplate(directory),fixed=parameters(index),binding=fixed.bindings.find(item=>item.capabilityId==='theme');
44
+ const scope={requirements:[{id:'THEME-1',text:'四主题',source:{type:'user',reference:'theme-request',statement:'明亮、暗黑、清新、科技'},capabilities:[binding]}],exemptions:[]};
45
+ const inputRoot=path.join(directory,'fixed-inputs');fs.mkdirSync(inputRoot);
46
+ const save=()=>{fs.writeFileSync(path.join(inputRoot,'capabilities.json'),JSON.stringify(fixed));fs.writeFileSync(path.join(inputRoot,'scope.json'),JSON.stringify(scope));};
47
+ save();assert.equal(contract.checkInstanceCapabilities(inputRoot).status,'passed');
48
+ fixed.decisions=[{bindingId:binding.id,status:'not-applicable',requirementId:'THEME-1'}];
49
+ scope.exemptions=[{...binding,requirementId:'THEME-1',reason:'Trying to waive acceptance without removing the enabled theme'}];
50
+ save();const checked=contract.checkInstanceCapabilities(inputRoot);
51
+ assert.equal(checked.status,'failed');assert.match(checked.errors.join(';'),/enabled theme.*acceptance|主题.*验收/i);
52
+ });
53
+
54
+ test('pagination supports an explicit adjustable contract without reinterpreting fixedPageSize', t => {
55
+ const { directory, index } = fixture(t);
56
+ const legacy = parameters(index);
57
+ assert.equal(contract.checkCapabilities(legacy).status, 'passed');
58
+ edit(directory, html => html.replace('"fixedPageSize":10,"minimumTestPages":3', '"mode":"adjustable","defaultPageSize":10,"pageSizeOptions":[10,20,50],"minimumTestPages":3'));
59
+ const synced = contract.syncTemplate(directory);
60
+ const binding = synced.bindings.find(entry => entry.capabilityId === 'pagination');
61
+ assert.deepEqual(binding.parameters, { mode: 'adjustable', defaultPageSize: 10, pageSizeOptions: [10,20,50], minimumTestPages: 3 });
62
+ assert.equal(contract.checkCapabilities(parameters(synced)).status, 'passed');
63
+ assert.equal(contract.checkCapabilities(legacy).status, 'passed', 'an existing fixed snapshot remains valid');
64
+ const oldSnapshot = structuredClone(legacy);
65
+ const oldPagination = oldSnapshot.definition.capabilities.find(item => item.id === 'pagination');
66
+ oldSnapshot.definition.version = '1.0.0';
67
+ oldPagination.parameters = { fixedPageSize: { type: 'integer', required: true, minimum: 1 }, minimumTestPages: { type: 'integer', required: true, minimum: 3 } };
68
+ assert.equal(contract.checkCapabilities(oldSnapshot).status, 'passed', 'the old fixed-only definition remains independently valid');
69
+ });
70
+
71
+ test('pagination rejects ambiguous modes and malformed adjustable defaults or options', t => {
72
+ const { index } = fixture(t);
73
+ const cases = [
74
+ { mode: 'automatic', fixedPageSize: 10 },
75
+ { mode: 10, fixedPageSize: 10 },
76
+ { mode: 'fixed' },
77
+ { mode: 'fixed', fixedPageSize: 10, pageSizeOptions: [10,20,50] },
78
+ { mode: 'fixed', fixedPageSize: 10, defaultPageSize: 10 },
79
+ { mode: 'adjustable', fixedPageSize: 10, defaultPageSize: 10, pageSizeOptions: [10,20,50] },
80
+ { mode: 'adjustable', pageSizeOptions: [10,20,50] },
81
+ { mode: 'adjustable', defaultPageSize: 10 },
82
+ { mode: 'adjustable', defaultPageSize: 10, pageSizeOptions: [20,50] },
83
+ { mode: 'adjustable', defaultPageSize: 10, pageSizeOptions: [10,10,50] },
84
+ { mode: 'adjustable', defaultPageSize: 10, pageSizeOptions: [10,'20',50] },
85
+ { mode: 'adjustable', defaultPageSize: 10, pageSizeOptions: [10,0,50] },
86
+ { mode: 'adjustable', defaultPageSize: 10, pageSizeOptions: [10,20.5,50] },
87
+ { mode: 'adjustable', defaultPageSize: 0, pageSizeOptions: [0,20,50] },
88
+ { mode: 'adjustable', defaultPageSize: '10', pageSizeOptions: [10,20,50] },
89
+ { mode: 'adjustable', defaultPageSize: 10, pageSizeOptions: [10] },
90
+ ];
91
+ for (const value of cases) {
92
+ const inputs = parameters(structuredClone(index));
93
+ inputs.bindings.find(binding => binding.capabilityId === 'pagination').parameters = { ...value, minimumTestPages: 3 };
94
+ const checked = contract.checkCapabilities(inputs);
95
+ assert.equal(checked.status, 'failed', JSON.stringify(value));
96
+ assert.ok(checked.errors.some(error => /pagination/.test(error)));
97
+ }
98
+ });
99
+
100
+ test('user owns an offline editable copy and preparing again never overwrites it', t => {
101
+ const { directory, index, created } = fixture(t);
102
+ assert.equal(created, true);
103
+ assert.equal(index.templateId, 'admin-list');
104
+ assert.equal(contract.loadTemplate(directory).index.digest, index.digest);
105
+ edit(directory, html => html.replace('设备管理', '客户设备管理'));
106
+ contract.syncTemplate(directory);
107
+ const again = legacyFixture.prepareTemplate(contract, { project: path.resolve(directory, '../..') });
108
+ assert.equal(again.created, false);
109
+ assert.match(fs.readFileSync(path.join(directory, 'index.html'), 'utf8'), /客户设备管理/);
110
+ const html = fs.readFileSync(path.join(directory, 'index.html'), 'utf8');
111
+ assert.doesNotMatch(html, /<script[^>]+type=["']module|(?:src|href)=["'](?:https?:|\/\/)/i);
112
+ assert.doesNotMatch(fs.readFileSync(path.join(directory, 'assets/reference.js'), 'utf8'), /\bfetch\s*\(|\bimport\s/);
113
+ });
114
+
115
+ test('template or asset edits invalidate index, and synchronization is deterministic', t => {
116
+ const { directory, index } = fixture(t);
117
+ edit(directory, html => html.replace('设备管理', '新设备清单'));
118
+ assert.throws(() => contract.loadTemplate(directory), /stale|不同步/);
119
+ const readonly = contract.syncTemplate(directory, { write: false });
120
+ assert.notEqual(readonly.digest, index.digest);
121
+ assert.throws(() => contract.loadTemplate(directory), /stale|不同步/);
122
+ const updated = contract.syncTemplate(directory);
123
+ assert.equal(updated.digest, contract.syncTemplate(directory, { write: false }).digest);
124
+ fs.appendFileSync(path.join(directory, 'assets/reference.css'), '\n/* user style */');
125
+ assert.throws(() => contract.loadTemplate(directory), /stale|不同步/);
126
+ contract.syncTemplate(directory);
127
+ assert.ok(contract.loadTemplate(directory).files.every(file => file.encoding === 'base64'));
128
+ });
129
+
130
+ test('deleted pagination is detected from region mode even when its binding is deleted', t => {
131
+ const { index } = fixture(t);
132
+ const inputs = parameters(index);
133
+ inputs.bindings = inputs.bindings.filter(binding => binding.capabilityId !== 'pagination');
134
+ const result = contract.checkCapabilities(inputs);
135
+ assert.equal(result.status, 'failed');
136
+ assert.ok(result.errors.some(error => error.includes('pagination')));
137
+ });
138
+
139
+ test('required capability cannot be waived by an unsupported N/A or pending decision', t => {
140
+ const { index } = fixture(t);
141
+ const inputs = parameters(index);
142
+ const binding = index.bindings.find(entry => entry.capabilityId === 'pagination');
143
+ inputs.decisions = [{ bindingId: binding.id, status: 'not-applicable', reason: 'Prototype only' }];
144
+ assert.equal(contract.checkCapabilities(inputs).status, 'failed');
145
+ inputs.decisions[0].status = 'pending';
146
+ assert.equal(contract.checkCapabilities(inputs).status, 'failed');
147
+ inputs.decisions[0].status = 'not-applicable';
148
+ inputs.scope = {
149
+ requirements: [{ id: 'SCOPE-1', text: 'The user explicitly excluded pagination for this area', source: { type: 'scope-change', reference: 'user-message-12', statement: 'This area contains exactly one fixed record; omit paging.' } }],
150
+ exemptions: [{ pageId: binding.pageId, regionId: binding.regionId, capabilityId: 'pagination', requirementId: 'SCOPE-1', reason: 'Explicit scope change' }],
151
+ };
152
+ inputs.decisions[0].requirementId = 'SCOPE-1';
153
+ const result = contract.checkCapabilities(inputs);
154
+ assert.equal(result.status, 'passed', result.errors.join('\n'));
155
+ assert.ok(!result.requiredBindings.includes(binding.id));
156
+ });
157
+
158
+ test('each region needs its own capabilities and scenarios', t => {
159
+ const { index } = fixture(t);
160
+ const inputs = parameters(index);
161
+ const original = inputs.regions.find(region => region.mode === 'management-list');
162
+ inputs.regions.push({ ...original, id: 'second-list' });
163
+ const result = contract.checkCapabilities(inputs);
164
+ assert.equal(result.status, 'failed');
165
+ assert.ok(result.errors.some(error => error.includes('second-list/pagination')));
166
+ });
167
+
168
+ test('duplicate identity, unknown capability and unbound controls block sync without changing old index', t => {
169
+ const { directory } = fixture(t);
170
+ const initial = fs.readFileSync(path.join(directory, 'index.html'), 'utf8');
171
+ const indexBefore = fs.readFileSync(path.join(directory, 'capabilities.generated.json'), 'utf8');
172
+ for (const invalid of [
173
+ initial.replace('</main>', '<section data-page-id="devices"></section></main>'),
174
+ initial.replace('data-capability-id="pagination"', 'data-capability-id="unknown-widget"'),
175
+ initial.replace('</main>', '<button>Unmapped action</button></main>'),
176
+ ]) {
177
+ fs.writeFileSync(path.join(directory, 'index.html'), invalid);
178
+ assert.throws(() => contract.syncTemplate(directory), /identity|capability|interaction|身份|能力|交互/);
179
+ assert.equal(fs.readFileSync(path.join(directory, 'capabilities.generated.json'), 'utf8'), indexBefore);
180
+ }
181
+ });
182
+
183
+ test('missing behavior parameters and manually edited index are rejected', t => {
184
+ const { directory } = fixture(t);
185
+ const inputs = parameters(contract.loadTemplate(directory).index);
186
+ inputs.bindings.find(binding => binding.capabilityId === 'selection').parameters = {};
187
+ assert.equal(contract.checkCapabilities(inputs).status, 'failed');
188
+ const file = path.join(directory, 'capabilities.generated.json');
189
+ const edited = JSON.parse(fs.readFileSync(file, 'utf8'));
190
+ edited.bindings = edited.bindings.filter(binding => binding.capabilityId !== 'pagination');
191
+ fs.writeFileSync(file, JSON.stringify(edited));
192
+ assert.throws(() => contract.loadTemplate(directory), /stale|不同步/);
193
+ });
194
+
195
+ test('a frozen input snapshot is checked independently of current built-ins', t => {
196
+ const { directory, index } = fixture(t);
197
+ const inputRoot = path.join(directory, 'fixed-inputs');
198
+ fs.mkdirSync(inputRoot);
199
+ fs.writeFileSync(path.join(inputRoot, 'capabilities.json'), JSON.stringify(parameters(index)));
200
+ fs.writeFileSync(path.join(inputRoot, 'scope.json'), JSON.stringify({ requirements: [], exemptions: [] }));
201
+ assert.equal(contract.checkInstanceCapabilities(inputRoot).status, 'passed');
202
+ const frozen = JSON.parse(fs.readFileSync(path.join(inputRoot, 'capabilities.json'), 'utf8'));
203
+ frozen.bindings = frozen.bindings.filter(binding => binding.capabilityId !== 'pagination');
204
+ fs.writeFileSync(path.join(inputRoot, 'capabilities.json'), JSON.stringify(frozen));
205
+ assert.equal(contract.checkInstanceCapabilities(inputRoot).status, 'failed');
206
+ });
207
+
208
+ test('frozen template regions cannot disappear or change mode in the instance contract', t => {
209
+ const { directory } = fixture(t);
210
+ const snapshot = contract.loadTemplate(directory);
211
+ const inputRoot = path.join(directory, 'fixed-inputs');
212
+ fs.mkdirSync(inputRoot);
213
+ fs.writeFileSync(path.join(inputRoot, 'template-snapshot.json'), JSON.stringify(snapshot));
214
+ const fixed = parameters(snapshot.index);
215
+ fixed.regions = fixed.regions.filter(region => region.mode !== 'management-list');
216
+ fixed.bindings = fixed.bindings.filter(binding => binding.regionId !== 'device-list');
217
+ fs.writeFileSync(path.join(inputRoot, 'capabilities.json'), JSON.stringify(fixed));
218
+ assert.equal(contract.checkInstanceCapabilities(inputRoot).status, 'failed');
219
+ });
220
+
221
+ test('remote resources, module scripts and async loads cannot become offline templates', t => {
222
+ const { directory } = fixture(t);
223
+ const html = fs.readFileSync(path.join(directory, 'index.html'), 'utf8');
224
+ for (const invalid of [html.replace('src="assets/reference.js"', 'src="https://example.test/reference.js"'), html.replace('<script src=', '<script type="module" src=')]) {
225
+ fs.writeFileSync(path.join(directory, 'index.html'), invalid);
226
+ assert.throws(() => contract.syncTemplate(directory), /offline|离线/);
227
+ }
228
+ fs.writeFileSync(path.join(directory, 'index.html'), html);
229
+ fs.appendFileSync(path.join(directory, 'assets/reference.js'), '\nfetch("parts/table.html")');
230
+ assert.throws(() => contract.syncTemplate(directory), /offline|离线/);
231
+ });
232
+
233
+ test('unsafe identifiers and symlinked user template directories never write outside project', t => {
234
+ const project = fs.mkdtempSync(path.join(os.tmpdir(), 'i3-ui-template-path-'));
235
+ const other = fs.mkdtempSync(path.join(os.tmpdir(), 'i3-ui-template-other-'));
236
+ t.after(() => { fs.rmSync(project, { recursive: true, force: true }); fs.rmSync(other, { recursive: true, force: true }); });
237
+ assert.throws(() => legacyFixture.prepareTemplate(contract, { project, id: '../escape' }), /identifier|标识/);
238
+ fs.symlinkSync(other, path.join(project, 'ui-templates'), 'dir');
239
+ assert.throws(() => legacyFixture.prepareTemplate(contract, { project }), /symlink|符号链接/);
240
+ assert.deepEqual(fs.readdirSync(other), []);
241
+ });
242
+
243
+ test('content extraction reads visible headings, labels and button text with stable identities', t => {
244
+ const { directory }=fixture(t);
245
+ edit(directory,html=>html
246
+ .replace('<h1>设备管理</h1>','<h1>资产 &amp; 设备</h1>')
247
+ .replace('data-field-id="device-name">设备名称','data-field-id="device-name">设备显示名称')
248
+ .replace('设备名称 <span aria-label="必填">*','资产名称 <span aria-label="必填">*')
249
+ .replace('data-action-id="query" type="button">查询','data-action-id="query" type="button"><strong>检索设备</strong>'));
250
+ const index=contract.syncTemplate(directory);
251
+ assert.equal(index.pages[0].title,'资产 & 设备');
252
+ assert.equal(index.fields.find(item=>item.localId==='device-name').label,'设备显示名称');
253
+ assert.equal(index.fields.find(item=>item.localId==='edit-name').label,'资产名称');
254
+ assert.equal(index.fields.find(item=>item.localId==='query-status').label,'设备状态');
255
+ assert.equal(index.actions.find(item=>item.localId==='query').label,'检索设备');
256
+ const projected=contract.projectAdminListContent(contract.loadTemplate(directory));
257
+ assert.equal(projected.pageTitle,'资产 & 设备');
258
+ assert.equal(projected.fields.name.label,'设备显示名称');
259
+ assert.equal(projected.fields.editName.label,'资产名称');
260
+ assert.equal(projected.actions.query,'检索设备');
261
+ });
@@ -112,3 +112,13 @@ test('unsafe identifiers and symlinked output parents are rejected before writes
112
112
  assert.throws(() => lib.initPrototype({ project, id: 'safe', expertMode: 'template' }), /symlink|符号链接/);
113
113
  assert.deepEqual(fs.readdirSync(elsewhere), []);
114
114
  });
115
+ test('a real review can reject every recommendation without inventing adoption at freeze', t => {
116
+ const dir=fixture(t); const metadata=json(dir,'prototype.json');
117
+ metadata.expertMode='professional'; metadata.expert={status:'reviewed'}; write(dir,'prototype.json',metadata);
118
+ write(dir,'qa/design-advice.json',{schemaVersion:1,mode:'professional',provider:{digest:'a'.repeat(64)},records:[{decision:'rejected',query:'targeted UX review',resultIdentity:'unit-fixture-result',rationale:'Fixture is intentionally incompatible with locked design',verification:'Reviewed against fixture contract'}]});
119
+ browserEvidence(dir);
120
+ const current=lib.contentManifest(dir);
121
+ assert.equal(lib.checkPrototype(dir,{acceptance:true}).status,'passed');
122
+ lib.freezeBaseline(dir,{revision:current.revision,digest:current.digest,reviewer:'Fixture reviewer',statement:'Isolated guard test only',source:'unit-test-fixture'});
123
+ assert.equal(json(dir,'prototype.json').expert.status,'reviewed');
124
+ });
@@ -0,0 +1,67 @@
1
+ 'use strict';
2
+ // Synthetic files and in-memory configuration only; never installs or launches a host.
3
+ const fs=require('node:fs'),path=require('node:path'),os=require('node:os'),test=require('node:test'),assert=require('node:assert/strict');
4
+ const prototypeScripts=(()=>{const file=path.join(__dirname,'lib/installation.js');return path.join(fs.existsSync(file)?require(file).loadInstallation().skillsDirectory:path.resolve(__dirname,'../skills'),'i3-ui-prototype/scripts');})();
5
+ const E=require(prototypeScripts+'/execution-evidence.cjs'),C=require(prototypeScripts+'/codebuddy-context.cjs'),R=require(prototypeScripts+'/codebuddy-registration.cjs');
6
+ function fixture(t) {
7
+ const root=fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(),'i3-transcript-contract-'))),project=path.join(root,'business'),native=path.join(root,'native');
8
+ fs.mkdirSync(project);fs.mkdirSync(native);t.after(()=>fs.rmSync(root,{recursive:true,force:true}));
9
+ const rows=[{type:'user',sessionId:'session-a',message:{content:'生成当前原型'}},{type:'assistant',sessionId:'session-a',message:{content:[{type:'tool_use',id:'front-1',name:'Skill',input:{skill:'i3-frontend-ui-engineering'}}]}},{type:'user',sessionId:'session-a',message:{content:[{type:'tool_result',tool_use_id:'front-1',content:'Native fixture instructions'}]}}];
10
+ const file=path.join(native,'session.txt');fs.writeFileSync(file,rows.map(JSON.stringify).join('\n')+'\n');return {root,project,native,file,rows};
11
+ }
12
+ test('new IDE and CLI registrations default to automatic transcripts in both scopes',()=>{
13
+ for(const product of ['ide','cli'])for(const scope of ['project','global']) {
14
+ const base=scope==='project'?'/work/p/.codebuddy':'/users/u/.codebuddy';
15
+ const change=R.plan({action:'enable',product,scope,base,project:'/work/p',nodePath:'/node',scriptPath:base+'/hooks/i3-prototype.cjs',scriptDigest:'a'.repeat(64)});
16
+ assert.equal(change.recordAfter.transcriptMode,'auto');assert.deepEqual(change.recordAfter.transcriptRoots,[]);
17
+ }
18
+ });
19
+ test('manual and legacy root restrictions are retained; empty manual policy is rejected',()=>{
20
+ const options={action:'enable',product:'ide',scope:'project',base:'/work/p/.codebuddy',project:'/work/p',nodePath:'/node',scriptPath:'/fixed/hook.cjs',scriptDigest:'a'.repeat(64),transcriptRoots:['/native/logs']};
21
+ const first=R.plan(options);assert.equal(first.recordAfter.transcriptMode,'manual');
22
+ const legacy=structuredClone(first.recordAfter);delete legacy.transcriptMode;
23
+ const kept=R.plan({...options,action:'keep',transcriptRoots:undefined,currentText:first.afterText,record:legacy});
24
+ assert.equal(kept.recordAfter.transcriptMode,'manual');assert.deepEqual(kept.recordAfter.transcriptRoots,['/native/logs']);
25
+ assert.throws(()=>R.plan({...options,transcriptMode:'manual',transcriptRoots:[]}),/manual|roots/i);
26
+ });
27
+ test('automatic source uses the exact callback file regardless of filename suffix',t=>{
28
+ const f=fixture(t),payload={transcript_path:f.file,session_id:'session-a'};
29
+ const source=C.source(payload,{transcriptMode:'auto',transcriptRoots:[]},f.project);
30
+ assert.equal(source.path,f.file);assert.equal(E.loaded(source.calls,'frontend').length,1);assert.equal(source.tamperProof,false);
31
+ assert.equal(source.sourceVerification,'native-hook-transcript');
32
+ });
33
+ test('automatic discovery never admits business files, leaf symlinks or missing files',t=>{
34
+ const f=fixture(t),record={transcriptMode:'auto',transcriptRoots:[]},load=file=>C.source({transcript_path:file,session_id:'session-a'},record,f.project);
35
+ const fake=path.join(f.project,'qa.jsonl');fs.copyFileSync(f.file,fake);assert.throws(()=>load(fake),/business project/i);
36
+ const alias=path.join(f.native,'alias.jsonl');fs.symlinkSync(f.file,alias);assert.throws(()=>load(alias),/symlink|native transcript/i);
37
+ assert.throws(()=>load(path.join(f.native,'missing.jsonl')),/not.*available|ENOENT/i);
38
+ assert.throws(()=>load('relative.jsonl'),/absolute|native transcript/i);
39
+ });
40
+ test('automatic source still rejects malformed streams, forged declarations and mismatched sessions',t=>{
41
+ const f=fixture(t),record={transcriptMode:'auto',transcriptRoots:[]},payload={transcript_path:f.file,session_id:'different'};
42
+ assert.throws(()=>C.source(payload,record,f.project),/session/i);
43
+ payload.session_id='session-a';
44
+ for(const content of ['plain transcript text','{"skill":"frontend","passed":true}\n','{"type":"assistant","message":{"content":[]}}\n']) {
45
+ fs.writeFileSync(f.file,content);assert.throws(()=>C.source(payload,record,f.project),/JSONL|native|Native|user task/);
46
+ }
47
+ });
48
+ test('manual roots remain enforced even when a valid callback points elsewhere',t=>{
49
+ const f=fixture(t),record={transcriptMode:'manual',transcriptRoots:[path.join(f.root,'other')]};
50
+ assert.throws(()=>C.source({transcript_path:f.file,session_id:'session-a'},record,f.project),/outside.*roots/);
51
+ record.transcriptRoots=[f.native];assert.equal(C.source({transcript_path:f.file,session_id:'session-a'},record,f.project).path,f.file);
52
+ });
53
+ test('automatic reading refuses oversized native files without parsing their contents',t=>{
54
+ const f=fixture(t);fs.truncateSync(f.file,64*1024*1024+1);
55
+ assert.throws(()=>C.source({transcript_path:f.file,session_id:'session-a'},{transcriptMode:'auto',transcriptRoots:[]},f.project),/oversized|native transcript/i);
56
+ });
57
+ test('unreadable native transcripts cannot become valid evidence',t=>{
58
+ const f=fixture(t);fs.chmodSync(f.file,0);
59
+ try{assert.throws(()=>C.source({transcript_path:f.file,session_id:'session-a'},{transcriptMode:'auto',transcriptRoots:[]},f.project),/EACCES|EPERM/);}
60
+ finally{fs.chmodSync(f.file,0o600);}
61
+ });
62
+ test('missing callback identity and contradictory policy cannot fall back to another log',t=>{
63
+ const f=fixture(t),record={transcriptMode:'auto',transcriptRoots:[]};
64
+ assert.throws(()=>C.source({session_id:'session-a'},record,f.project),/fields required/);
65
+ assert.throws(()=>C.source({transcript_path:f.file},record,f.project),/fields required/);
66
+ assert.throws(()=>C.source({transcript_path:f.file,session_id:'session-a'},{...record,transcriptRoots:[f.native]},f.project),/manual roots/);
67
+ });
@@ -31,7 +31,7 @@ Build production-quality user interfaces that are accessible, performant, and vi
31
31
 
32
32
  ## Applicable Prototype Baselines
33
33
 
34
- When the project has `.i3-ui-prototypes.json`, read the prototype entry applicable to the UI module being changed, then its `baseline.json`, `design-system/MASTER.md`, tokens and page rules. Preserve approved design and interaction decisions; explain conflicts with new requirements instead of silently changing them. New pages reuse the selected template and component conventions. Changes to global tokens, navigation, component behavior or business states go through the `i3-ui-prototype` workflow and corresponding review. HTML review prototypes need explicit component mapping when implemented in another framework. Verify affected production pages in the browser; a prototype manifest check alone does not prove production fidelity. Projects without an applicable baseline retain this skill's existing workflow.
34
+ Read the project prototype index at `prototypes/index.json`, falling back to legacy `.i3-ui-prototypes.json` when no new index exists. Select the entry whose `appliesTo` contains the explicit business module ID; an empty array applies to no module, and conflicting indices or multiple matches require resolution before selecting a baseline. New associations are derived from fixed scope.moduleIds by `i3-ui-prototype`, not inferred from a PRD filename. Then read its `baseline.json`, `design-system/MASTER.md`, tokens and page rules. Preserve approved design and interaction decisions; explain conflicts with new requirements instead of silently changing them. New pages reuse the selected template and component conventions. Changes to global tokens, navigation, component behavior or business states go through the `i3-ui-prototype` workflow and corresponding review. HTML review prototypes need explicit component mapping when implemented in another framework. Verify affected production pages in the browser; a prototype manifest check alone does not prove production fidelity. Projects without an applicable baseline retain this skill's existing workflow.
35
35
 
36
36
  ## Component Architecture
37
37
 
@@ -194,7 +194,7 @@ Each increment should be independently revertable:
194
194
 
195
195
  ## UI Slices with Prototype Baselines
196
196
 
197
- For a UI slice, check `.i3-ui-prototypes.json` for an applicable approved prototype. Read its design contract, business mapping and baseline before implementation, and include affected interaction/visual checks in the slice acceptance. Preserve prior snapshots; coordinate contract changes through `i3-ui-prototype`. Backend-only slices and projects without a baseline continue the existing increment workflow.
197
+ For a UI slice, check `prototypes/index.json` (legacy `.i3-ui-prototypes.json` fallback) for an applicable approved prototype. Match explicit module IDs against appliesTo; an empty array is unbound. Resolve conflicting indices or multiple matches instead of applying a prototype globally. New appliesTo values come from fixed scope.moduleIds through `i3-ui-prototype`. Read its design contract, business mapping and baseline before implementation, and include affected interaction/visual checks in the slice acceptance. Preserve prior snapshots; coordinate contract changes through `i3-ui-prototype`. Backend-only slices and projects without a baseline continue the existing increment workflow.
198
198
 
199
199
  ## Working with Agents
200
200