@givolt/i3-composer-agent 2.3.2 → 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.
- package/.agents/plugins/marketplace.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +5 -1
- package/bin/agent-targets.json +4 -2
- package/bin/i3-composer-agent.cjs +44 -8
- package/bin/install.cjs +2 -1
- package/bin/native-layout.cjs +1 -1
- package/bin/tool-selector.cjs +24 -7
- package/bin/wizard.cjs +89 -15
- package/docs/codebuddy-setup.md +27 -2
- package/docs/codex-setup.md +3 -1
- package/docs/other-hosts.md +1 -1
- package/docs/ui-prototype-native/USAGE.md +4 -2
- package/docs/workbuddy-setup.md +3 -1
- package/hooks/codebuddy-upstream.cjs +93 -0
- package/hooks/i3-prototype.cjs +12 -0
- package/hooks/manage.cjs +162 -0
- package/package.json +31 -1
- package/plugin.json +1 -1
- package/scripts/prototype-cache-test.js +55 -0
- package/scripts/prototype-generation-entry-test.js +6 -0
- package/scripts/prototype-generation-evidence-test.js +10 -0
- package/scripts/prototype-generation-native-test.js +78 -0
- package/scripts/prototype-generation-test.js +99 -0
- package/scripts/prototype-inventory-test.js +43 -0
- package/scripts/prototype-locations-test.js +21 -0
- package/scripts/prototype-project-test.js +183 -0
- package/scripts/ui-prototype-codebuddy-hook-test.js +106 -0
- package/scripts/ui-prototype-execution-evidence-test.js +62 -0
- package/scripts/ui-prototype-execution-gates-test.js +66 -0
- package/scripts/ui-prototype-execution-policy-test.js +42 -0
- package/scripts/ui-prototype-execution-runtime-test.js +27 -0
- package/scripts/ui-prototype-ide-source-test.js +73 -0
- package/scripts/ui-prototype-registration-test.js +53 -0
- package/scripts/ui-prototype-transcript-auto-test.js +67 -0
- package/skills/i3-frontend-ui-engineering/SKILL.md +1 -1
- package/skills/i3-incremental-implementation/SKILL.md +1 -1
- package/skills/i3-ui-prototype/SKILL.md +27 -7
- package/skills/i3-ui-prototype/assets/capabilities/generation.json +833 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/generation-component-map.json +300 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/App.vue +14 -3
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/PrototypeShell.vue +59 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/config.ts +6 -1
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/domain/presentation.ts +29 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/style.css +13 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/tests/presentation.test.ts +23 -0
- package/skills/i3-ui-prototype/assets/hosts/codebuddy/profile.json +13 -0
- package/skills/i3-ui-prototype/assets/optional-skills/host-targets.json +4 -2
- package/skills/i3-ui-prototype/assets/templates/catalog.json +2 -1
- package/skills/i3-ui-prototype/assets/templates/enterprise-admin/component-inventory.json +707 -0
- package/skills/i3-ui-prototype/assets/templates/enterprise-admin/generation.json +49 -0
- package/skills/i3-ui-prototype/assets/templates/enterprise-admin/template-inventory.json +28791 -0
- package/skills/i3-ui-prototype/assets/templates/gallery/index.html +1 -1
- package/skills/i3-ui-prototype/assets/templates/generation-options.js +42 -0
- package/skills/i3-ui-prototype/assets/templates/presentation.html +17 -0
- package/skills/i3-ui-prototype/references/artifact-contract.md +1 -1
- package/skills/i3-ui-prototype/references/content-template.md +9 -4
- package/skills/i3-ui-prototype/references/execution-evidence.md +66 -0
- package/skills/i3-ui-prototype/references/generation-contract.md +122 -0
- package/skills/i3-ui-prototype/references/interaction-acceptance.md +4 -0
- package/skills/i3-ui-prototype/references/optional-skills.md +4 -0
- package/skills/i3-ui-prototype/references/project-layout.md +92 -0
- package/skills/i3-ui-prototype/references/template-adapters.md +6 -0
- package/skills/i3-ui-prototype/references/workflow-contract.md +4 -0
- package/skills/i3-ui-prototype/scripts/browser-workspace.cjs +72 -0
- package/skills/i3-ui-prototype/scripts/codebuddy-context.cjs +75 -0
- package/skills/i3-ui-prototype/scripts/codebuddy-hook.cjs +157 -0
- package/skills/i3-ui-prototype/scripts/codebuddy-ide-source.cjs +69 -0
- package/skills/i3-ui-prototype/scripts/codebuddy-registration.cjs +123 -0
- package/skills/i3-ui-prototype/scripts/content-template.cjs +2 -2
- package/skills/i3-ui-prototype/scripts/execution-assertions.cjs +43 -0
- package/skills/i3-ui-prototype/scripts/execution-evidence.cjs +146 -0
- package/skills/i3-ui-prototype/scripts/execution-gates.cjs +147 -0
- package/skills/i3-ui-prototype/scripts/execution-policy.cjs +41 -0
- package/skills/i3-ui-prototype/scripts/execution-runtime.cjs +60 -0
- package/skills/i3-ui-prototype/scripts/generation-assets.cjs +18 -0
- package/skills/i3-ui-prototype/scripts/generation-contract.cjs +214 -0
- package/skills/i3-ui-prototype/scripts/generation-evidence.cjs +50 -0
- package/skills/i3-ui-prototype/scripts/generation-replan.cjs +34 -0
- package/skills/i3-ui-prototype/scripts/hosts/codebuddy.cjs +16 -0
- package/skills/i3-ui-prototype/scripts/native-delivery.cjs +17 -5
- package/skills/i3-ui-prototype/scripts/native-devtools.cjs +1 -0
- package/skills/i3-ui-prototype/scripts/native-pilot-lib.cjs +66 -18
- package/skills/i3-ui-prototype/scripts/native-pilot.cjs +18 -2
- package/skills/i3-ui-prototype/scripts/project-registry.cjs +126 -0
- package/skills/i3-ui-prototype/scripts/prototype-hooks.cjs +87 -0
- package/skills/i3-ui-prototype/scripts/prototype-lib.cjs +5 -7
- package/skills/i3-ui-prototype/scripts/prototype-locations.cjs +5 -1
- package/skills/i3-ui-prototype/scripts/template-contract.cjs +11 -2
- package/skills/i3-ui-prototype/scripts/template-inventory.cjs +78 -0
|
@@ -4,6 +4,7 @@ const {fs,path,crypto,assert,hash,json,id,inside,walk,write,entries,excluded}=F;
|
|
|
4
4
|
const skillRoot=path.resolve(__dirname,'..');
|
|
5
5
|
const frameworkRoot=path.join(skillRoot,'assets/frameworks/vue-antdv-next');
|
|
6
6
|
const templates=()=>require('./template-contract.cjs');
|
|
7
|
+
const projectRegistry=require('./project-registry.cjs');
|
|
7
8
|
function metadata(directory) { const data=json(inside(directory,'prototype.json')); assert.equal(data.schemaVersion,2,'Not a native v2 prototype'); assert.equal(data.runtime.kind,'vue-antdv-next'); return data; }
|
|
8
9
|
function effectiveReview(data,current) {
|
|
9
10
|
const review=data.review;
|
|
@@ -32,6 +33,9 @@ function defaultScope() {
|
|
|
32
33
|
],exemptions:[],openQuestions:[]};
|
|
33
34
|
}
|
|
34
35
|
function createPrototype(options) {
|
|
36
|
+
const installedFile=path.resolve(__dirname,'../../../.i3-composer-agent.json');
|
|
37
|
+
if(fs.existsSync(installedFile))options=require('./execution-policy.cjs').creationOptions(options,json(installedFile));
|
|
38
|
+
if(options.executionPolicy!==undefined){assert.equal(options.executionPolicy,require('./execution-policy.cjs').ID,'Unsupported execution policy');assert.equal(options.host,'codebuddy','Explicit CodeBuddy host required for A+B');}
|
|
35
39
|
assert.ok(options.framework===undefined||options.framework==='vue-antdv-next','Unsupported framework / 不支持的框架');
|
|
36
40
|
assert.ok(options.title===undefined||(typeof options.title==='string'&&options.title.trim()),'Title must be a nonempty string');
|
|
37
41
|
const project=path.resolve(options.project||'.'); assert.ok(fs.statSync(project).isDirectory(),'Project must exist'); id(options.id);
|
|
@@ -41,24 +45,29 @@ function createPrototype(options) {
|
|
|
41
45
|
provider=require('./prototype-lib.cjs').checkDesignProvider({...options,project});
|
|
42
46
|
if(provider.availability!=='available') return {status:'needs-design-provider',provider,sideEffects:[],choices:['locate-or-repair','explicit-template-mode','analyze']};
|
|
43
47
|
}
|
|
44
|
-
|
|
48
|
+
assert.ok(!(options.templateRoot && options.template), 'Choose a template ID or an explicit template root');
|
|
49
|
+
const template=options.templateRoot?templates().loadTemplate(path.resolve(options.templateRoot)):templates().loadBundledTemplate(options.template);
|
|
45
50
|
const reference=template.kind==='content-reference';
|
|
46
51
|
assert.ok(!reference||options.scope,'A sourced scope file is required for business generation; no default device demo');
|
|
47
52
|
const scope=options.scope?json(path.resolve(options.scope)):defaultScope();
|
|
53
|
+
const appliesTo=projectRegistry.moduleIds(scope);
|
|
48
54
|
const selection=reference?require('./content-template.cjs').scopeInputs(template,scope,templates()):null;
|
|
55
|
+
const generation=scope.schemaVersion===3?require('./generation-contract.cjs').compile(template,scope):null;
|
|
49
56
|
const content=reference?{pageTitle:scope.title||'业务原型'}:templates().projectAdminListContent(template);
|
|
50
57
|
if(options.title!==undefined)content.pageTitle=options.title;
|
|
51
58
|
const profile=json(path.join(frameworkRoot,'profile.json'));
|
|
52
59
|
const target=inside(project,`prototypes/${options.id}`); assert.ok(!fs.existsSync(target),'Prototype exists; revise without reinitializing');
|
|
53
|
-
|
|
60
|
+
return projectRegistry.transaction(project,store=>{
|
|
54
61
|
let stage=null,published=false;
|
|
55
62
|
try {
|
|
56
63
|
fs.mkdirSync(path.dirname(target),{recursive:true}); stage=fs.mkdtempSync(path.join(path.dirname(target),'.native-'));
|
|
57
|
-
const
|
|
64
|
+
const registry=store.registry;
|
|
58
65
|
assert.equal(registry.schemaVersion,1); assert.ok(Array.isArray(registry.prototypes)); assert.ok(!registry.prototypes.some(p=>p.id===options.id),'Duplicate prototype ID');
|
|
59
66
|
const frameworkFiles=entries(frameworkRoot,walk(frameworkRoot,'',excluded));
|
|
60
67
|
const instanceId=crypto.randomUUID();
|
|
61
68
|
const data={schemaVersion:2,id:options.id,title:options.title||content.pageTitle||'设备清单原型',revision:'r001',artifactKind:'framework-prototype',runtime:{kind:'vue-antdv-next',version:'1',...(reference?{contentReference:true}:{})},...(reference?{business:{status:'needs-adaptation',source:'inputs/scope.json'}}:{}),template:{id:template.id,version:template.version,digest:template.digest},framework:{id:'vue-antdv-next',version:profile.version||'1',digest:hash(JSON.stringify(frameworkFiles))},expertMode:mode,expert:{status:provider?'pending':'skipped',provider},verification:{status:'not-run'},review:{status:'pending-review'},instanceId};
|
|
69
|
+
if(generation)data.runtime.generationContract=1;
|
|
70
|
+
if(options.executionPolicy){data.runtime.executionPolicy=options.executionPolicy;write(stage,'inputs/execution-policy.json',require('./execution-policy.cjs').create({host:options.host,mode,officialCallable:!options.officialSourceFallback}));}
|
|
62
71
|
write(stage,'prototype.json',data);
|
|
63
72
|
for(const file of template.files) {
|
|
64
73
|
assert.equal(file.encoding,'base64');
|
|
@@ -66,28 +75,35 @@ function createPrototype(options) {
|
|
|
66
75
|
}
|
|
67
76
|
write(stage,'inputs/template-snapshot.json',template);
|
|
68
77
|
write(stage,'inputs/scope.json',scope);
|
|
78
|
+
if(generation)write(stage,'inputs/generation.json',generation);
|
|
69
79
|
write(stage,'inputs/capabilities.json',selection?selection.fixed:{schemaVersion:1,definition:template.definition,bindings:template.index.bindings,regions:template.index.regions,decisions:[]});
|
|
70
|
-
write(stage,'inputs/framework.json',{profile,componentMap:json(path.join(frameworkRoot,'component-map.json')),acceptance:json(path.join(frameworkRoot,'acceptance.json')),files:frameworkFiles});
|
|
80
|
+
write(stage,'inputs/framework.json',{profile,componentMap:generation?require('./generation-evidence.cjs').framework():json(path.join(frameworkRoot,'component-map.json')),acceptance:json(path.join(frameworkRoot,'acceptance.json')),files:frameworkFiles});
|
|
71
81
|
const checked=templates().checkInstanceCapabilities(path.join(stage,'inputs')); assert.equal(checked.status,'passed',checked.errors?.join('; '));
|
|
72
|
-
const
|
|
73
|
-
|
|
82
|
+
const scenarioDetails=[...checked.requiredScenarios,...(generation?.scenarios||[])],requiredScenarios=scenarioDetails.map(s=>typeof s==='string'?s:s.id);
|
|
83
|
+
const hasDrawer=reference&&(selection.fixed.regions.some(region=>region.mode==='edit-form')||generation?.bindings.some(binding=>binding.capabilityId==='dialog'&&binding.parameters.presentation==='drawer'));
|
|
84
|
+
write(stage,'inputs/acceptance.json',{schemaVersion:1,requiredScenarios,scenarioDetails,frameworkScenarios:reference?['framework-responsive','framework-instance-isolation','template-content-projection',...(hasDrawer?['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'],browserReview:json(path.join(frameworkRoot,'acceptance.json')).browserReview});
|
|
85
|
+
if(options.executionPolicy){const all=json(inside(stage,'inputs/acceptance.json'));assert.ok(options.executionAssertions,`Strict policy requires --execution-assertions with fixed expected JSON facts for: ${[...all.requiredScenarios,...all.frameworkScenarios].join(', ')}`);const fixed=require('./execution-assertions.cjs').validate(json(path.resolve(options.executionAssertions)),[...all.requiredScenarios,...all.frameworkScenarios]);write(stage,'inputs/execution-assertions.json',fixed);}
|
|
74
86
|
const requirements=json(path.join(stage,'inputs/scope.json')).requirements;
|
|
75
|
-
write(stage,'requirements-map.json',{schemaVersion:2,requirements:requirements.map(r=>({...r,pages:reference?r.pages:template.index.pages.map(page=>page.id),scenarios:reference?
|
|
87
|
+
write(stage,'requirements-map.json',{schemaVersion:2,requirements:requirements.map(r=>({...r,pages:reference?r.pages:template.index.pages.map(page=>page.id),scenarios:reference?scenarioDetails.filter(s=>s.bindingId.startsWith('app-shell/')||r.pages.some(page=>s.bindingId.startsWith(page+'/'))).map(s=>s.id):requiredScenarios})),bindings:[...(selection?selection.fixed.bindings:template.index.bindings),...(generation?.bindings||[])]});
|
|
76
88
|
const starter=path.join(frameworkRoot,reference?profile.starter:(profile.legacyStarter||profile.starter));
|
|
77
89
|
fs.cpSync(starter,path.join(stage,'app'),{recursive:true,filter:file=>!excluded(`starter/${path.relative(starter,file).split(path.sep).join('/')}`,path.basename(file))});
|
|
78
90
|
const themeBinding=(selection?selection.fixed.bindings:template.index.bindings).find(binding=>binding.capabilityId==='theme');
|
|
79
|
-
write(stage,'app/public/prototype-config.json',{instanceId,storageNamespace:`i3-native-${options.id}-${instanceId}`,mockVersion:reference?'business-v1':'admin-list-v1',title:data.title,...(reference?{pages:scope.pages.map(page=>({id:page.id,title:page.title}))}:{content}),...(themeBinding?{theme:templates().resolveThemeParameters(themeBinding.parameters)}:{})});
|
|
91
|
+
write(stage,'app/public/prototype-config.json',{instanceId,storageNamespace:`i3-native-${options.id}-${instanceId}`,mockVersion:reference?'business-v1':'admin-list-v1',title:data.title,...(reference?{pages:scope.pages.map(page=>({id:page.id,title:page.title}))}:{content}),...(generation?{presentation:generation.presentation,navigation:generation.navigation,generation:{pages:generation.pages}}:{}),...(themeBinding?{theme:templates().resolveThemeParameters(themeBinding.parameters)}:{})});
|
|
80
92
|
write(stage,'mocks/contract.json',reference?{schemaVersion:1,instanceId,status:'needs-business-data',requirements:scope.requirements.map(r=>r.id)}:{schemaVersion:1,seed:'admin-list-v1',count:21,pageSize:10,selection:'current-page-clear-on-page-or-filter',instanceId});
|
|
81
93
|
write(stage,'design-system/MASTER.md',`# ${data.title}\n\n使用 antDv-next 原生组件与主题。管理列表采用紧凑信息层级,正文与操作可读;错误在字段附近呈现。抽屉保留进入与退出过程,显式开启遮罩模糊。\n\n本地模拟,不代表生产权限。\n`);
|
|
94
|
+
if(generation)write(stage,'design-system/MASTER.md',require('./generation-evidence.cjs').designSummary(generation,`# ${data.title}\n\nPrototypeShell 提供已选原生外壳;按逐区域/字段/操作合同实现业务,补齐真实源码映射和浏览器证据。骨架不是业务完成。本地模拟不代表生产权限。`));
|
|
95
|
+
if(generation)write(stage,'design-system/implementation.json',require('./generation-evidence.cjs').initialize(generation));
|
|
82
96
|
write(stage,'design-system/decisions.json',{schemaVersion:1,records:[],reviewStatus:mode==='template'?'skipped':'pending'});
|
|
83
97
|
write(stage,'qa/design-advice.json',{schemaVersion:2,provider,queries:[],records:[],status:mode==='template'?'skipped':'pending'});
|
|
84
98
|
write(stage,'HANDOFF.md',`# ${data.title}\n\n本次为 ${mode} 模式;来源 ${template.id} ${template.version}。运行形态为真实 Vue+antDv-next。读取 inputs 中固定合同和实际源码,勿从安装目录更新旧合同。\n\n使用 native-pilot.cjs inspect/build/preview。当前未验证,待用户评审;不要把检查通过当用户认可。\n`);
|
|
85
99
|
const candidate=manifest(stage); data.inputs={digest:candidate.inputDigest}; write(stage,'prototype.json',data);
|
|
86
100
|
fs.renameSync(stage,target); published=true;
|
|
87
|
-
|
|
88
|
-
|
|
101
|
+
const digest=manifest(target).digest;
|
|
102
|
+
registry.prototypes.push({id:options.id,directory:`prototypes/${options.id}`,appliesTo,schemaVersion:2}); store.save();
|
|
103
|
+
return {status:'created',directory:target,revision:'r001',digest};
|
|
89
104
|
} catch(error) { if(published) fs.rmSync(target,{recursive:true,force:true}); throw error; }
|
|
90
|
-
finally { if(stage&&fs.existsSync(stage)) fs.rmSync(stage,{recursive:true,force:true});
|
|
105
|
+
finally { if(stage&&fs.existsSync(stage)) fs.rmSync(stage,{recursive:true,force:true}); }
|
|
106
|
+
});
|
|
91
107
|
}
|
|
92
108
|
function inspectDesignReview(directory,current=manifest(directory)) {
|
|
93
109
|
const data=metadata(directory);
|
|
@@ -112,12 +128,16 @@ function inspectWithInputs(directory,options={},acceptedInputDigest) {
|
|
|
112
128
|
assert.equal(acceptedInputDigest??data.inputs.digest,current.inputDigest,'Input contract changed; explicitly revise fixed inputs');
|
|
113
129
|
const capability=templates().checkInstanceCapabilities(path.join(directory,'inputs')); assert.equal(capability.status,'passed',capability.errors?.join('; '));
|
|
114
130
|
const acceptance=json(inside(directory,'inputs/acceptance.json'));
|
|
131
|
+
require('./execution-runtime.cjs').policy(directory);
|
|
115
132
|
require('./native-devtools.cjs').readPolicy(directory);
|
|
116
133
|
for(const scenario of capability.requiredScenarios.map(s=>typeof s==='string'?s:s.id)) assert.ok(acceptance.requiredScenarios.includes(scenario),`Required scenario omitted: ${scenario}`);
|
|
117
134
|
const fixedFramework=json(inside(directory,'inputs/framework.json'));
|
|
118
135
|
if(fixedFramework.acceptance?.scenarios?.some(scenario=>scenario.id==='template-content-projection'&&scenario.required))assert.ok(acceptance.frameworkScenarios.includes('template-content-projection'),'Required template-content-projection scenario omitted from fixed framework acceptance');
|
|
119
136
|
const template=json(inside(directory,'inputs/template-snapshot.json')),mapping=json(inside(directory,'requirements-map.json')),scope=json(inside(directory,'inputs/scope.json'));
|
|
120
|
-
const
|
|
137
|
+
const generation=scope.schemaVersion===3?json(inside(directory,'inputs/generation.json')):null;
|
|
138
|
+
if(generation){assert.equal(data.runtime.generationContract,1,'Generation runtime identity is required');require('./generation-contract.cjs').verify(template,scope,generation);for(const scenario of generation.scenarios)assert.ok(acceptance.requiredScenarios.includes(scenario.id),`Generation scenario omitted: ${scenario.id}`);}
|
|
139
|
+
projectRegistry.moduleIds(scope);
|
|
140
|
+
const pageIds=new Set(data.runtime.contentReference?['app-shell',...scope.pages.map(page=>page.id)]:template.index.pages.map(page=>page.id)),bindingIds=new Set([...json(inside(directory,'inputs/capabilities.json')).bindings,...(generation?.bindings||[])].map(binding=>binding.id));
|
|
121
141
|
for(const requirement of scope.requirements)assert.ok(mapping.requirements.some(item=>item.id===requirement.id),`Requirement omitted from mapping: ${requirement.id}`);
|
|
122
142
|
for(const requirement of mapping.requirements) {
|
|
123
143
|
assert.ok(scope.requirements.some(item=>item.id===requirement.id),`Unknown mapped requirement: ${requirement.id}`);
|
|
@@ -131,14 +151,17 @@ function inspectWithInputs(directory,options={},acceptedInputDigest) {
|
|
|
131
151
|
if(data.runtime.contentReference) {
|
|
132
152
|
assert.deepEqual(config.pages,scope.pages.map(page=>({id:page.id,title:page.title})),'Business configuration differs from fixed page scope');
|
|
133
153
|
assert.ok(['needs-adaptation','adapted'].includes(data.business?.status),'Unknown business implementation status');
|
|
134
|
-
const fixedBindings=json(inside(directory,'inputs/capabilities.json')).bindings;
|
|
154
|
+
const fixedBindings=[...json(inside(directory,'inputs/capabilities.json')).bindings,...(generation?.bindings||[])];
|
|
135
155
|
assert.deepEqual((mapping.bindings||[]).map(binding=>binding.id).sort(),fixedBindings.map(binding=>binding.id).sort(),'Business mapping omits fixed capabilities');
|
|
136
156
|
}
|
|
137
157
|
const themeBinding=json(inside(directory,'inputs/capabilities.json')).bindings.find(binding=>data.runtime.contentReference?binding.capabilityId==='theme':binding.id==='devices/device-list/theme');
|
|
138
158
|
if(themeBinding)assert.deepEqual(config.theme,templates().resolveThemeParameters(themeBinding.parameters),'Theme configuration differs from fixed input binding');
|
|
139
159
|
else assert.equal(config.theme,undefined,'Theme requires an explicit optional capability binding');
|
|
160
|
+
if(generation){assert.deepEqual(config.presentation,generation.presentation,'Customer presentation differs from fixed choices');assert.deepEqual(config.navigation,generation.navigation,'Navigation differs from fixed generation');assert.deepEqual(config.generation,{pages:generation.pages},'Business page patterns differ from fixed generation');}
|
|
140
161
|
const deps=json(inside(directory,'app/package.json')).dependencies; assert.ok(deps['antdv-next'] && deps.vue,'Native framework dependencies missing');
|
|
162
|
+
if(generation)require('./generation-evidence.cjs').validateImplementation(directory,generation,json(inside(directory,'design-system/implementation.json')),{complete:!!options.acceptance,frameworkMap:fixedFramework.componentMap});
|
|
141
163
|
if(options.acceptance) {
|
|
164
|
+
if(generation){assert.equal(generation.pendingDecisions.length,0,'Reference applicability decisions remain unresolved');assert.equal(generation.pendingReferenceElements.length,0,'Reference functional elements remain unaccounted for');}
|
|
142
165
|
if(data.runtime.contentReference)assert.equal(data.business?.status,'adapted','Business implementation pending; a scaffold cannot pass acceptance');
|
|
143
166
|
const adoptedAdvice=inspectDesignReview(directory,current).adoptedAdvice;
|
|
144
167
|
const report=options.report||json(inside(directory,json(inside(directory,'qa/latest.json')).report));
|
|
@@ -149,7 +172,9 @@ function inspectWithInputs(directory,options={},acceptedInputDigest) {
|
|
|
149
172
|
assert.deepEqual(report.consoleErrors,[],'Browser errors remain');
|
|
150
173
|
const required=[...acceptance.requiredScenarios,...acceptance.frameworkScenarios];
|
|
151
174
|
for(const scenario of required) assert.ok(report.journeys.some(j=>j.id===scenario && j.status==='passed'),`Missing browser scenario: ${scenario}`);
|
|
175
|
+
if(generation)require('./generation-evidence.cjs').validateObservations(directory,generation,report);
|
|
152
176
|
for(const record of adoptedAdvice)assert.ok(report.journeys.some(j=>j.id===record.verificationScenario && j.status==='passed'),`Advice verification missing: ${record.resultIdentity}`);
|
|
177
|
+
require('./execution-runtime.cjs').requireStage(directory,'delivery',module.exports,{businessReport:report});
|
|
153
178
|
}
|
|
154
179
|
return {status:'passed',level:options.acceptance?'acceptance':'structure',revision:current.revision,digest:current.digest,verification:data.verification.status,review:effectiveReview(data,current).status};
|
|
155
180
|
} catch(error) { return {status:'failed',errors:[error.message]}; }
|
|
@@ -186,12 +211,35 @@ function revise(directory,options) {
|
|
|
186
211
|
assert.equal(checked.status,'passed',checked.errors?.join('; '));
|
|
187
212
|
} else assert.equal(data.inputs.digest,current.inputDigest,'Input contract changed; revise --accept-inputs must explicitly validate the new contract');
|
|
188
213
|
assert.equal(manifest(directory).digest,current.digest,'Candidate changed while validating fixed inputs');
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
214
|
+
const scope=json(inside(directory,'inputs/scope.json')); projectRegistry.moduleIds(scope);
|
|
215
|
+
return projectRegistry.withInstanceAssociation(directory,scope,()=>{
|
|
216
|
+
if(options.acceptInputs)data.inputs={digest:current.inputDigest};
|
|
217
|
+
data.revision=options.revision; data.verification={status:'not-run'}; data.review={status:'pending-review'};
|
|
218
|
+
write(directory,'prototype.json',data); return manifest(directory);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
function replan(directory,options) { const owner=options.owner||`replan-${crypto.randomUUID()}`;return mutate(directory,{...options,owner},()=>require('./generation-replan.cjs').run(directory,{...options,owner},module.exports)); }
|
|
223
|
+
function build(directory,options={}) { const gates=require('./execution-runtime.cjs');gates.requireStage(directory,'implementation',module.exports);gates.requireStage(directory,'design',module.exports);return require('./native-build.cjs').build(directory,options,{manifest,inspect}); }
|
|
224
|
+
function synchronizeIndex(options={}) {
|
|
225
|
+
id(options.id);
|
|
226
|
+
const index=projectRegistry.read(options.project),entry=index.registry.prototypes.find(item=>item.id===options.id);
|
|
227
|
+
assert.ok(entry,'Prototype is not registered');
|
|
228
|
+
const directory=inside(index.project,entry.directory),scope=json(inside(directory,'inputs/scope.json'));
|
|
229
|
+
assert.ok(scope.moduleIds!==undefined||scope.schemaVersion===2,'Legacy scope has no explicit moduleIds; do not invent or rewrite old input contracts');
|
|
230
|
+
const current=manifest(directory);
|
|
231
|
+
return mutate(directory,{...options,expectedRevision:current.revision,expectedDigest:current.digest},()=>{
|
|
232
|
+
const checked=inspect(directory);assert.equal(checked.status,'passed',checked.errors?.join('; '));
|
|
233
|
+
return projectRegistry.transaction(index.project,store=>{
|
|
234
|
+
const selected=store.registry.prototypes.find(item=>item.id===options.id);
|
|
235
|
+
assert.ok(selected&&selected.directory===entry.directory,'Prototype index entry changed during synchronization');
|
|
236
|
+
const fixed=json(inside(directory,'inputs/scope.json')),appliesTo=projectRegistry.moduleIds(fixed);
|
|
237
|
+
assert.equal(manifest(directory).digest,current.digest,'Candidate changed during synchronization');
|
|
238
|
+
selected.appliesTo=appliesTo;store.save();
|
|
239
|
+
return {status:'synchronized',index:store.file,id:selected.id,appliesTo};
|
|
240
|
+
});
|
|
192
241
|
});
|
|
193
242
|
}
|
|
194
|
-
function build(directory,options={}) { return require('./native-build.cjs').build(directory,options,{manifest,inspect}); }
|
|
195
243
|
function verifyBuild(directory,buildId) { return require('./native-build.cjs').verifyBuild(directory,buildId,{manifest}); }
|
|
196
244
|
function preview(directory,options={}) { return require('./native-build.cjs').preview(directory,options,{manifest}); }
|
|
197
245
|
function recordRun(directory,report) {
|
|
@@ -241,4 +289,4 @@ function compareBaseline(directory) {
|
|
|
241
289
|
}
|
|
242
290
|
function state(directory,options={}) { return require('./native-delivery.cjs').state(directory,options,module.exports); }
|
|
243
291
|
function finalize(directory,options={}) { return require('./native-delivery.cjs').finalize(directory,options,module.exports); }
|
|
244
|
-
module.exports={analyze,prepareTemplate,createPrototype,manifest,inspect,inspectDesignReview,effectiveReview,claim,release,revise,build,verifyBuild,preview,recordRun,confirmBaseline,compareBaseline,state,finalize};
|
|
292
|
+
module.exports={analyze,prepareTemplate,createPrototype,manifest,inspect,inspectDesignReview,effectiveReview,claim,release,revise,replan,synchronizeIndex,build,verifyBuild,preview,recordRun,confirmBaseline,compareBaseline,state,finalize};
|
|
@@ -5,14 +5,29 @@ const fs=require('node:fs');
|
|
|
5
5
|
const lib=require('./native-pilot-lib.cjs');
|
|
6
6
|
async function main(argv=process.argv.slice(2)) {
|
|
7
7
|
const operation=argv.shift()||'analyze'; const positional=[]; const options={};
|
|
8
|
-
while(argv.length) { const value=argv.shift(); if(value.startsWith('--')) { const key=value.slice(2).replace(/-([a-z])/g,(_,c)=>c.toUpperCase()); options[key]=!argv.length||argv[0].startsWith('--')?true:argv.shift(); } else positional.push(value); }
|
|
8
|
+
while(argv.length) { const value=argv.shift(); if(value==='--') { options.command=argv.splice(0); break; } if(value.startsWith('--')) { const key=value.slice(2).replace(/-([a-z])/g,(_,c)=>c.toUpperCase()); options[key]=!argv.length||argv[0].startsWith('--')?true:argv.shift(); } else positional.push(value); }
|
|
9
9
|
const root=positional[0]?path.resolve(positional[0]):null;
|
|
10
10
|
switch(operation) {
|
|
11
11
|
case 'analyze': return lib.analyze(options);
|
|
12
|
+
case 'paths': { const context=require('./prototype-locations.cjs').resolveLocations(options); return {...require('./project-registry.cjs').paths(options.project,options.id),host:context.host,scope:context.scope,projectKey:context.projectKey,toolsDirectory:context.toolsDirectory,cacheDirectory:context.cacheDirectory}; }
|
|
13
|
+
case 'index': return require('./project-registry.cjs').query(options);
|
|
14
|
+
case 'sync-index': return lib.synchronizeIndex(options);
|
|
15
|
+
case 'migrate-index': return require('./project-registry.cjs').migrate(options);
|
|
16
|
+
case 'browser-cache': return require('./browser-workspace.cjs').prepare(options);
|
|
17
|
+
case 'browser-cli': return require('./browser-workspace.cjs').runCli(options);
|
|
18
|
+
case 'archive-browser-output': return require('./browser-workspace.cjs').archive(options);
|
|
12
19
|
case 'prepare-template': return lib.prepareTemplate(options);
|
|
13
20
|
case 'sync-template': { const templates=require('./template-contract.cjs'); return options.check?templates.loadTemplate(root).index:templates.syncTemplate(root); }
|
|
14
|
-
case 'template-page': { const templates=require('./template-contract.cjs'); return require('./content-template.cjs').pageContent(root?templates.loadTemplate(root):templates.loadBundledTemplate(),options.page); }
|
|
21
|
+
case 'template-page': { const templates=require('./template-contract.cjs'); return require('./content-template.cjs').pageContent(root?templates.loadTemplate(root):templates.loadBundledTemplate(options.template),options.page); }
|
|
22
|
+
case 'generation-options': { const templates=require('./template-contract.cjs'),snapshot=root?templates.loadTemplate(root):templates.loadBundledTemplate(options.template),meta=require('./generation-contract.cjs').resource(snapshot,'generation.json');return {status:'passed',readOnly:true,template:snapshot.id,...meta,patterns:require('./generation-contract.cjs').definition().patterns}; }
|
|
23
|
+
case 'generation-plan': { const templates=require('./template-contract.cjs'),snapshot=root?templates.loadTemplate(root):templates.loadBundledTemplate(options.template);const contract=require('./generation-contract.cjs');return contract.view(contract.compile(snapshot,JSON.parse(fs.readFileSync(path.resolve(options.scope),'utf8'))),options); }
|
|
24
|
+
case 'inventory': { const templates=require('./template-contract.cjs'),snapshot=root?templates.loadTemplate(root):templates.loadBundledTemplate(options.template),value=require('./template-inventory.cjs').verifySnapshot(snapshot);return options.details?value:{status:'passed',readOnly:true,template:snapshot.id,files:Object.keys(value.files).length,routes:value.routes.length,pages:value.pages.length,fragments:value.fragments.length,components:Object.keys(value.components).length,events:value.eventCount}; }
|
|
25
|
+
case 'sync-generation-assets': return require('./generation-assets.cjs').refresh(path.resolve(__dirname,'..'),{check:!!options.check});
|
|
15
26
|
case 'create': return lib.createPrototype(options);
|
|
27
|
+
case 'execution-begin': return require('./execution-runtime.cjs').begin(root,lib);
|
|
28
|
+
case 'execution-assertions-template': return require('./execution-assertions.cjs').template(options);
|
|
29
|
+
case 'execution-template': return require('./execution-runtime.cjs').template(root,lib);
|
|
30
|
+
case 'execution-check': return require('./execution-runtime.cjs').check(root,options.stage||'delivery',lib);
|
|
16
31
|
case 'manifest': return lib.manifest(root);
|
|
17
32
|
case 'inspect': return lib.inspect(root,{acceptance:!!options.acceptance});
|
|
18
33
|
case 'state': return lib.state(root,options);
|
|
@@ -22,6 +37,7 @@ async function main(argv=process.argv.slice(2)) {
|
|
|
22
37
|
case 'build': return lib.build(root,options);
|
|
23
38
|
case 'preview': { const running=await lib.preview(root,options); process.stdout.write(JSON.stringify({url:running.url,buildId:running.build.buildId})+'\n'); return null; }
|
|
24
39
|
case 'revise': return lib.revise(root,options);
|
|
40
|
+
case 'replan': return lib.replan(root,options);
|
|
25
41
|
case 'claim': return lib.claim(root,options);
|
|
26
42
|
case 'release': return lib.release(root,options.owner);
|
|
27
43
|
case 'record-run': return lib.recordRun(root,JSON.parse(fs.readFileSync(path.resolve(options.report),'utf8')));
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const { fs, path, crypto, hash, assert, json, id, inside, write } = require('./native-files.cjs');
|
|
3
|
+
function paths(project, prototypeId) {
|
|
4
|
+
const root = fs.realpathSync(path.resolve(project || '.'));
|
|
5
|
+
assert.ok(fs.statSync(root).isDirectory(), 'Project must be a directory');
|
|
6
|
+
if (prototypeId !== undefined) id(prototypeId);
|
|
7
|
+
return { project: root, output: inside(root, 'prototypes'), index: inside(root, 'prototypes/index.json'),
|
|
8
|
+
legacyIndex: inside(root, '.i3-ui-prototypes.json'), lock: inside(root, 'prototypes/.index.lock'), legacyLock: inside(root, '.i3-ui-prototypes.lock'),
|
|
9
|
+
...(prototypeId === undefined ? {} : { directory: inside(root, `prototypes/${prototypeId}`), preparation: inside(root, `prototypes/.preparation/${prototypeId}`), scopeFile: inside(root, `prototypes/.preparation/${prototypeId}/scope.json`) }) };
|
|
10
|
+
}
|
|
11
|
+
function moduleIds(scope = {}) {
|
|
12
|
+
if (scope.schemaVersion !== undefined) assert.ok([1, 2, 3].includes(scope.schemaVersion), 'Unsupported scope schemaVersion');
|
|
13
|
+
if (scope.moduleIds === undefined && ![2, 3].includes(scope.schemaVersion)) return [];
|
|
14
|
+
assert.ok(Array.isArray(scope.moduleIds), 'scope.moduleIds must be an array; use [] for unbound scope');
|
|
15
|
+
for (const value of scope.moduleIds) assert.ok(typeof value === 'string' && value.length <= 128 && /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(value), 'Invalid module identifier in scope.moduleIds; use stable PRD module IDs, not paths or requirement text');
|
|
16
|
+
return [...new Set(scope.moduleIds)];
|
|
17
|
+
}
|
|
18
|
+
function validate(registry, project) {
|
|
19
|
+
assert.equal(registry.schemaVersion, 1, 'Invalid project prototype registry version');
|
|
20
|
+
assert.ok(Array.isArray(registry.prototypes), 'Invalid project prototype registry');
|
|
21
|
+
const ids = new Set();
|
|
22
|
+
for (const entry of registry.prototypes) {
|
|
23
|
+
id(entry.id); assert.ok(!ids.has(entry.id), 'Duplicate prototype ID in registry'); ids.add(entry.id);
|
|
24
|
+
inside(project, entry.directory);
|
|
25
|
+
assert.ok(entry.appliesTo === undefined || (Array.isArray(entry.appliesTo) && entry.appliesTo.every(value => typeof value === 'string')), 'Invalid appliesTo array');
|
|
26
|
+
}
|
|
27
|
+
return registry;
|
|
28
|
+
}
|
|
29
|
+
function read(project) {
|
|
30
|
+
const locations = paths(project), modern = fs.existsSync(locations.index), legacy = fs.existsSync(locations.legacyIndex);
|
|
31
|
+
if (modern && legacy) assert.deepEqual(json(locations.index), json(locations.legacyIndex), 'Conflicting prototype indices; resolve or explicitly migrate before writing');
|
|
32
|
+
const file = modern || !legacy ? locations.index : locations.legacyIndex;
|
|
33
|
+
const bytes = fs.existsSync(file) ? fs.readFileSync(file) : null;
|
|
34
|
+
return { ...locations, file, legacy: !modern && legacy, duplicateLegacy: modern && legacy,
|
|
35
|
+
bytes, registry: validate(bytes ? JSON.parse(bytes.toString('utf8')) : { schemaVersion: 1, prototypes: [] }, locations.project) };
|
|
36
|
+
}
|
|
37
|
+
function transaction(project, operation, { allowDuplicate = false } = {}) {
|
|
38
|
+
const initial = read(project);
|
|
39
|
+
assert.ok(allowDuplicate || !initial.duplicateLegacy, 'Duplicate active indices; explicitly resolve before writing');
|
|
40
|
+
assert.ok(!fs.existsSync(initial.legacyLock), 'Legacy prototype writer lock is active');
|
|
41
|
+
const createdOutput = !fs.existsSync(initial.output), locks = [];
|
|
42
|
+
fs.mkdirSync(initial.output, { recursive: true });
|
|
43
|
+
const acquire = file => { const fd = fs.openSync(file, 'wx'); locks.push({ file, fd }); };
|
|
44
|
+
try {
|
|
45
|
+
acquire(initial.lock);
|
|
46
|
+
if (initial.legacy || initial.duplicateLegacy) acquire(initial.legacyLock);
|
|
47
|
+
else assert.ok(!fs.existsSync(initial.legacyLock), 'Legacy prototype writer lock is active');
|
|
48
|
+
const current = read(project);
|
|
49
|
+
assert.equal(current.file, initial.file, 'Prototype index location changed while acquiring lock');
|
|
50
|
+
assert.ok(allowDuplicate || !current.duplicateLegacy, 'Duplicate active indices');
|
|
51
|
+
return operation({ ...current, save() {
|
|
52
|
+
const latest = read(project);
|
|
53
|
+
assert.equal(latest.file, current.file, 'Prototype index location changed');
|
|
54
|
+
assert.deepEqual(latest.bytes, current.bytes, 'Prototype index changed outside the active writer');
|
|
55
|
+
validate(current.registry, current.project);
|
|
56
|
+
write(current.project, path.relative(current.project, current.file).split(path.sep).join('/'), current.registry);
|
|
57
|
+
} });
|
|
58
|
+
} finally {
|
|
59
|
+
for (const { file, fd } of locks.reverse()) { fs.closeSync(fd); fs.unlinkSync(file); }
|
|
60
|
+
if (createdOutput && fs.existsSync(initial.output) && !fs.readdirSync(initial.output).length) fs.rmdirSync(initial.output);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function query(options = {}) {
|
|
64
|
+
const current = read(options.project), prototypes = current.registry.prototypes;
|
|
65
|
+
if (options.module !== undefined) assert.ok(typeof options.module === 'string' && options.module.trim(), 'Provide a nonempty module identifier or existing legacy label');
|
|
66
|
+
const errors = [], invalid = new Set();
|
|
67
|
+
for (const entry of prototypes) {
|
|
68
|
+
try {
|
|
69
|
+
const scopeFile = inside(current.project, `${entry.directory}/inputs/scope.json`);
|
|
70
|
+
if (fs.existsSync(scopeFile)) {
|
|
71
|
+
const scope = json(scopeFile);
|
|
72
|
+
if (scope.schemaVersion === 2 || scope.moduleIds !== undefined) assert.deepEqual(entry.appliesTo || [], moduleIds(scope), 'appliesTo differs from fixed scope.moduleIds; explicitly revise or synchronize the index');
|
|
73
|
+
}
|
|
74
|
+
} catch (error) { invalid.add(entry.id); errors.push(`${entry.id}: ${error.message}`); }
|
|
75
|
+
}
|
|
76
|
+
const matches = options.module === undefined ? [] : prototypes.filter(entry => !invalid.has(entry.id) && entry.appliesTo?.includes(options.module));
|
|
77
|
+
return { status: errors.length ? 'failed' : 'checked', index: current.file, legacy: current.legacy, prototypes, errors,
|
|
78
|
+
matches, unbound: prototypes.filter(entry => !invalid.has(entry.id) && !entry.appliesTo?.length), selection: matches.length > 1 ? 'ambiguous' : matches.length ? 'matched' : 'none', readOnly: true,
|
|
79
|
+
note: 'Module association does not prove implementation, current verification or human approval.' };
|
|
80
|
+
}
|
|
81
|
+
function withInstanceAssociation(directory, scope, operation) {
|
|
82
|
+
const parent = path.dirname(path.resolve(directory));
|
|
83
|
+
if (path.basename(parent) !== 'prototypes' || (scope.schemaVersion !== 2 && scope.moduleIds === undefined)) return operation();
|
|
84
|
+
const project = path.dirname(parent), current = read(project);
|
|
85
|
+
if (!current.registry.prototypes.some(entry => path.resolve(project, entry.directory) === path.resolve(directory))) return operation();
|
|
86
|
+
const appliesTo = moduleIds(scope), metadataFile = inside(directory, 'prototype.json'), before = fs.readFileSync(metadataFile);
|
|
87
|
+
return transaction(project, store => {
|
|
88
|
+
const entry = store.registry.prototypes.find(item => path.resolve(project, item.directory) === path.resolve(directory));
|
|
89
|
+
assert.ok(entry, 'Prototype entry disappeared during revision');
|
|
90
|
+
try { const result = operation(); entry.appliesTo = appliesTo; store.save(); return result; }
|
|
91
|
+
catch (error) { write(directory, 'prototype.json', before.toString('utf8')); throw error; }
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
function migrate(options = {}) {
|
|
95
|
+
assert.ok(options.check === undefined || typeof options.check === 'boolean', '--check must be a boolean flag');
|
|
96
|
+
const initial = read(options.project);
|
|
97
|
+
if (!fs.existsSync(initial.legacyIndex)) return { status: 'current', index: initial.index, sideEffects: [] };
|
|
98
|
+
const original = fs.readFileSync(initial.legacyIndex), digest = hash(original);
|
|
99
|
+
if (options.expectedDigest !== undefined) assert.equal(options.expectedDigest, digest, 'Legacy index changed since preview');
|
|
100
|
+
const plan = { source: initial.legacyIndex, destination: initial.index, sourceDigest: digest, prototypes: initial.registry.prototypes.length };
|
|
101
|
+
if (options.check) return { status: 'migration-preview', ...plan, sideEffects: [] };
|
|
102
|
+
return transaction(options.project, store => {
|
|
103
|
+
assert.deepEqual(fs.readFileSync(store.legacyIndex), original, 'Legacy index changed during migration');
|
|
104
|
+
const folder = inside(store.project, `prototypes/.migration/index-${crypto.randomUUID()}`);
|
|
105
|
+
fs.mkdirSync(folder, { recursive: true });
|
|
106
|
+
const backup = inside(folder, 'legacy-index.json'), temp = inside(store.project, `prototypes/.index-${crypto.randomUUID()}.tmp`);
|
|
107
|
+
fs.writeFileSync(backup, original, { flag: 'wx' });
|
|
108
|
+
write(folder, 'source.json', { ...plan, backup, purpose: 'Source snapshot before explicit index migration' });
|
|
109
|
+
let created = false;
|
|
110
|
+
try {
|
|
111
|
+
if (!fs.existsSync(store.index)) {
|
|
112
|
+
fs.writeFileSync(temp, original, { flag: 'wx' });
|
|
113
|
+
try { fs.linkSync(temp, store.index); created = true; }
|
|
114
|
+
finally { if (fs.existsSync(temp)) fs.unlinkSync(temp); }
|
|
115
|
+
} else assert.deepEqual(fs.readFileSync(store.index), original, 'Destination index already exists with different content');
|
|
116
|
+
assert.deepEqual(fs.readFileSync(backup), original, 'Migration backup mismatch');
|
|
117
|
+
assert.deepEqual(fs.readFileSync(store.legacyIndex), original, 'Legacy index changed before commit');
|
|
118
|
+
fs.unlinkSync(store.legacyIndex);
|
|
119
|
+
return { status: 'migrated', ...plan, backup };
|
|
120
|
+
} catch (error) {
|
|
121
|
+
if (created && fs.existsSync(store.index) && fs.readFileSync(store.index).equals(original)) fs.unlinkSync(store.index);
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
}, { allowDuplicate: true });
|
|
125
|
+
}
|
|
126
|
+
module.exports = { paths, moduleIds, read, transaction, query, withInstanceAssociation, migrate };
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// Explicit configuration lifecycle. check is read-only and never invokes the installer.
|
|
4
|
+
const fs=require('node:fs'),path=require('node:path'),os=require('node:os'),assert=require('node:assert/strict');
|
|
5
|
+
const R=require('./codebuddy-registration.cjs'),C=require('./codebuddy-context.cjs'),E=require('./execution-evidence.cjs');
|
|
6
|
+
function context(options) {
|
|
7
|
+
assert.equal(options.host||'codebuddy','codebuddy','A+B supports CodeBuddy only');
|
|
8
|
+
const project=fs.realpathSync(path.resolve(options.project||'.')),scope=options.scope;
|
|
9
|
+
assert.ok(['project','global'].includes(scope),'Specify project or global installation scope');
|
|
10
|
+
const base=scope==='project'?path.join(project,'.codebuddy'):C.globalBase(options);
|
|
11
|
+
const recordText=R.fileIO.read(path.join(base,'state/i3-prototype/installation.json'));
|
|
12
|
+
const record=recordText?JSON.parse(recordText):null;
|
|
13
|
+
const unified=path.join(base,'i3-composer-agent/hooks/i3-prototype.cjs');
|
|
14
|
+
const scriptPath=options.entryPath||(fs.existsSync(unified)?unified:path.join(base,'skills/i3-ui-prototype/scripts/hosts/codebuddy.cjs'));
|
|
15
|
+
return {project,scope,base,record,scriptPath};
|
|
16
|
+
}
|
|
17
|
+
function prepare(options,runtime={}) {
|
|
18
|
+
const current=runtime.current||context(options),readText=runtime.readText||R.fileIO.read,action=options.action||'keep';
|
|
19
|
+
assert.ok(['keep','enable','disable'].includes(action),'Invalid Hook configuration action');
|
|
20
|
+
if(['keep','disable'].includes(action)&&!current.record){
|
|
21
|
+
if(action==='disable'){const settings=R.config(readText(path.join(current.base,'settings.json')));assert.ok(!Object.values(settings.hooks||{}).flat().some(g=>g.hooks.some(h=>h.command?.includes(R.OWNER))),'Unknown owned-looking Hook has no ownership record; preserve it for review');}
|
|
22
|
+
return {summary:{status:'not-enabled',scope:current.scope,changed:false},unchanged:true};
|
|
23
|
+
}
|
|
24
|
+
const product=options.product||current.record?.product;assert.ok(['ide','cli'].includes(product),'Use --codebuddy-product ide or cli when enabling Hooks');
|
|
25
|
+
if(action!=='disable') {
|
|
26
|
+
const manifest=readText(path.join(current.base,'.i3-composer-agent.json'));
|
|
27
|
+
if(manifest)assert.ok(!JSON.parse(manifest).agents?.includes('workbuddy'),'Shared CodeBuddy/WorkBuddy registration cannot safely identify the product; preserve settings and use an exclusive CodeBuddy scope');
|
|
28
|
+
assert.ok(!options.agents?.includes('workbuddy')||current.scope!=='project','Do not enable CodeBuddy audit in a shared WorkBuddy project target');
|
|
29
|
+
}
|
|
30
|
+
const explicit=options.transcriptRoot,roots=explicit==='auto'?[]:explicit?[path.resolve(explicit)]:current.record?.transcriptRoots||[];
|
|
31
|
+
const mode=explicit?(explicit==='auto'?'auto':'manual'):current.record?.transcriptMode||(roots.length?'manual':'auto');
|
|
32
|
+
if(action!=='disable') {
|
|
33
|
+
for(const root of roots){const relative=path.relative(current.project,root);assert.ok(relative==='..'||relative.startsWith('..'+path.sep)||path.isAbsolute(relative),'Native log root must be outside the business project; QA exports are not native provenance');}
|
|
34
|
+
}
|
|
35
|
+
const settingsText=readText(path.join(current.base,'settings.json')),settings=R.config(settingsText);
|
|
36
|
+
if(action!=='disable')assert.ok(!settings.disableAllHooks&&!settings.allowManagedHooksOnly,'Host policy prevents automatic auditing; preserve the existing policy');
|
|
37
|
+
return R.plan({action,...current,product,nodePath:process.execPath,scriptDigest:runtime.scriptDigest||C.bundle(options.entrySource||(path.basename(current.scriptPath)==='i3-prototype.cjs'?current.scriptPath:path.join(__dirname,'hosts/codebuddy.cjs'))),currentText:settingsText,
|
|
38
|
+
transcriptMode:mode,transcriptRoots:roots,productVersion:options.productVersion||current.record?.productVersion||'unreported'});
|
|
39
|
+
}
|
|
40
|
+
function configure(options) {
|
|
41
|
+
const change=prepare(options);if(change.unchanged||options.preview)return change.summary;
|
|
42
|
+
return R.apply(change);
|
|
43
|
+
}
|
|
44
|
+
function check(options,runtime={}) {
|
|
45
|
+
const current=runtime.current||context(options),profile=require('../assets/hosts/codebuddy/profile.json');
|
|
46
|
+
const result={scope:current.scope,base:current.base,scriptPath:current.scriptPath,filesPresent:fs.existsSync(current.scriptPath),profile:profile.id,nativeValidation:profile.nativeValidation,readOnly:true,sideEffects:[]};
|
|
47
|
+
if(process.platform==='win32')return {...result,status:'unsupported',reason:'CodeBuddy Hook quoting on Windows has not been validated'};
|
|
48
|
+
if(!current.record){
|
|
49
|
+
const settings=R.config(R.fileIO.read(path.join(current.base,'settings.json')));
|
|
50
|
+
if(Object.values(settings.hooks||{}).flat().some(group=>group.hooks.some(h=>h.command?.includes(R.OWNER))))return {...result,status:'failed',errors:['Unknown prototype Hook registration has no ownership record; preserve it for review']};
|
|
51
|
+
return {...result,status:result.filesPresent?'files-present':'not-enabled'};
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const record=(runtime.registration||C.registration)(current.base),effective=(runtime.select||C.select)({project:current.project,product:record.product,home:options.home,env:options.env});
|
|
55
|
+
Object.assign(result,{product:record.product,productVersion:record.productVersion,registrationDigest:record.registrationDigest,effective:effective?.base===current.base,transcriptRoots:record.transcriptRoots,
|
|
56
|
+
transcript:{mode:record.transcriptMode||(record.transcriptRoots?.length?'manual':'auto'),status:'awaiting-native-event',invocationVerified:false}});
|
|
57
|
+
let observation=null;
|
|
58
|
+
const parent=path.join(current.base,'state/i3-prototype',C.projectKey(current.project));
|
|
59
|
+
const entries=runtime.entries||[];
|
|
60
|
+
if(!runtime.entries&&fs.existsSync(parent))for(const name of fs.readdirSync(parent)) {
|
|
61
|
+
if(!/^[a-f0-9]{24}$/.test(name))continue;
|
|
62
|
+
const raw=R.fileIO.read(path.join(parent,name,'session.json'));if(!raw)continue;
|
|
63
|
+
entries.push(...Object.values(JSON.parse(raw).bindings||{}));
|
|
64
|
+
}
|
|
65
|
+
for(const entry of entries)if(entry.project===current.project&&entry.registrationDigest===record.registrationDigest&&entry.settingsDigest===record.settingsDigest&&(!observation||entry.lastHook.at>observation.lastHook.at))observation=entry;
|
|
66
|
+
if(observation) {
|
|
67
|
+
result.transcript.path=observation.source.path;
|
|
68
|
+
C.prefix(observation.source.path,observation.source.bytes,observation.source.digest);
|
|
69
|
+
C.source({session_id:observation.sessionId,transcript_path:observation.source.path},record,current.project);
|
|
70
|
+
Object.assign(result.transcript,{status:'verified',invocationVerified:true});
|
|
71
|
+
result.observation={sessionId:observation.sessionId,projectKey:observation.projectKey,runId:observation.runId,event:observation.lastHook.event,at:observation.lastHook.at};
|
|
72
|
+
}
|
|
73
|
+
const recent=observation&&Date.now()-Date.parse(observation.lastHook.at)>=0&&Date.now()-Date.parse(observation.lastHook.at)<30*60*1000;
|
|
74
|
+
return {...result,status:result.effective&&recent?'active':record.product==='cli'?'pending-host-review':'registered',
|
|
75
|
+
activationScope:'Only the displayed project/session/configuration and adapter bytes; not all projects or business acceptance',
|
|
76
|
+
next:record.product==='cli'?'Review /hooks in CodeBuddy Code, then use a new session.':'Reload CodeBuddy and verify a real prototype write task.'};
|
|
77
|
+
}catch(error){if(result.transcript)Object.assign(result.transcript,{status:'unverified',invocationVerified:false});return {...result,status:'failed',errors:[error.message]};}
|
|
78
|
+
}
|
|
79
|
+
function main(argv=process.argv.slice(2)) {
|
|
80
|
+
const central=[path.resolve(__dirname,'../../../i3-composer-agent/hooks/manage.cjs'),path.resolve(__dirname,'../../../hooks/manage.cjs')].find(file=>fs.existsSync(file));
|
|
81
|
+
if(central)return require(central).main([...argv,'--groups','prototype']);
|
|
82
|
+
const options={action:argv.shift()||'check',host:'codebuddy',project:process.cwd()};
|
|
83
|
+
while(argv.length){const arg=argv.shift();assert.ok(arg.startsWith('--')&&argv[0]&&!argv[0].startsWith('--'),'Hook option needs a value');const key=arg.slice(2).replace(/-([a-z])/g,(_,c)=>c.toUpperCase());assert.ok(['host','scope','project','product','productVersion','transcriptRoot'].includes(key),'Unknown Hook option');options[key]=argv.shift();}
|
|
84
|
+
return options.action==='check'?check(options):configure(options);
|
|
85
|
+
}
|
|
86
|
+
module.exports={context,globalBase:C.globalBase,prepare,configure,check,main};
|
|
87
|
+
if(require.main===module)try{const result=main();process.stdout.write(JSON.stringify(result,null,2)+'\n');if(result.status==='failed')process.exitCode=1;}catch(error){process.stderr.write(error.message+'\n');process.exitCode=1;}
|
|
@@ -5,6 +5,7 @@ const crypto = require('node:crypto');
|
|
|
5
5
|
const { spawnSync } = require('node:child_process');
|
|
6
6
|
const templateRegistry = require('./template-adapters.cjs').createTemplateRegistry();
|
|
7
7
|
const { discoverProviderCandidates } = require('./design-provider-discovery.cjs');
|
|
8
|
+
const projectRegistry = require('./project-registry.cjs');
|
|
8
9
|
const hash = value => crypto.createHash('sha256').update(value).digest('hex');
|
|
9
10
|
const json = file => JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
10
11
|
function ensure(condition, message) { if (!condition) throw new Error(message); }
|
|
@@ -113,11 +114,8 @@ function initPrototype(options, registry = templateRegistry) {
|
|
|
113
114
|
ensure(fs.existsSync(project) && fs.statSync(project).isDirectory(), 'Project directory does not exist');
|
|
114
115
|
const directory = contained(project, `prototypes/${id}`);
|
|
115
116
|
ensure(!fs.existsSync(directory), 'Prototype already exists / 原型已存在,使用 revise 或另选标识');
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
try {
|
|
119
|
-
const registryPath = contained(project, '.i3-ui-prototypes.json');
|
|
120
|
-
const registry = fs.existsSync(registryPath) ? json(registryPath) : { schemaVersion: 1, prototypes: [] };
|
|
117
|
+
return projectRegistry.transaction(project, store => {
|
|
118
|
+
const registry = store.registry;
|
|
121
119
|
ensure(registry.schemaVersion === 1 && Array.isArray(registry.prototypes), 'Invalid project prototype registry');
|
|
122
120
|
ensure(!registry.prototypes.some(entry => entry.id === id), 'Prototype registry already contains identifier');
|
|
123
121
|
const preset = selected.preset;
|
|
@@ -142,11 +140,11 @@ function initPrototype(options, registry = templateRegistry) {
|
|
|
142
140
|
fs.renameSync(staging, directory);
|
|
143
141
|
created = true;
|
|
144
142
|
registry.prototypes.push({ id, directory: `prototypes/${id}`, appliesTo: [] });
|
|
145
|
-
|
|
143
|
+
store.save();
|
|
146
144
|
} catch (error) { if (created) fs.rmSync(directory, { recursive: true, force: true }); throw error; }
|
|
147
145
|
finally { if (fs.existsSync(staging)) fs.rmSync(staging, { recursive: true, force: true }); }
|
|
148
146
|
return { status: 'created', directory, template: template.id, expertMode };
|
|
149
|
-
}
|
|
147
|
+
});
|
|
150
148
|
}
|
|
151
149
|
function contentManifest(directory, registry = templateRegistry) {
|
|
152
150
|
const metadata = json(contained(directory, 'prototype.json'));
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
const fs = require('node:fs');
|
|
3
3
|
const path = require('node:path');
|
|
4
4
|
const os = require('node:os');
|
|
5
|
+
const crypto = require('node:crypto');
|
|
5
6
|
// Reviewed mirror of i3's installer target table; maintenance tests require byte equality.
|
|
6
7
|
const registry = require('../assets/optional-skills/host-targets.json');
|
|
7
8
|
const aliases = { claude: 'claude-code', gemini: 'gemini-cli', copilot: 'github-copilot' };
|
|
@@ -11,6 +12,7 @@ function globalSkillsDir(agent, home, env) {
|
|
|
11
12
|
let base = home;
|
|
12
13
|
if (config.base === 'config') base = env.XDG_CONFIG_HOME && path.isAbsolute(env.XDG_CONFIG_HOME) ? env.XDG_CONFIG_HOME : path.join(home, '.config');
|
|
13
14
|
if (config.base === 'env') base = env[config.env]?.trim() || path.join(home, config.fallback);
|
|
15
|
+
if (config.env === 'CODEBUDDY_CONFIG_DIR' && (!path.isAbsolute(base) || /[\x00\r\n]/.test(base))) throw new Error('CODEBUDDY_CONFIG_DIR must be an absolute safe directory');
|
|
14
16
|
if (config.alternatives && !fs.existsSync(path.join(home, path.dirname(config.path)))) {
|
|
15
17
|
const alternative = config.alternatives.find(relative => fs.existsSync(path.join(home, path.dirname(relative))));
|
|
16
18
|
if (alternative) return path.join(home, alternative);
|
|
@@ -41,8 +43,10 @@ function resolveLocations(options = {}) {
|
|
|
41
43
|
const projectBase = path.dirname(projectSkills), globalBase = globalSkills ? path.dirname(globalSkills) : null;
|
|
42
44
|
// Some hosts put skills directly in the workspace; tools still get a host directory.
|
|
43
45
|
const projectToolsBase = projectBase === project ? path.join(project, `.${host}`) : projectBase;
|
|
46
|
+
const projectKey = `${path.basename(project).replace(/[^a-zA-Z0-9_-]/g, '-').slice(0, 32) || 'project'}-${crypto.createHash('sha256').update(project).digest('hex').slice(0, 16)}`;
|
|
47
|
+
const cacheBase = path.join(scope === 'global' ? globalBase : projectToolsBase, 'cache/i3-prototype');
|
|
44
48
|
return { host, scope, project, home, skillsDirectory, base, projectSkills, globalSkills,
|
|
45
|
-
projectBase, globalBase, projectToolsBase,
|
|
49
|
+
projectBase, globalBase, projectToolsBase, projectKey, cacheDirectory: scope === 'global' ? path.join(cacheBase, projectKey) : cacheBase,
|
|
46
50
|
toolsDirectory: path.join(scope === 'global' ? globalBase : projectToolsBase, 'tools/i3-prototype') };
|
|
47
51
|
}
|
|
48
52
|
function scopeBases(context) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
2
3
|
const fs = require('node:fs');
|
|
3
4
|
const path = require('node:path');
|
|
4
5
|
const crypto = require('node:crypto');
|
|
@@ -300,6 +301,7 @@ function checkCapabilities({ definition, bindings = [], regions = [], scope = {}
|
|
|
300
301
|
}
|
|
301
302
|
function syncTemplate(directory, { write = true } = {}) {
|
|
302
303
|
directory = path.resolve(directory);
|
|
304
|
+
if (write && fs.existsSync(path.join(directory, 'component-inventory.json'))) fs.writeFileSync(path.join(directory, 'template-inventory.json'), JSON.stringify(require('./template-inventory.cjs').scan(directory), null, 2) + '\n');
|
|
303
305
|
const files = filesUnder(directory);
|
|
304
306
|
const manifest = readJson(localFile(directory, 'template.json'));
|
|
305
307
|
identifier(manifest.id, 'template');
|
|
@@ -345,8 +347,15 @@ function loadTemplate(directory) {
|
|
|
345
347
|
if (!stored || canonical(stored) !== canonical(expected)) throw new Error('Template index is stale / 源和索引不同步; run sync-template.cjs');
|
|
346
348
|
return templateSnapshot(directory, expected);
|
|
347
349
|
}
|
|
348
|
-
function loadBundledTemplate() {
|
|
349
|
-
const
|
|
350
|
+
function loadBundledTemplate(templateId, { templatesRoot = path.join(ASSETS, 'templates') } = {}) {
|
|
351
|
+
const catalog = readJson(localFile(templatesRoot, 'catalog.json'));
|
|
352
|
+
templateId ||= catalog.defaultTemplate; identifier(templateId, 'template');
|
|
353
|
+
const selected = catalog.templates.find(item => item.id === templateId);
|
|
354
|
+
assert.ok(selected?.availability === 'available' && selected.contentRoot, `Unsupported or not available native template: ${templateId}`);
|
|
355
|
+
const directory = localFile(templatesRoot, selected.contentRoot);
|
|
356
|
+
const manifest = readJson(localFile(directory, 'template.json'));
|
|
357
|
+
assert.equal(manifest.kind, 'content-reference', 'Native templates require an explicit content-reference manifest');
|
|
358
|
+
assert.equal(manifest.id, templateId, 'Template catalog identity differs from its manifest');
|
|
350
359
|
return templateSnapshot(directory, syncTemplate(directory, { write: false }));
|
|
351
360
|
}
|
|
352
361
|
function prepareTemplate({ project, id = 'enterprise-admin' }) {
|