@antprofuse/saddle-islands-tools 0.1.14 → 0.1.15

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antprofuse/saddle-islands-tools",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
4
4
  "description": "Independent Saddle–Islands engineering tools",
5
5
  "files": [
6
6
  "tools"
@@ -110,3 +110,11 @@ MT使用 `iteration stage-gate` 时必须提供 `--compile-result NEW_EXTERNAL_D
110
110
  Owner在项目配置填写 `pageSourceId`,精确引用冻结 `anchorInventory` 中 `kind: page` 的源ID;与已有 `pageRoute` 一并绑定到编译产物。公共编译拒绝未知页面、缺绑定及借用组件身份,执行前拒绝与冻结绑定不同的运行路由。
111
111
 
112
112
  产品在业务页面唯一根节点声明 `data-page-root="true"`,它是布尔页面载体标记,不是组件ID,不加入 `.registry`。工具在已选择的业务frame内采集该根节点,Minifish原生 `$$dataset.pageRoot` 优先;缺失、重复或非法标记失败。不按期望颜色、尺寸、CSS类猜选根节点。背景取该实际根的计算值,水平溢出同时检查该根和业务文档;运行证据保留源页面绑定与实际几何。测试阶段静态验证声明和绑定,实际渲染结果由产品阶段验证。
113
+
114
+ ### 空文字内容叶
115
+
116
+ 公开 `semantic-observation.mjs` 导出 `observeNativeEmptyText(node, {target, instance?})`,返回实际 `leafStyles`、`box`、`leafBox`、`clips`、`viewport`、身份与实例。`node` 由公开语义定位得到,`target` 是活动cmp。仅限选定组件独占一个原生 `.a-text`,其中一个SPAN内容叶(无子元素时原生实体自身为叶);拓扑不符明确失败,不按字号、颜色或业务名称选叶。原生dataset为身份权威,跨组件叶禁止借用。
117
+
118
+ 接口要求原始内容精确为空,拒绝空白/非空、缺失/重复、隐藏或透明祖先;允许空字形及零几何,但仍检查有限几何、祖先/视口裁剪。样式取真实内容叶而非外包装;用既有 `validateNativeTextStyles(e.leafStyles, expected, scale)` 比较源期望。采到样式本身不代表样式正确。
119
+
120
+ 业务行、兄弟组件、实例顺序、布局和源允许为空的条件仍由项目测试依据Spec声明,通过公开语义集合与所有权接口检查;不从cmp字符串推断业务路径,不把允许空字形扩展成普通文字可隐藏。MT只做静态及合成设施检查,MP负责实际产品运行。
@@ -9,15 +9,15 @@ import {compileCase} from '../compiler/case-plan.mjs';
9
9
  async function fixture(){
10
10
  const root=await mkdtemp(path.join(os.tmpdir(),'alternate-tool-project-')),project=path.join(root,'alternate'),configFile=path.join(project,'config.json');
11
11
  await mkdir(path.join(project,'harness/regions'),{recursive:true});await mkdir(path.join(project,'definitions'));await mkdir(path.join(project,'harness/plugin'));
12
- const config={schemaVersion:1,definitions:'definitions',catalog:'catalog.json',compilerAdapter:'harness/compiler.mjs',adapterRoots:['harness'],observationRoot:'harness',observationModules:['regions/alternate.mjs'],observationImports:['node:assert/strict','@antprofuse/saddle-islands-tools/tools/modules/minifish/harness/semantic-anchor.mjs'],pageRoute:'pages/alternate/view',pluginSource:'harness/plugin',repositoryRoot:'.',runtimeAdapters:{'@minifish/project-runtime':'harness/runtime.mjs','@minifish/project-host':'harness/host.mjs'}};
12
+ const config={schemaVersion:1,definitions:'definitions',catalog:'catalog.json',compilerAdapter:'harness/compiler.mjs',adapterRoots:['harness'],observationRoot:'harness',observationModules:['regions/alternate.mjs'],observationImports:['@antprofuse/saddle-islands-tools/tools/modules/minifish/harness/semantic-observation.mjs','node:assert/strict','@antprofuse/saddle-islands-tools/tools/modules/minifish/harness/semantic-anchor.mjs'],pageRoute:'pages/alternate/view',pluginSource:'harness/plugin',repositoryRoot:'.',runtimeAdapters:{'@minifish/project-runtime':'harness/runtime.mjs','@minifish/project-host':'harness/host.mjs'}};
13
13
  await writeFile(configFile,JSON.stringify(config));
14
14
  await writeFile(path.join(project,'harness/compiler.mjs'),'export function compile(){throw Error("synthetic compile adapter")}');
15
15
  await writeFile(path.join(project,'harness/runtime.mjs'),'export const prepareHeldSnapshot=()=>{},independentBoundary=()=>{},assertNativeState=()=>{},projectAssert=()=>{},locateProject=()=>{},queryBoundaryIds=()=>{},validateReleasedPaymentRows=()=>{};');
16
16
  await writeFile(path.join(project,'harness/host.mjs'),'export const hostPolicy={};');
17
- await writeFile(path.join(project,'harness/regions/alternate.mjs'),"import {semanticLocator} from '@antprofuse/saddle-islands-tools/tools/modules/minifish/harness/semantic-anchor.mjs';export function locate(root,name){return semanticLocator(root,name)};export function assertTarget(){}");
17
+ await writeFile(path.join(project,'harness/regions/alternate.mjs'),"import {semanticLocator} from '@antprofuse/saddle-islands-tools/tools/modules/minifish/harness/semantic-anchor.mjs';import {observeNativeEmptyText} from '@antprofuse/saddle-islands-tools/tools/modules/minifish/harness/semantic-observation.mjs';export function locate(root,name){return semanticLocator(root,name)};export function assertTarget(){}");
18
18
  return {root,project,configFile,config,loaded:await loadProject(project,configFile)};
19
19
  }
20
- test('alternate project static closure records tool/adapter separately and copies no unrelated region',async()=>{const f=await fixture();try{const output=path.join(f.root,'artifact'),records=await packageRunner(f.loaded,output,'regions/alternate.mjs');assert(records.some(r=>r.kind==='tool'&&r.path==='harness/semantic-anchor.mjs'));for(const name of ['component-membership.mjs','held-spatial.mjs','spatial-contract.mjs','region-order.mjs','visual-expectation.mjs','visual-carrier.mjs','jsapi.mjs','browser.mjs','official-bridge-dispatch.mjs']){assert(records.some(r=>r.kind==='tool'&&r.path==='harness/'+name));assert((await readFile(path.join(output,'tools/harness',name),'utf8')).length>0);}assert(records.some(r=>r.kind==='adapter'&&r.path==='harness/regions/alternate.mjs'));assert(records.every(r=>/^[a-f0-9]{64}$/.test(r.sha256)));assert(!records.some(r=>r.path.includes('T1')));assert((await readFile(path.join(output,'adapters/harness/regions/alternate.mjs'),'utf8')).includes('../../../tools/harness/semantic-anchor.mjs'));}finally{await rm(f.root,{recursive:true,force:true});}});
20
+ test('alternate project static closure records tool/adapter separately and copies no unrelated region',async()=>{const f=await fixture();try{const output=path.join(f.root,'artifact'),records=await packageRunner(f.loaded,output,'regions/alternate.mjs');assert(records.some(r=>r.kind==='tool'&&r.path==='harness/semantic-anchor.mjs'));for(const name of ['component-membership.mjs','held-spatial.mjs','spatial-contract.mjs','region-order.mjs','visual-expectation.mjs','visual-carrier.mjs','jsapi.mjs','browser.mjs','official-bridge-dispatch.mjs','empty-native-text.mjs']){assert(records.some(r=>r.kind==='tool'&&r.path==='harness/'+name));assert((await readFile(path.join(output,'tools/harness',name),'utf8')).length>0);}assert(records.some(r=>r.kind==='adapter'&&r.path==='harness/regions/alternate.mjs'));assert(records.every(r=>/^[a-f0-9]{64}$/.test(r.sha256)));assert(!records.some(r=>r.path.includes('T1')));assert((await readFile(path.join(output,'adapters/harness/regions/alternate.mjs'),'utf8')).includes('../../../tools/harness/semantic-anchor.mjs'));}finally{await rm(f.root,{recursive:true,force:true});}});
21
21
  for(const [name,code] of [['unknown package',"import 'fs';"],['dependency traversal',"import '../../outside.mjs';"],['direct DOM',"export const read=root=>root.evaluate(()=>1);"],['dynamic import',"export const read=()=>import('./other.mjs');"]])test(name+' is rejected from the closed observation dependency graph',async()=>{const f=await fixture();try{await writeFile(path.join(f.project,'harness/regions/alternate.mjs'),code);await writeFile(path.join(f.project,'outside.mjs'),'export const leaked=true;');await assert.rejects(packageRunner(f.loaded,path.join(f.root,'artifact'),'regions/alternate.mjs'));}finally{await rm(f.root,{recursive:true,force:true});}});
22
22
  test('symlink aliases, undeclared module, missing config, and package output are rejected',async()=>{const f=await fixture();try{const module=path.join(f.project,'harness/regions/alternate.mjs');await rm(module);await writeFile(path.join(f.project,'harness/other.mjs'),'export const safe=true;');await symlink('../other.mjs',module);await assert.rejects(packageRunner(f.loaded,path.join(f.root,'alias'),'regions/alternate.mjs'),/Symlink/);await assert.rejects(packageRunner(f.loaded,path.join(f.root,'unknown'),'regions/unknown.mjs'),/declared module/);await assert.rejects(loadProject(f.project,path.join(f.project,'missing.json')));await assert.rejects(packageRunner(f.loaded,path.join(f.project,'artifact'),'regions/alternate.mjs'),/outside/);}finally{await rm(f.root,{recursive:true,force:true});}});
23
23
  test('generic composition preserves exact preparation and original ID mappings without project names',()=>{const noop=()=>{},policy={validateSemanticTargets:noop,supplementalDefinition:()=>null,officialTitle:()=>false,validatePreparationOverrides:noop,validateFixtureTypes:noop,validateSpecialBoundaries:noop,validateIndependentFixture:noop,validatePacketPlan:noop,validatePluginEvents:noop,validatePluginAssertionIdentity:noop,validateObservationModuleName:noop};const catalog={cases:[{id:'alternate / page / value / content',title:'value'}]},definition={caseId:catalog.cases[0].id,ancestors:['open'],basis:[{path:'alternate/source.yaml'}],preparation:{},assertions:[{id:'value',kind:'text',target:'alternate / page / value',expected:'42',count:1}]},fragment={id:'open',inputs:{query:{key:'42'}},boundaries:[],actions:[{kind:'open-page'}]};const plan=compileCase(catalog,definition,[fragment],policy);assert.deepEqual(plan.inputs,fragment.inputs);assert.deepEqual(plan.assertions,definition.assertions);assert.equal(plan.case.id,definition.caseId);assert.throws(()=>compileCase(catalog,{...definition,ancestors:['unknown']},[fragment],policy),/missing preparation/);});
@@ -0,0 +1,38 @@
1
+ import assert from 'node:assert/strict';
2
+ // Platform topology only; never select by expected text, paint or business name.
3
+ export function sampleEmptyNativeText(root){
4
+ const doc=root.ownerDocument,win=doc.defaultView;
5
+ const data=(n,key)=>Object.prototype.hasOwnProperty.call(n,'$$dataset')?n.$$dataset?.[key]:n.getAttribute('data-'+key.replace(/[A-Z]/g,c=>'-'+c.toLowerCase()));
6
+ const identity=data(root,'anchorId');
7
+ if(typeof identity!=='string'||!/^cmp_[a-f0-9]{6}$/.test(identity))throw Error('Selected component identity required');
8
+ const declared=n=>Object.prototype.hasOwnProperty.call(n,'$$dataset')?Object.prototype.hasOwnProperty.call(n.$$dataset??{},'anchorId'):n.hasAttribute('data-anchor-id');
9
+ const parts=[root,...root.querySelectorAll('.a-text')].filter(n=>n.matches('.a-text'));
10
+ if(parts.length!==1)throw Error('One native text entity required');
11
+ const native=parts[0],leaves=[...native.querySelectorAll('*')].filter(n=>!n.children.length);
12
+ if(!leaves.length)leaves.push(native);
13
+ if(leaves.length!==1||(leaves[0]!==native&&leaves[0].tagName!=='SPAN'))throw Error('One native text content leaf required');
14
+ const leaf=leaves[0];
15
+ for(let n=leaf;n&&n!==root;n=n.parentElement)if(declared(n))throw Error('Foreign semantic text ownership');
16
+ if(root.textContent!=='')throw Error('Exact empty content required');
17
+ const rect=n=>{const b=n.getBoundingClientRect();return {x:b.x,y:b.y,width:b.width,height:b.height};};
18
+ let visible=true,opacity=1;const clips=[];
19
+ for(let n=leaf;n;n=n.parentElement){const s=win.getComputedStyle(n);visible&&=s.display!=='none'&&s.visibility==='visible';opacity*=Number(s.opacity);clips.push({...rect(n),clipX:/hidden|clip|auto|scroll/.test(s.overflowX),clipY:/hidden|clip|auto|scroll/.test(s.overflowY)});}
20
+ const s=win.getComputedStyle(leaf);
21
+ return {anchorId:identity,instance:data(root,'anchorInstance')??null,rawText:root.textContent,visible,opacity,box:rect(native),leafBox:rect(leaf),viewport:{x:0,y:0,width:win.innerWidth,height:win.innerHeight},clips,leafStyles:[Object.fromEntries(['fontFamily','fontSize','fontWeight','lineHeight','color'].map(k=>[k,s[k]]))]};
22
+ }
23
+ export function validateEmptyNativeText(e,{target,instance}={}){
24
+ assert.equal(e.anchorId,target,'Exact empty component identity');
25
+ if(instance!==undefined)assert.equal(String(e.instance),String(instance),'Exact empty component instance');
26
+ assert.equal(e.rawText,'','Exact empty content required');assert(e.visible&&e.opacity>0,'Empty native leaf must remain rendered');
27
+ for(const key of ['x','y','width','height'])assert(Number.isFinite(e.box[key]),'Finite empty text geometry');
28
+ assert(e.box.width>=0&&e.box.height>=0,'Empty text allows zero geometry, not negative geometry');
29
+ for(const b of [...e.clips,{...e.viewport,clipX:true,clipY:true}]){
30
+ if(b.clipX)assert(e.box.x>=b.x-.5&&e.box.x+e.box.width<=b.x+b.width+.5,'Empty text horizontal clipping');
31
+ if(b.clipY)assert(e.box.y>=b.y-.5&&e.box.y+e.box.height<=b.y+b.height+.5,'Empty text vertical clipping');
32
+ }
33
+ assert.equal(e.leafStyles.length,1,'Actual empty content leaf styles required');return e;
34
+ }
35
+ export async function observeEmptyNativeText(node,options){
36
+ assert.equal(await node.count(),1,'Unique empty component required');
37
+ return validateEmptyNativeText(await node.evaluate(sampleEmptyNativeText),options);
38
+ }
@@ -195,3 +195,4 @@ export async function nativeInnerBox(node){
195
195
  }
196
196
 
197
197
  export async function sampleTextVisibility(node){return node.evaluate(sampleNativeTextVisibility);}
198
+ export {observeEmptyNativeText as observeNativeEmptyText,sampleEmptyNativeText,validateEmptyNativeText} from './empty-native-text.mjs';