@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,98 @@
1
+ 'use strict';
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+ const assert = require('node:assert/strict');
5
+ const read = file => JSON.parse(fs.readFileSync(file, 'utf8'));
6
+
7
+ function extract(directory, manifest, files) {
8
+ assert.equal(manifest.entry, 'index.html', 'Content templates use the offline index.html convention');
9
+ const html = fs.readFileSync(path.join(directory, manifest.entry), 'utf8');
10
+ assert.ok(!/<script\b[^>]*type=["']module/i.test(html), 'Offline entry cannot use modules');
11
+ const routesMatch = html.match(/id="prototype-offline-routes">([\s\S]*?)<\/script>/);
12
+ assert.ok(routesMatch, 'Editable route inventory is required');
13
+ const routes = JSON.parse(routesMatch[1]);
14
+ const fragments = new Map([...html.matchAll(/^<template data-source="([^"]+)">\n([\s\S]*?)^<\/template>/gm)].map(match => [match[1], match[2]]));
15
+ assert.ok(fragments.size, 'Editable HTML fragments are required');
16
+ const resources = [];
17
+ for (const match of html.matchAll(/<(script|link)\b[^>]*\b(src|href)="([^"]+)"/gi)) {
18
+ if (match[3].startsWith('data:')) continue;
19
+ assert.ok(!/^(?:[a-z]+:|\/)/i.test(match[3]) && !match[3].split('/').includes('..'), 'Offline resources must remain inside the template');
20
+ const resource = match[3].split(/[?#]/)[0];
21
+ assert.ok(files.some(file => file.path === resource), `Offline resource missing: ${resource}`);
22
+ resources.push({ file: manifest.entry, path: resource });
23
+ if (match[1].toLowerCase() === 'script') {
24
+ const script = fs.readFileSync(path.join(directory, resource), 'utf8');
25
+ assert.ok(!/\bfetch\s*\(|\bXMLHttpRequest\b|\bimport\s*(?:\(|["'{*])|\bfrom\s*["']/.test(script), 'Offline runtime cannot load modules or remote HTML');
26
+ }
27
+ }
28
+ const pages = [...new Set(routes.map(route => route.component).filter(Boolean))].map(component => {
29
+ const fragment = 'reference/' + component.replace(/^\.\//, '').replace(/\.js$/, '.html');
30
+ assert.ok(fragments.has(fragment), `Missing page HTML: ${fragment}`);
31
+ const title = fragments.get(fragment).match(/<h1[^>]*>([^<{]+)<\/h1>/)?.[1]?.trim();
32
+ return { id: path.posix.basename(component, '.js').replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase(),
33
+ title: title || path.posix.basename(component, '.js'), file: 'index.html', fragment,
34
+ routes: routes.filter(route => route.component === component).map(route => route.path) };
35
+ });
36
+ assert.equal(new Set(pages.map(page => page.id)).size, pages.length, 'Duplicate content page identity');
37
+ return { kind: 'content-reference', pages, regions: [], bindings: [], fields: [], actions: [], resources,
38
+ htmlFragments: [...fragments.keys()], routes, requiredBindings: [], requiredScenarios: [] };
39
+ }
40
+
41
+ function scopeInputs(snapshot, scope, contract) {
42
+ assert.ok(Array.isArray(scope?.pages) && scope.pages.length, 'Provide sourced PRD scope.pages; the reference is not the business requirement');
43
+ assert.ok(scope.requirements?.length, 'Provide sourced business requirements');
44
+ const known = new Set(snapshot.index.pages.map(page => page.id));
45
+ const seen = new Set(['app-shell']), regions = [{ pageId: 'app-shell', id: 'appearance', mode: 'application-shell' }], bindings = [];
46
+ const defaults = {
47
+ theme: { defaultTheme: 'light', presets: ['light', 'dark', 'fresh', 'tech'] },
48
+ filter: { resetPage: true, clearSelection: true }, selection: { strategy: 'current-page-clear-on-change' },
49
+ pagination: { mode: 'adjustable', defaultPageSize: 10, pageSizeOptions: [10, 20, 50], minimumTestPages: 3 },
50
+ bulk: { target: 'selected-only' }, edit: { presentation: 'drawer', preserveOnFailure: true },
51
+ };
52
+ for (const page of scope.pages) {
53
+ assert.match(page.id || '', /^[a-z][a-z0-9-]{0,63}$/);
54
+ assert.ok(!seen.has(page.id), `Duplicate business page: ${page.id}`); seen.add(page.id);
55
+ assert.ok(page.title?.trim() && known.has(page.referencePage), `Missing explicit template reference page: ${page.id}`);
56
+ assert.ok(page.regions?.length, `Declare the actual region modes: ${page.id}`);
57
+ for (const region of page.regions) {
58
+ assert.match(region.id || '', /^[a-z][a-z0-9-]{0,63}$/);
59
+ assert.ok(['management-list', 'edit-form', 'content'].includes(region.mode) || (scope.schemaVersion === 3 && require('./generation-contract.cjs').resource(snapshot, 'generation.json').patterns.includes(region.pattern || region.mode)), `Unknown business region mode: ${region.mode}`);
60
+ regions.push({ pageId: page.id, id: region.id, mode: scope.schemaVersion === 3 ? 'content' : region.mode });
61
+ }
62
+ }
63
+ for (const region of regions) for (const capability of snapshot.definition.capabilities) {
64
+ if (capability.requirement !== 'required' || !capability.appliesTo.includes(region.mode)) continue;
65
+ const configured = scope.pages.find(page => page.id === region.pageId)?.regions.find(item => item.id === region.id)?.parameters?.[capability.id];
66
+ const parameters = configured || (capability.id === 'feedback'
67
+ ? { states: region.mode === 'edit-form' ? ['loading', 'error', 'success'] : ['loading', 'empty', 'error', 'success'] }
68
+ : defaults[capability.id]);
69
+ bindings.push({ id: `${region.pageId}/${region.id}/${capability.id}`, pageId: region.pageId, regionId: region.id, capabilityId: capability.id, parameters });
70
+ }
71
+ const requirementIds = new Set();
72
+ for (const requirement of scope.requirements) {
73
+ assert.ok(typeof requirement.id==='string'&&requirement.id.trim()&&!requirementIds.has(requirement.id), `Missing or duplicate requirement identity: ${requirement.id}`);
74
+ requirementIds.add(requirement.id);
75
+ assert.ok(requirement.pages?.length && requirement.pages.every(page => seen.has(page) && page !== 'app-shell'), `Map each business requirement to its pages: ${requirement.id}`);
76
+ }
77
+ for (const page of scope.pages) assert.ok(scope.requirements.some(requirement => requirement.pages?.includes(page.id)), `Business page lacks a requirement: ${page.id}`);
78
+ const fixed = { schemaVersion: 1, definition: snapshot.definition, regions, bindings, decisions: scope.decisions || [] };
79
+ assert.ok(!fixed.decisions.some(decision=>decision.bindingId==='app-shell/appearance/theme'&&decision.status==='not-applicable'), 'The default four-theme contract must retain its acceptance');
80
+ const result = contract.checkCapabilities({ ...fixed, scope });
81
+ assert.equal(result.status, 'passed', result.errors.join('; '));
82
+ return { fixed, result };
83
+ }
84
+ function checkInputs(inputsRoot, contract) {
85
+ const snapshot = read(path.join(inputsRoot, 'template-snapshot.json')), scope = read(path.join(inputsRoot, 'scope.json'));
86
+ const expected = scopeInputs(snapshot, scope, contract), fixed = read(path.join(inputsRoot, 'capabilities.json'));
87
+ assert.deepEqual(fixed, expected.fixed, 'Business capability bindings differ from the fixed sourced page/region contract');
88
+ return expected.result;
89
+ }
90
+ function pageContent(snapshot, pageId) {
91
+ assert.equal(snapshot.kind, 'content-reference');
92
+ const page = snapshot.index.pages.find(entry=>entry.id===pageId);
93
+ assert.ok(page, `Unknown reference page: ${pageId}`);
94
+ const html = Buffer.from(snapshot.files.find(file=>file.path==='index.html').content, 'base64').toString('utf8');
95
+ const fragments = new Map([...html.matchAll(/^<template data-source="([^"]+)">\n([\s\S]*?)^<\/template>/gm)].map(match=>[match[1],match[2]]));
96
+ return { ...page, html: fragments.get(page.fragment), templateDigest: snapshot.digest, readOnly: true };
97
+ }
98
+ module.exports = { extract, scopeInputs, checkInputs, pageContent };
@@ -74,12 +74,19 @@ function initialize(directory, { id, title, purpose, templateRoot = assetRoot })
74
74
  adaptations.push({ file, reason: reasons.join(';'), source: origin.files[file], generated: hash(next) });
75
75
  }
76
76
  }
77
- const index = path.join(app, 'index.html'), original = fs.readFileSync(index, 'utf8');
78
- assert.ok(original.includes('src="app.js"'), 'Review changed EASEL boot entry before adapting');
79
- const adapted = brandRuntimeResource('index.html', original).replace('</head>', ' <link rel="stylesheet" href="tokens.css">\n <link rel="stylesheet" href="usability.css">\n</head>')
80
- .replace('src="app.js"', 'src="enterprise-bootstrap.mjs"');
77
+ const index = path.join(app, 'index.html');
78
+ const portable = fs.readFileSync(path.join(templateRoot, 'index.html'), 'utf8');
79
+ assert.ok(portable.includes('src="offline.js"') && !portable.includes('type="module"'), 'Offline enterprise entry required');
80
+ const adapted = portable.replaceAll('href="reference/', 'href="').replaceAll('data-source="reference/', 'data-source="')
81
+ .replace('</head>', ' <link rel="stylesheet" href="tokens.css">\n</head>')
82
+ .replace(/(<script type="application\/json" id="prototype-offline-config">)[\s\S]*?(<\/script>)/,
83
+ (_, start, end) => start + JSON.stringify({ title }).replaceAll('<', '\\u003c') + end);
81
84
  fs.writeFileSync(index, adapted);
82
- adaptations.push({ file: 'index.html', reason: '默认品牌统一为 i3-Composer;接入原有设计 token、实例配置和独立滚动适配,完整保留页面内容', source: origin.files['index.html'], generated: hash(adapted) });
85
+ const offline = fs.readFileSync(path.join(templateRoot, 'offline.js'), 'utf8')
86
+ .replaceAll('new URL("reference/', 'new URL("').replaceAll('i3-prototype-antdv-', storagePrefix);
87
+ fs.writeFileSync(path.join(app, 'offline.js'), offline);
88
+ adaptations.push({ file: 'index.html', reason: 'i3-Composer 完整可编辑 HTML 片段、路由和实例配置内嵌;经典脚本支持 file://;保留全部内容与本地样式', source: origin.files['index.html'], generated: hash(adapted) });
89
+ adaptations.push({ file: 'offline.js', reason: '来源模块的离线运行适配,实例存储前缀隔离;原始模块仍保留', generated: hash(offline) });
83
90
  fs.copyFileSync(path.join(templateRoot, 'usability.css'), path.join(app, 'usability.css'));
84
91
  fs.writeFileSync(path.join(app, 'enterprise-bootstrap.mjs'), "const response = await fetch(new URL('./config.json', import.meta.url));\nif (!response.ok) throw new Error('无法读取企业后台实例配置');\nconst config = await response.json();\ndocument.title = config.title;\nawait import('./app.js');\n");
85
92
  fs.copyFileSync(path.join(templateRoot, 'serve.cjs'), path.join(app, 'serve.cjs'));
@@ -92,6 +99,6 @@ function initialize(directory, { id, title, purpose, templateRoot = assetRoot })
92
99
  fs.mkdirSync(path.join(directory, 'mocks'), { recursive: true });
93
100
  fs.writeFileSync(path.join(directory, 'mocks/README.md'), '# 演示数据\n\n完整 i3-Composer 示例的数据与规则保留在 app/src/ 各模块中,并使用实例独立的本地存储。它们用于模板审查;新业务需要按需求替换,不能当作真实服务或已完成业务适配。\n');
94
101
  fs.writeFileSync(path.join(directory, 'requirements-map.json'), JSON.stringify(mapping, null, 2) + '\n');
95
- return { kind: 'easel-html', entry: 'app/index.html', sourceVersion: origin.sourceVersion, instanceId, storagePrefix };
102
+ return { kind: 'easel-html', entry: 'app/index.html', sourceVersion: origin.sourceVersion, instanceId, storagePrefix, offlineVersion: 1 };
96
103
  }
97
104
  module.exports = { verifyReference, initialize, brandRuntimeResource };
@@ -0,0 +1,43 @@
1
+ 'use strict';
2
+ const assert=require('node:assert/strict'),E=require('./execution-evidence.cjs');
3
+ const METHODS=['evaluate_script','take_snapshot','list_console_messages','list_network_requests','performance_stop_trace'];
4
+ function validate(value,scenarioIds) {
5
+ assert.equal(value?.schemaVersion,1,'Fixed native assertions schema required');assert.ok(Array.isArray(value.scenarios),'Fixed native assertion scenarios required');
6
+ assert.deepEqual(value.scenarios.map(s=>s.scenarioId).sort(),[...scenarioIds].sort(),'Fixed native assertion scenario coverage differs');
7
+ for(const scenario of value.scenarios) {
8
+ assert.ok(scenario.checks?.length,'Each fixed scenario needs actual result assertions before implementation');
9
+ for(const check of scenario.checks) {
10
+ assert.ok(METHODS.includes(check.method),'Unsupported native assertion method');
11
+ assert.ok(typeof check.pointer==='string'&&/^\/(?:[^~]|~[01])*$/.test(check.pointer)&&!/(?:__proto__|constructor|prototype)/.test(check.pointer),'Safe JSON pointer required');
12
+ assert.ok(['equals','includes','gte','lte'].includes(check.operator),'Unsupported native assertion operator');
13
+ assert.ok(Object.hasOwn(check,'expected')&&check.expected!==null,'Explicit fixed expected fact required');
14
+ if(['gte','lte'].includes(check.operator))assert.equal(typeof check.expected,'number','Numeric fixed expectation required');
15
+ }
16
+ }
17
+ return value;
18
+ }
19
+ function resultData(result) {
20
+ const text=E.contentText(result).trim();let value;
21
+ try{value=JSON.parse(text);}catch{const blocks=[...text.matchAll(/```(?:json)?\s*\n([\s\S]*?)\n```/g)];assert.equal(blocks.length,1,'One actual JSON result or JSON code block required');value=JSON.parse(blocks[0][1]);}
22
+ assert.ok(value&&typeof value==='object','Native JSON fact result required');return value;
23
+ }
24
+ function extract(value,pointer) {
25
+ for(const part of pointer.slice(1).split('/').map(p=>p.replaceAll('~1','/').replaceAll('~0','~'))) {assert.ok(value&&Object.hasOwn(value,part),'Native expected fact is absent');value=value[part];}return value;
26
+ }
27
+ function compare(actual,check) {
28
+ if(check.operator==='equals')assert.deepEqual(actual,check.expected,'Native fact differs from fixed expectation');
29
+ else if(check.operator==='includes')assert.ok((typeof actual==='string'||Array.isArray(actual))&&actual.includes(check.expected),'Native fact does not include fixed expectation');
30
+ else {assert.equal(typeof actual,'number','Native numeric fact required');assert.ok(check.operator==='gte'?actual>=check.expected:actual<=check.expected,'Native numeric fact differs from fixed expectation');}
31
+ }
32
+ function template(options) {
33
+ const fs=require('node:fs'),path=require('node:path'),T=require('./template-contract.cjs');
34
+ assert.ok(options.scope,'Sourced scope file required');const scope=JSON.parse(fs.readFileSync(path.resolve(options.scope),'utf8'));
35
+ const snapshot=options.templateRoot?T.loadTemplate(path.resolve(options.templateRoot)):T.loadBundledTemplate(options.template);
36
+ const selection=snapshot.kind==='content-reference'?require('./content-template.cjs').scopeInputs(snapshot,scope,T):null;
37
+ const generation=scope.schemaVersion===3?require('./generation-contract.cjs').compile(snapshot,scope):null;
38
+ const scenarios=[...(selection?.result.requiredScenarios||snapshot.index.requiredScenarios),...(generation?.scenarios||[])];
39
+ const drawer=selection&&(selection.fixed.regions.some(r=>r.mode==='edit-form')||generation?.bindings.some(b=>b.capabilityId==='dialog'&&b.parameters.presentation==='drawer'));
40
+ const framework=selection?['framework-responsive','framework-instance-isolation','template-content-projection',...(drawer?['framework-drawer-motion','framework-mask-blur','framework-focus-scroll']:[])]:['framework-drawer-motion','framework-mask-blur','framework-focus-scroll','framework-states','framework-responsive','framework-instance-isolation','template-content-projection'];
41
+ return {schemaVersion:1,scopeDigest:E.sha(JSON.stringify(scope)),templateDigest:snapshot.digest,scenarios:[...scenarios.map(s=>({scenarioId:typeof s==='string'?s:s.id,expectedSummary:s.expected||null,checks:[]})),...framework.map(scenarioId=>({scenarioId,checks:[]}))]};
42
+ }
43
+ module.exports={validate,resultData,extract,compare,template};
@@ -0,0 +1,146 @@
1
+ 'use strict';
2
+ const assert = require('node:assert/strict');
3
+ const crypto = require('node:crypto');
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+ const P = require('./execution-policy.cjs');
7
+ const sha = value => crypto.createHash('sha256').update(value).digest('hex');
8
+ function contentText(content) {
9
+ if (typeof content === 'string') return content;
10
+ if (content?.type === 'text' && typeof content.text === 'string') return content.text;
11
+ if (Array.isArray(content)) return content.map(item => item?.type === 'text' ? item.text || '' : item?.type === 'image' ? '[native image result]' : '').join('\n');
12
+ return content === undefined ? '' : JSON.stringify(content);
13
+ }
14
+ // CodeBuddy Code 2.169 serializes flat message/function_call rows. Keep the
15
+ // original line cursor and session identity while adapting only observed shapes.
16
+ // Legacy assistant/user + tool_use/tool_result JSONL remains supported.
17
+ function nativeEvent(event) {
18
+ if (event.type === 'message' && ['assistant', 'user'].includes(event.role)) {
19
+ const content = Array.isArray(event.content) ? event.content.map(block =>
20
+ ['input_text', 'output_text'].includes(block?.type) ? { ...block, type: 'text' } : block) : event.content;
21
+ return { ...event, type: event.role, message: { content } };
22
+ }
23
+ if (event.type === 'function_call') {
24
+ let input;
25
+ try { input = typeof event.arguments === 'string' ? JSON.parse(event.arguments) : event.arguments; }
26
+ catch { throw new Error('Invalid native function arguments JSON'); }
27
+ assert.ok(input && typeof input === 'object' && !Array.isArray(input), 'Invalid native function arguments');
28
+ return { ...event, type: 'assistant', message: { content: [{ type: 'tool_use', id: event.callId, name: event.name, input }] } };
29
+ }
30
+ if (event.type === 'function_call_result') {
31
+ return { ...event, type: 'user', message: { content: [{ type: 'tool_result', tool_use_id: event.callId,
32
+ tool_name: event.name, content: event.output, is_error: event.status !== 'completed' }] } };
33
+ }
34
+ return event;
35
+ }
36
+ function successful(block) {
37
+ if (block.is_error || block.isError || block.success === false || block.error) return false;
38
+ const text = contentText(block.content ?? block.result ?? block.tool_response);
39
+ if (!text.trim() || /^\s*(?:Error:|Error\b|错误[::]|Unknown skill\b|Skill[^\n]*not (?:found|available))/i.test(text)) return false;
40
+ try {
41
+ const value = JSON.parse(text);
42
+ if (value && typeof value === 'object') {
43
+ if (value.isError || value.success === false || value.error || value.status === 'failed') return false;
44
+ if (typeof value.exitCode === 'number' && value.exitCode !== 0) return false;
45
+ if (typeof value.stdout === 'string' && /^\s*(?:Error:|Error\b|错误[::])/i.test(value.stdout)) return false;
46
+ }
47
+ } catch { /* Native tool text need not be JSON. */ }
48
+ return true;
49
+ }
50
+ function parse(text) {
51
+ assert.ok(typeof text === 'string' && Buffer.byteLength(text) <= 64 * 1024 * 1024, 'Native transcript is too large');
52
+ const calls = [], requests = new Map(); let nativeMessages = 0, contextLine = 0, promptLine = 0;
53
+ for (const [lineIndex, line] of text.split('\n').entries()) {
54
+ if (!line.trim()) continue;
55
+ let event; try { event = JSON.parse(line); } catch { throw new Error(`Invalid native JSONL at line ${lineIndex + 1}`); }
56
+ event = nativeEvent(event);
57
+ if (event.type === 'system' && ['compact_boundary','compact'].includes(event.subtype)) contextLine = lineIndex + 1;
58
+ const message = event.message;
59
+ if (!message || !['assistant', 'user'].includes(event.type)) continue;
60
+ const content = typeof message.content === 'string' ? [{type:'text',text:message.content}] : message.content;
61
+ if (!Array.isArray(content)) continue;
62
+ nativeMessages++;
63
+ if (event.type === 'user' && !event.isMeta && !event.isSidechain && content.some(block=>block.type==='text') && !content.some(block=>block.type==='tool_result')) promptLine = lineIndex + 1;
64
+ for (const block of content) {
65
+ if (event.type === 'assistant' && block.type === 'tool_use') {
66
+ assert.ok(typeof block.id === 'string' && block.id && typeof block.name === 'string', 'Native request identity missing');
67
+ assert.ok(!requests.has(block.id), 'Duplicate native tool call ID');
68
+ const input = block.input || {}, skillName = ['Skill', 'skill'].includes(block.name) ? input.skill || input.name || input.command : null;
69
+ const normalized = typeof skillName === 'string' ? skillName.replace(/^i3-composer-agent:/, '') : null;
70
+ const call = { toolCallId: block.id, name: block.name, input, skillName: normalized, skill: P.skillKey(normalized),
71
+ sessionId:event.sessionId||event.session_id||null, cwd:event.cwd||null, nativeRequestId:event.nativeRequestId||null, contextLine, promptLine,
72
+ requestLine: lineIndex + 1, requestDigest: sha(JSON.stringify(block)), success: false };
73
+ requests.set(block.id, call); calls.push(call);
74
+ } else if (event.type === 'user' && block.type === 'tool_result') {
75
+ const call = requests.get(block.tool_use_id);
76
+ assert.ok(call && !call.resultLine, 'Unmatched or duplicate native tool result');
77
+ if (block.tool_name !== undefined) assert.equal(block.tool_name, call.name, 'Native tool result name differs');
78
+ if (call.sessionId && (event.sessionId||event.session_id)) assert.equal(call.sessionId,event.sessionId||event.session_id,'Cross-session native result');
79
+ Object.assign(call, { resultLine: lineIndex + 1, resultDigest: sha(JSON.stringify(block)),
80
+ result: block.content, success: successful(block) });
81
+ }
82
+ }
83
+ }
84
+ assert.ok(nativeMessages, 'No native assistant/user transcript messages');
85
+ return calls;
86
+ }
87
+ function loaded(calls, skill) { return calls.filter(call => call.skill === skill && call.success && call.resultLine); }
88
+ function reference(calls, value) {
89
+ const call = calls.find(item => item.toolCallId === value?.toolCallId);
90
+ assert.ok(call?.success && call.resultLine, 'Successful native tool result missing');
91
+ if (value.resultDigest !== undefined) assert.equal(value.resultDigest, call.resultDigest, 'Native result digest differs');
92
+ return call;
93
+ }
94
+ function describe(text) {
95
+ const prompts=[];let contextLine=0,latestAssistant='';
96
+ for(const [index,line] of text.split('\n').entries()) {
97
+ if(!line.trim())continue;const row=nativeEvent(JSON.parse(line));
98
+ if(row.type==='system'&&['compact_boundary','compact'].includes(row.subtype))contextLine=index+1;
99
+ const content=row.message?.content;
100
+ if(row.type==='assistant'&&(typeof content==='string'||Array.isArray(content)&&content.some(b=>b.type==='text')&&!content.some(b=>b.type==='tool_use')))latestAssistant=contentText(content);
101
+ if(row.type==='user'&&!row.isMeta&&!row.isSidechain&&(typeof content==='string'||Array.isArray(content)&&content.some(b=>b.type==='text')&&!content.some(b=>b.type==='tool_result')))
102
+ prompts.push({line:index+1,digest:sha(line),text:contentText(content),sessionId:row.sessionId||row.session_id||null,
103
+ ...(row.nativeRequestId?{nativeRequestId:row.nativeRequestId}:{})});
104
+ }
105
+ return {prompts,contextLine,latestAssistant};
106
+ }
107
+ function sourceData(file) {
108
+ if(path.basename(file)==='index.json') {
109
+ const ide=require('./codebuddy-ide-source.cjs').read(file);
110
+ return {...ide,buffer:Buffer.from(ide.text)};
111
+ }
112
+ const buffer=fs.readFileSync(file);return {buffer,text:buffer.toString('utf8'),format:'native-jsonl'};
113
+ }
114
+ function cursorBytes(file){return sourceData(file).buffer;}
115
+ function ideWorkspace(file,sessionId) {
116
+ assert.ok(typeof file==='string'&&path.isAbsolute(file)&&path.basename(file)==='index.json','Native IDE index path required');
117
+ assert.equal(path.basename(path.dirname(file)),sessionId,'Native IDE session directory differs');
118
+ const data=sourceData(file);assert.ok(data.workspace,'Native IDE workspace context is missing');
119
+ return data.workspace;
120
+ }
121
+ function readSource(file, roots, {mode='manual',project}={}) {
122
+ assert.ok(typeof file==='string'&&path.isAbsolute(file)&&!/[\x00\r\n]/.test(file), 'Absolute native transcript file required');
123
+ assert.ok(['auto','manual'].includes(mode)&&Array.isArray(roots), 'Invalid native transcript policy');
124
+ if(mode==='manual')assert.ok(roots.length, 'Configured host transcript roots required');
125
+ else assert.equal(roots.length,0,'Automatic transcript mode cannot bypass configured manual roots');
126
+ const real = fs.realpathSync(file);
127
+ if(mode==='manual')assert.ok(roots.some(root => { if(!fs.existsSync(root))return false;const relative = path.relative(fs.realpathSync(root), real); return relative && relative!=='..'&&!relative.startsWith('..'+path.sep) && !path.isAbsolute(relative); }), 'Transcript is outside the configured native host log roots');
128
+ if(mode==='auto') {
129
+ assert.ok(typeof project==='string'&&path.isAbsolute(project), 'Native project required for automatic transcript validation');
130
+ const relative=path.relative(fs.realpathSync(project),real);
131
+ assert.ok(relative==='..'||relative.startsWith('..'+path.sep)||path.isAbsolute(relative), 'Native transcript cannot be inside the business project');
132
+ }
133
+ assert.ok(!fs.lstatSync(file).isSymbolicLink() && fs.statSync(real).isFile() && fs.statSync(real).size <= 64 * 1024 * 1024, 'Invalid native transcript file');
134
+ const data=sourceData(real),text=data.text,calls=parse(text);
135
+ if(data.workspace&&project)assert.equal(fs.realpathSync(data.workspace),fs.realpathSync(project),'Native IDE workspace differs from the bound project');
136
+ // IDE rewrites streaming argument objects before persisting their results.
137
+ // Freeze only the completed prefix; a pending request cannot attest success.
138
+ const pending=data.format==='codebuddy-ide-index-v1'?calls.filter(call=>!call.resultLine):[];
139
+ const firstPending=pending.length?Math.min(...pending.map(call=>call.requestLine)):null;
140
+ const bytes=firstPending?Buffer.from(text.split('\n').slice(0,firstPending-1).join('\n')+'\n'):data.buffer;
141
+ return { path: real, digest: sha(bytes), bytes: bytes.length, format:data.format, calls, ...describe(text),
142
+ ...(firstPending?{stableThroughLine:firstPending-1}:{}),
143
+ ...(data.latestAssistant!==undefined?{latestAssistant:data.latestAssistant}:{}),
144
+ sourceVerification: mode==='auto'?'native-hook-transcript':'configured-host-transcript', tamperProof: false };
145
+ }
146
+ module.exports = { sha, contentText, successful, parse, loaded, reference, describe, readSource, cursorBytes, ideWorkspace };
@@ -0,0 +1,147 @@
1
+ 'use strict';
2
+ // A: deterministic predicates shared by command gates, receipts, and Hook feedback.
3
+ // A successful predicate correlates native source and current bytes; it is not a signature.
4
+ const assert=require('node:assert/strict'),path=require('node:path');
5
+ const P=require('./execution-policy.cjs'),E=require('./execution-evidence.cjs');
6
+ const A=require('./execution-assertions.cjs');
7
+ const same=(a,b,label)=>assert.deepEqual(a,b,label);
8
+ const exact=(a,b,label)=>same([...new Set(a)].sort(),[...new Set(b)].sort(),label);
9
+ function uiWrite(call,directory) {
10
+ if(!['Write','Edit','MultiEdit','write_to_file','replace_in_file'].includes(call.name))return false;
11
+ const file=call.input.file_path||call.input.filePath||call.input.path||call.input.TargetFile;
12
+ if(typeof file!=='string')return false;
13
+ const rel=path.relative(directory,path.resolve(call.cwd||directory,file));
14
+ return /^(?:app[\\/](?:src|public)[\\/]|mocks[\\/]|design-system[\\/])/.test(rel);
15
+ }
16
+ function nativeRef(data,ref) {
17
+ const call=E.reference(data.source.calls,ref);
18
+ if(data.source.stableThroughLine!==undefined)assert.ok(call.resultLine<=data.source.stableThroughLine,'Native result is outside the completed evidence prefix');
19
+ same(call.sessionId,data.binding.sessionId,'Native call belongs to another or unknown session');
20
+ assert.ok(call.requestLine>=data.binding.entryLine,'Native call predates this task');return call;
21
+ }
22
+ function skill(data,ref,key,before=Infinity) {
23
+ // Loading may be reused within this session/context, including before task binding.
24
+ const call=E.reference(data.source.calls,ref);
25
+ if(data.source.stableThroughLine!==undefined)assert.ok(call.resultLine<=data.source.stableThroughLine,'Skill result is outside the completed evidence prefix');
26
+ same(call.sessionId,data.binding.sessionId,'Skill session differs or is unknown');same(call.skill,key,`Actual ${key} Skill load required`);
27
+ assert.ok(call.requestLine>(data.source.contextLine||0),'Skill must be reloaded in the current context');
28
+ assert.ok(call.resultLine<before,'Skill must load before implementation or execution');return call;
29
+ }
30
+ function quote(data,ref) {
31
+ const call=nativeRef(data,ref);assert.ok(typeof ref.quote==='string'&&ref.quote.trim(),'Actual native result quote required');
32
+ assert.ok(E.contentText(call.result).includes(ref.quote),'Quoted finding absent from native result');return call;
33
+ }
34
+ function identity(d) {
35
+ P.validate(d.policy);assert.equal(d.binding?.schemaVersion,1,'Hook task binding required');assert.equal(d.proof?.schemaVersion,1,'Execution evidence required');
36
+ for(const key of ['runId','sessionId','projectKey','policyDigest'])same(d.proof[key],d.binding[key],`Execution ${key} differs`);
37
+ same(d.binding.policyDigest,d.policy.digest,'Execution policy differs');same(d.binding.inputDigest,d.current.inputDigest,'Inputs changed; begin a new execution run');
38
+ for(const key of ['inputDigest','candidateDigest','revision'])same(d.proof[key],key==='candidateDigest'?d.current.digest:d.current[key],`Current ${key} differs`);
39
+ assert.ok(d.source.prompts.some(p=>p.line===d.binding.entryLine),'Native user task source missing');
40
+ }
41
+ function frontend(d,complete) {
42
+ const writes=d.source.calls.filter(c=>c.success&&c.requestLine>=d.binding.entryLine&&uiWrite({...c,cwd:c.cwd||(d.source.format==='codebuddy-ide-index-v1'?d.binding.project:undefined)},d.binding.directory));
43
+ skill(d,d.proof.frontend,'frontend',writes.length?Math.min(...writes.map(c=>c.requestLine)):Infinity);
44
+ if(!complete)return;
45
+ assert.ok(writes.length,'Actual implementation write missing; metadata is not implementation');
46
+ const required=Object.keys(d.files||{}).filter(file=>/^(?:app\/src\/|mocks\/)/.test(file));
47
+ for(const binding of d.implementation?.bindings||[])for(const file of binding.files||[])required.push(file.path);
48
+ assert.ok(required.length,'Actual UI source review required');
49
+ exact((d.proof.frontend.files||[]).map(f=>f.path),required,'Frontend review file coverage differs');
50
+ for(const file of d.proof.frontend.files)same(file.sha256,E.sha(d.files[file.path]),'Frontend reviewed source bytes changed');
51
+ exact(d.proof.frontend.requirementIds||[],(d.scope?.requirements||[]).map(r=>r.id),'Frontend requirement review coverage differs');
52
+ }
53
+ function design(d) {
54
+ const queries=d.proof.official?.queries;assert.ok(Array.isArray(queries)&&queries.length,'Business framework queries required');
55
+ if(d.policy.officialCallable)skill(d,d.proof.official.skill,'antdv',Math.min(...queries.map(q=>nativeRef(d,q).requestLine)));
56
+ else {
57
+ assert.equal(d.proof.official.channel,'official-source','Disclose the equivalent official source channel');
58
+ const failed=d.source.calls.find(c=>c.toolCallId===d.proof.official.unavailable?.toolCallId);
59
+ assert.ok(failed?.skill==='antdv'&&!failed.success&&failed.resultLine&&failed.sessionId===d.binding.sessionId&&failed.requestLine>=d.binding.entryLine&&failed.requestLine>(d.source.contextLine||0)&&/not found|unavailable|not available|unknown skill|not in.*list|未找到|不可用/i.test(E.contentText(failed.result)),'Actual unavailable official Skill result required for fallback');
60
+ }
61
+ const version=quote(d,d.proof.official.version);
62
+ assert.ok(/(?:Read|read_file|Bash|WebFetch|mcp__)/.test(version.name),'Version must come from an actual source tool');
63
+ assert.ok(JSON.stringify(version.input).includes('antdv-next'),'Target package version source required');
64
+ assert.ok(d.frameworkVersion&&d.proof.official.version.quote.includes(d.frameworkVersion),'Target framework version was not verified');
65
+ const components=[...new Set((d.implementation?.bindings||[]).flatMap(b=>b.components||[]).filter(c=>!c.startsWith('application:')))];
66
+ for(const component of components)assert.ok(queries.some(q=>q.component===component),`Actual API query missing: ${component}`);
67
+ for(const query of queries) {
68
+ assert.ok(query.component&&(!components.length||components.includes(query.component)),'Unrelated framework query');
69
+ const call=quote(d,query),input=JSON.stringify(call.input);
70
+ assert.ok(!call.skill&&input.includes(query.component),'Probe or Skill load is not a business API query');
71
+ if(!d.policy.officialCallable)assert.ok(/https:\/\/(?:www\.)?(?:antdv-next\.com|github\.com\/antdv-next)|node_modules[\\/]antdv-next/.test(input),'Fallback must use official docs, types, or source');
72
+ }
73
+ if(d.policy.mode==='template') {
74
+ const chosen=d.proof.templateChoice?.quote;
75
+ const explicit=typeof chosen==='string'&&/template\s*(?:mode|模式)|模板模式|基础(?:生成|模式)|(?:选择|使用|采用)\s*template/i.test(chosen);
76
+ const promptChoice=typeof chosen==='string'&&d.source.prompts.some(p=>p.line<=d.binding.entryLine&&p.text.includes(chosen)&&(explicit||p.text.trim()==='template'&&chosen==='template'));
77
+ let answerChoice=false;
78
+ if(d.proof.templateChoice?.toolCallId){const answer=nativeRef(d,d.proof.templateChoice);assert.ok(/^(?:AskUserQuestion|ask_followup_question)$/.test(answer.name),'Actual native user question result required');const selected=A.extract(A.resultData(answer.result),d.proof.templateChoice.answerPointer);answerChoice=['template','模板模式','基础生成','基础模式'].includes(selected);}
79
+ assert.ok(promptChoice||answerChoice,'Explicit native user template choice required');return;
80
+ }
81
+ assert.ok(d.proof.uiux?.queries?.length,'Actual UIUX queries required in professional mode');
82
+ skill(d,d.proof.uiux.skill,'uiux',Math.min(...d.proof.uiux.queries.map(q=>nativeRef(d,q).requestLine)));
83
+ for(const query of d.proof.uiux.queries) {
84
+ const call=quote(d,query);assert.ok(!call.skill&&query.query&&JSON.stringify(call.input).includes(query.query),'UIUX probe is not a task query');
85
+ assert.ok(query.decisions?.length,'UIUX result decisions required');
86
+ for(const decision of query.decisions) {
87
+ assert.ok(['adopted','adjusted','rejected','no-match'].includes(decision.decision)&&decision.rationale&&decision.identity,'UIUX decision identity and rationale required');
88
+ assert.ok(E.contentText(call.result).includes(decision.identity),'UIUX identity absent from actual result');
89
+ if(['adopted','adjusted'].includes(decision.decision))assert.ok(Object.hasOwn(d.files||{},decision.implementation)&&decision.scenarioId,'Adopted UIUX result needs source and verification scenario');
90
+ }
91
+ }
92
+ assert.ok(d.designReview?.records?.length,'Current design review required');
93
+ for(const record of d.designReview.records)assert.ok(d.proof.uiux.queries.some(q=>q.query===record.query&&q.decisions.some(r=>r.identity===record.resultIdentity&&r.decision===record.decision&&r.rationale===record.rationale)),'Design review differs from actual UIUX query decisions');
94
+ }
95
+ function build(d) {
96
+ assert.ok(d.build?.buildId&&d.build?.buildDigest,'Current checked build required');
97
+ const call=nativeRef(d,d.proof.build),input=JSON.stringify(call.input),output=E.contentText(call.result);
98
+ assert.ok(['Bash','execute_command','run_terminal_command'].includes(call.name)&&input.includes('native-pilot.cjs')&&/\bbuild\b/.test(input)&&input.includes(d.binding.directory),'Actual current build command required');
99
+ assert.ok(output.includes(d.build.buildId)&&output.includes(d.build.buildDigest),'Native build result differs from current build');
100
+ same(d.build.candidateDigest,d.current.digest,'Build candidate is stale');return call;
101
+ }
102
+ function browser(d) {
103
+ const built=build(d);assert.ok(d.business&&d.devtools,'Actual business and DevTools reports required');
104
+ const loaded=skill(d,d.devtools.skill,'browser');
105
+ const calls=d.devtools.mcp?.calls;assert.ok(calls?.length,'Actual DevTools calls required');
106
+ for(const ref of calls) {
107
+ const call=nativeRef(d,ref);same(call.name,ref.tool,'DevTools native tool name differs');
108
+ assert.ok(/^mcp__.*chrome.*devtools.*__/.test(call.name),'CodeBuddy requires Chrome DevTools MCP');
109
+ assert.ok(call.requestLine>loaded.resultLine&&call.requestLine>built.resultLine,'Browser calls must follow Skill loading and the current build');
110
+ }
111
+ const preview=nativeRef(d,d.proof.browser?.preview);
112
+ assert.ok(/^mcp__.*chrome.*devtools.*__(?:navigate_page|new_page)$/.test(preview.name)&&JSON.stringify(preview.input).includes(d.business.previewBaseURL),'Actual current preview navigation required');
113
+ assert.ok(preview.requestLine>built.resultLine&&preview.requestLine>loaded.resultLine,'Preview must follow the current build and browser Skill');
114
+ const identityCall=quote(d,d.proof.browser?.identity);
115
+ assert.ok(calls.some(c=>c.toolCallId===identityCall.toolCallId)&&d.proof.browser.identity.quote.includes(d.current.digest),'Actual browser candidate identity missing');
116
+ for(const kind of ['dom','screenshot','console','network','accessibility','performance']) {
117
+ const fact=d.proof.browser.observations?.[kind],call=quote(d,fact);
118
+ assert.ok(calls.some(c=>c.toolCallId===call.toolCallId),`${kind} observation is not from reviewed MCP calls`);
119
+ assert.ok(({dom:['evaluate_script'],screenshot:['take_screenshot'],console:['list_console_messages'],network:['list_network_requests'],accessibility:['take_snapshot'],performance:['performance_stop_trace']})[kind].some(method=>call.name.endsWith(`__${method}`)),`${kind} fact uses the wrong native method`);
120
+ }
121
+ for(const journey of d.business.journeys||[]) {
122
+ const facts=(d.proof.browser.facts||[]).filter(f=>f.scenarioId===journey.id);assert.ok(facts.length,`Native scenario facts missing: ${journey.id}`);
123
+ const observation=d.business.generationObservations?.find(o=>o.scenarioId===journey.id);
124
+ for(const fact of facts) {
125
+ const call=quote(d,fact);assert.ok(/^mcp__.*chrome.*devtools.*__/.test(call.name)&&call.requestLine>preview.resultLine,'Scenario facts must follow actual preview navigation');
126
+ if(observation)assert.ok(observation.toolCallIds.includes(call.toolCallId)&&observation.steps.some(s=>s.observed.includes(fact.quote)),'Scenario fact differs from actual observation');
127
+ }
128
+ }
129
+ const fixed=A.validate(d.assertions,d.business.journeys.map(j=>j.id));
130
+ for(const scenario of fixed.scenarios)for(const [checkIndex,check] of scenario.checks.entries()) {
131
+ const fact=d.proof.browser.facts.find(f=>f.scenarioId===scenario.scenarioId&&f.checkIndex===checkIndex);assert.ok(fact,'Fixed scenario assertion has no actual native fact');
132
+ const call=nativeRef(d,fact);assert.ok(call.name.endsWith(`__${check.method}`),'Native fact method differs from fixed check');
133
+ A.compare(A.extract(A.resultData(call.result),check.pointer),check);
134
+ }
135
+ }
136
+ function evaluate(data,stage) {
137
+ try {
138
+ identity(data);const needs=P.requirements(data.policy,stage);
139
+ if(needs.includes('frontend')||needs.includes('implementation'))frontend(data,needs.includes('implementation'));
140
+ if(needs.some(n=>['antdv','official-source','uiux'].includes(n)))design(data);
141
+ if(needs.includes('build'))build(data);
142
+ if(needs.includes('browser'))skill(data,data.proof.browser?.skill||data.devtools?.skill,'browser');
143
+ if(needs.includes('browser-results'))browser(data);
144
+ return {status:'passed',stage,policyDigest:data.policy.digest,runId:data.binding.runId,sessionId:data.binding.sessionId,projectKey:data.binding.projectKey,host:'codebuddy',productForm:data.binding.product||'unreported',productVersion:data.binding.productVersion||'unreported',installationScope:data.binding.scope||'unreported',operation:data.binding.operation||data.policy.operation,mode:data.policy.mode,inputDigest:data.current.inputDigest,candidateDigest:data.current.digest,revision:data.current.revision,source:data.source.path?{path:data.source.path,bytes:data.source.bytes,digest:data.source.digest}:undefined,invocationVerification:'native-source-correlated',tamperProof:false};
145
+ } catch(error){return {status:'failed',stage,errors:[error.message],invocationVerification:'unverified'};}
146
+ }
147
+ module.exports={evaluate,uiWrite};
@@ -0,0 +1,41 @@
1
+ 'use strict';
2
+ const assert = require('node:assert/strict');
3
+ const crypto = require('node:crypto');
4
+ const ID = 'codebuddy-ab-v1';
5
+ const SKILLS = Object.freeze({
6
+ frontend: [['frontend','ui','engineering'].join('-'), 'i3-frontend-ui-engineering'],
7
+ browser: [['browser','testing','with','devtools'].join('-'), 'i3-browser-testing-with-devtools'],
8
+ prototype: [['ui','prototype'].join('-'), 'i3-ui-prototype'],
9
+ antdv: ['antdv'], uiux: ['ui-ux-pro-max'],
10
+ });
11
+ const STAGES = ['design', 'implementation-entry', 'implementation', 'build', 'browser-entry', 'browser', 'delivery'];
12
+ function digest(value) { return crypto.createHash('sha256').update(JSON.stringify(value)).digest('hex'); }
13
+ function create(options = {}) {
14
+ const { host = 'codebuddy', operation = 'create', mode = 'professional', officialCallable = true } = options;
15
+ assert.equal(host, 'codebuddy', 'This execution policy supports CodeBuddy only');
16
+ assert.ok(['analyze', 'create', 'revise', 'inspect', 'confirm-baseline'].includes(operation), 'Invalid execution operation');
17
+ assert.ok(['professional', 'template'].includes(mode), 'Invalid execution mode');
18
+ assert.equal(typeof officialCallable, 'boolean');
19
+ const value = { schemaVersion: 1, id: ID, host, operation, mode, officialCallable,
20
+ readOnly: !['create', 'revise'].includes(operation), skills: SKILLS, stages: STAGES };
21
+ return { ...value, digest: digest(value) };
22
+ }
23
+ function validate(policy) {
24
+ assert.ok(policy && policy.id === ID, 'Unsupported execution policy');
25
+ const expected = create(policy);
26
+ assert.deepEqual(policy, expected, 'Execution policy was changed or is invalid');
27
+ return policy;
28
+ }
29
+ function requirements(policy, stage) {
30
+ validate(policy); assert.ok(STAGES.includes(stage), 'Unknown execution stage');
31
+ if (policy.readOnly) return [];
32
+ const design = [policy.officialCallable ? 'antdv' : 'official-source', ...(policy.mode === 'professional' ? ['uiux'] : [])];
33
+ return ({ design, 'implementation-entry': ['frontend'], implementation: ['frontend', 'implementation'],
34
+ build: ['implementation', 'build'], 'browser-entry': ['browser', 'build'],
35
+ browser: ['browser', 'build', 'browser-results'], delivery: [...design, 'frontend', 'implementation', 'build', 'browser', 'browser-results'] })[stage];
36
+ }
37
+ function skillKey(name) { return Object.keys(SKILLS).find(key => SKILLS[key].includes(name)) || null; }
38
+ function creationOptions(options,installed) {
39
+ return installed?.name==='i3-composer-agent'&&options.host==='codebuddy'?{...options,executionPolicy:options.executionPolicy||ID}:options;
40
+ }
41
+ module.exports = { ID, SKILLS, STAGES, digest, create, validate, requirements, skillKey, creationOptions };
@@ -0,0 +1,60 @@
1
+ 'use strict';
2
+ const F=require('./native-files.cjs'),{fs,path,assert,json,inside}=F;
3
+ const P=require('./execution-policy.cjs'),E=require('./execution-evidence.cjs'),G=require('./execution-gates.cjs'),C=require('./codebuddy-context.cjs');
4
+ const optional=(root,file)=>fs.existsSync(inside(root,file))?json(inside(root,file)):null;
5
+ function policy(directory) {
6
+ const value=optional(directory,'inputs/execution-policy.json'),metaFile=inside(directory,'prototype.json');
7
+ if(!value&&!fs.existsSync(metaFile))return null; // Standalone legacy evidence validators have no artifact metadata.
8
+ const meta=json(metaFile);
9
+ if(!value){assert.ok(!meta.runtime.executionPolicy&&!fs.existsSync(inside(directory,'qa/execution-context.json')),'Fixed execution policy was removed');return null;}
10
+ P.validate(value);assert.equal(meta.runtime.executionPolicy,value.id,'Execution runtime identity differs');assert.equal(meta.expertMode,value.mode,'Execution mode changed');
11
+ const acceptance=json(inside(directory,'inputs/acceptance.json'));require('./execution-assertions.cjs').validate(json(inside(directory,'inputs/execution-assertions.json')),[...acceptance.requiredScenarios,...acceptance.frameworkScenarios]);return value;
12
+ }
13
+ function binding(directory) {
14
+ const pointer=optional(directory,'qa/execution-context.json');assert.ok(pointer?.schemaVersion===1,'No native Hook task binding; run execution-begin inside enabled CodeBuddy');
15
+ const project=fs.realpathSync(pointer.project),selected=C.select({project,product:pointer.product});assert.ok(selected,'Hooks are not enabled or are shadowed');
16
+ assert.equal(selected.base,pointer.base,'Effective Hook scope changed');
17
+ const stateDirectory=C.sessionDirectory(selected,project,pointer.sessionId),indexFile=path.join(stateDirectory,'session.json');
18
+ const index=JSON.parse(C.read(indexFile)||'null'),entry=index?.bindings?.[directory];assert.ok(entry,'Native Hook run is missing');
19
+ assert.equal(entry.runId,pointer.runId,'Native run pointer differs');assert.equal(entry.directory,directory);assert.equal(entry.project,project);
20
+ assert.equal(entry.instanceId,json(inside(directory,'prototype.json')).instanceId,'Prototype instance differs');
21
+ assert.equal(entry.registrationDigest,selected.registrationDigest,'Hook registration changed');assert.equal(entry.settingsDigest,selected.settingsDigest,'Hook settings changed');
22
+ C.prefix(entry.source.path,entry.source.bytes,entry.source.digest);
23
+ const source=C.source({session_id:entry.sessionId,transcript_path:entry.source.path},selected,project);
24
+ const prompt=source.prompts.find(p=>p.line===entry.entryLine);assert.equal(prompt?.digest,entry.promptDigest,'Original user task changed');
25
+ assert.ok(!C.readOnly(source.prompts.at(-1).text),'Latest user request is read-only or stopped');
26
+ return {entry,source,selected};
27
+ }
28
+ function data(directory,lib,options={}) {
29
+ directory=fs.realpathSync(directory);const fixed=policy(directory);if(!fixed)return null;
30
+ const {entry,source,selected}=binding(directory),current=lib.manifest(directory);
31
+ const proof=options.proof||optional(directory,'qa/execution-evidence.json');
32
+ const files={};for(const file of Object.keys(current.files))if(/^(?:app\/src\/|app\/public\/|mocks\/|design-system\/)/.test(file))files[file]=fs.readFileSync(inside(directory,file));
33
+ const pointer=optional(directory,'last-build.json');let build=null;
34
+ if(pointer)try{build=lib.verifyBuild(directory,pointer.buildId);}catch{/* A changed candidate must be allowed to rebuild; build/browser gates reject null. */}
35
+ const latest=optional(directory,'qa/latest.json');
36
+ const business=options.businessReport||(latest?json(inside(directory,latest.report)):proof?.browser?.report?json(inside(directory,proof.browser.report)):null);
37
+ const framework=json(inside(directory,'inputs/framework.json'));
38
+ return {policy:fixed,current,binding:entry,source,proof,assertions:json(inside(directory,'inputs/execution-assertions.json')),designReview:optional(directory,'qa/design-advice.json'),scope:json(inside(directory,'inputs/scope.json')),implementation:optional(directory,'design-system/implementation.json'),files,frameworkVersion:framework.profile?.versions?.['antdv-next']||json(inside(directory,'app/package.json')).dependencies['antdv-next'].replace(/^[~^]/,''),build,business,devtools:options.devtoolsReport||optional(directory,'qa/devtools-review.json'),selected};
39
+ }
40
+ function check(directory,stage,lib,options={}) {
41
+ try {const value=data(directory,lib,options);if(!value)return {status:'not-required',policy:'legacy',invocationVerification:'unverified',tamperProof:false};return G.evaluate(value,stage);}
42
+ catch(error){return {status:'failed',stage,errors:[error.message],invocationVerification:'unverified'};}
43
+ }
44
+ function requireStage(directory,stage,lib,options={}) {
45
+ const result=check(directory,stage,lib,options);assert.ok(['passed','not-required'].includes(result.status),`Execution gate ${stage}: ${result.errors?.join('; ')}`);return result;
46
+ }
47
+ function template(directory,lib) {
48
+ const fixed=policy(directory);assert.ok(fixed,'This existing prototype retains its original execution contract');
49
+ const {entry,source}=binding(fs.realpathSync(directory)),current=lib.manifest(directory);
50
+ const ref=key=>{const call=E.loaded(source.calls,key).filter(c=>c.requestLine>source.contextLine).at(-1);return call?{toolCallId:call.toolCallId,resultDigest:call.resultDigest}:null;};
51
+ return {schemaVersion:1,runId:entry.runId,sessionId:entry.sessionId,projectKey:entry.projectKey,policyDigest:fixed.digest,inputDigest:current.inputDigest,candidateDigest:current.digest,revision:current.revision,
52
+ frontend:{...ref('frontend'),files:[],requirementIds:[]},official:{skill:ref('antdv'),channel:fixed.officialCallable?'native-skill':'official-source',version:null,queries:[]},
53
+ ...(fixed.mode==='professional'?{uiux:{skill:ref('uiux'),queries:[]}}:{templateChoice:{quote:null}}),build:null,browser:{skill:ref('browser'),report:'qa/business-browser-input.json',preview:null,identity:null,observations:{},facts:[]}};
54
+ }
55
+ function begin(directory,lib) {
56
+ directory=fs.realpathSync(directory);const {entry}=binding(directory);
57
+ return {status:'bound',operation:'execution-begin',directory,runId:entry.runId,sessionId:entry.sessionId,policyDigest:entry.policyDigest,evidenceTemplate:template(directory,lib),
58
+ next:'Load applicable Skills through CodeBuddy, implement the current scope, and bind actual native result IDs in qa/execution-evidence.json. Binding is not completion.'};
59
+ }
60
+ module.exports={policy,binding,data,check,requireStage,template,begin};
@@ -0,0 +1,18 @@
1
+ 'use strict';
2
+ // Maintainer-only derived assets. Runtime analysis never invokes write mode.
3
+ const {fs,path,assert,json,write,inside}=require('./native-files.cjs');
4
+ function refresh(skillRoot,options={}){
5
+ const templates=path.join(skillRoot,'assets/templates'),catalog=json(path.join(templates,'catalog.json')),inventory=require('./template-inventory.cjs');const choices=[];const summaries=[];
6
+ for(const entry of catalog.templates.filter(item=>item.availability==='available'&&item.contentRoot)){
7
+ assert.ok(/^[a-z][a-z0-9-]{0,63}$/.test(entry.contentRoot),'Unsafe template content root');const root=inside(templates,entry.contentRoot),meta=json(path.join(root,'generation.json'));
8
+ assert.equal(meta.templateId,entry.id,'Generation template identity differs');
9
+ const current=inventory.scan(root),file=path.join(root,'template-inventory.json');
10
+ if(options.check)inventory.verify(root,json(file));else write(root,'template-inventory.json',current);
11
+ choices.push({id:entry.id,name:entry.name,defaults:meta.defaults,choices:meta.choices,patterns:meta.patterns});
12
+ summaries.push({id:entry.id,files:Object.keys(current.files).length,pages:current.pages.length,components:Object.keys(current.components).length,events:current.eventCount});
13
+ }
14
+ const script=`// Generated from registered template metadata; refresh with sync-generation-assets.\nglobalThis.I3_GENERATION_OPTIONS = ${JSON.stringify(choices,null,2)};\n`,file=path.join(templates,'generation-options.js');
15
+ if(options.check)assert.equal(fs.readFileSync(file,'utf8'),script,'Offline customer choices differ from registered template metadata');else write(templates,'generation-options.js',script);
16
+ return {status:'passed',readOnly:!!options.check,templates:summaries};
17
+ }
18
+ module.exports={refresh};