@antprofuse/saddle-islands-tools 0.1.9 → 0.1.13

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 (32) hide show
  1. package/package.json +1 -1
  2. package/tools/adapters/islands/component_reference.py +62 -0
  3. package/tools/adapters/islands/visual_targets.py +106 -0
  4. package/tools/cli/main.py +2 -2
  5. package/tools/cli/minifish.mjs +1 -1
  6. package/tools/cli/minifish.py +13 -1
  7. package/tools/docs/minifish-testing.md +53 -1
  8. package/tools/docs/semantic-identity.md +3 -3
  9. package/tools/modules/minifish/anchor_check.py +108 -0
  10. package/tools/modules/minifish/anchor_templates.py +31 -0
  11. package/tools/modules/minifish/checks/package.test.mjs +1 -1
  12. package/tools/modules/minifish/compiler/anchor-coverage.mjs +37 -0
  13. package/tools/modules/minifish/compiler/anchor-values-cli.mjs +16 -0
  14. package/tools/modules/minifish/compiler/anchor-values.mjs +56 -0
  15. package/tools/modules/minifish/compiler/build-case.mjs +7 -1
  16. package/tools/modules/minifish/compiler/compile-check.mjs +16 -3
  17. package/tools/modules/minifish/compiler/component-contract.mjs +37 -0
  18. package/tools/modules/minifish/compiler/config.mjs +1 -1
  19. package/tools/modules/minifish/component_index.py +192 -0
  20. package/tools/modules/minifish/component_index_bridge.py +28 -0
  21. package/tools/modules/minifish/harness/browser.mjs +14 -3
  22. package/tools/modules/minifish/harness/component-membership.mjs +32 -0
  23. package/tools/modules/minifish/harness/component-state.mjs +43 -0
  24. package/tools/modules/minifish/harness/held-snapshot.mjs +23 -6
  25. package/tools/modules/minifish/harness/held-spatial.mjs +25 -0
  26. package/tools/modules/minifish/harness/independent-case-runner.mjs +17 -3
  27. package/tools/modules/minifish/harness/jsapi.mjs +4 -1
  28. package/tools/modules/minifish/harness/official-bridge-dispatch.mjs +20 -0
  29. package/tools/modules/minifish/harness/spatial-contract.mjs +20 -0
  30. package/tools/modules/minifish/harness/visual-carrier.mjs +7 -0
  31. package/tools/modules/minifish/harness/visual-expectation.mjs +1 -1
  32. package/tools/modules/workflow/compile_gate.py +29 -1
@@ -0,0 +1,20 @@
1
+ import {createHash} from 'node:crypto';
2
+ import assert from 'node:assert/strict';
3
+
4
+ // Compatibility adapter for this exact official runtime, not an SDK public API.
5
+ // The original hook eligibility and native delegation stay byte-for-byte intact.
6
+ export const dispatchRuntimeSha256='a7401f68d3e27235736e29a3c33865ed01260e239c2853ead5482fcab63e15b8';
7
+ export const dispatchFragment='(e||!r||h||A)&&n.preventDefault()';
8
+ export function ownsMockInvocation(api,option,managed,officialUi){
9
+ return managed.includes(api)&&!officialUi.includes(api)
10
+ &&!(api==='pushWindow'&&typeof option?.url==='string'&&option.url.startsWith('#'));
11
+ }
12
+ export function adaptOfficialBridgeDispatch(source,{managed,officialUi}){
13
+ for(const list of [managed,officialUi])assert(Array.isArray(list)&&list.every(x=>typeof x==='string'&&x.length),'Explicit API lists required');
14
+ const sha256=createHash('sha256').update(source).digest('hex');
15
+ assert.equal(sha256,dispatchRuntimeSha256,'Unsupported official bridge runtime: refusing compatibility adaptation');
16
+ assert.equal(source.split(dispatchFragment).length,2,'Official dispatch fragment must be unique');
17
+ const condition=`(${ownsMockInvocation.toString()})(n.apiName,n.option,${JSON.stringify([...new Set(managed)])},${JSON.stringify([...new Set(officialUi)])})`;
18
+ const body=source.replace(dispatchFragment,`(e||!r||h||A||${condition})&&n.preventDefault()`);
19
+ return {body,evidence:{adapter:'mock-default-dispatch-v1',sourceSha256:sha256,adaptedSha256:createHash('sha256').update(body).digest('hex'),managed:[...new Set(managed)],officialUi:[...new Set(officialUi)]}};
20
+ }
@@ -0,0 +1,20 @@
1
+ import assert from 'node:assert/strict';
2
+ const keyOf=(target,instance)=>JSON.stringify([target,instance===undefined?{unspecified:true}:instance]);
3
+ export function spatialTarget(ref){
4
+ assert(ref&&Object.keys(ref).every(k=>['target','instance','carrier'].includes(k)),'Unknown spatial target field');
5
+ assert(/^cmp_[0-9a-f]{6}$/.test(ref.target),'Allocated spatial component reference required');
6
+ assert(ref.instance===undefined||(Number.isSafeInteger(ref.instance)&&ref.instance>=0),'Invalid spatial instance');
7
+ assert(ref.carrier===undefined||['semantic-root','exclusive-parent'].includes(ref.carrier),'Unsupported spatial carrier');
8
+ const key=ref.carrier==='exclusive-parent'?JSON.stringify([ref.target,ref.instance===undefined?{unspecified:true}:ref.instance,ref.carrier]):keyOf(ref.target,ref.instance);
9
+ return {...ref,key,styleKeys:[],waitForPresence:false};
10
+ }
11
+ export function validateSpatial(spec){
12
+ assert(spec&&Object.keys(spec).every(k=>['order','absent'].includes(k)),'Unknown spatial obligation');
13
+ for(const name of ['order','absent'])assert(spec[name]===undefined||Array.isArray(spec[name]),'Spatial obligation must be an array');
14
+ assert((spec.order?.length??0)+(spec.absent?.length??0)>0,'Empty spatial obligation');
15
+ for(const pair of spec.order??[]){
16
+ assert(pair&&Object.keys(pair).length===2&&pair.above&&pair.below,'Order pair required');
17
+ assert.notEqual(spatialTarget(pair.above).key,spatialTarget(pair.below).key,'Order requires distinct targets');
18
+ }
19
+ for(const ref of spec.absent??[])spatialTarget(ref);
20
+ }
@@ -1,6 +1,13 @@
1
1
  /** Browser-side, expectation-independent selection inside one semantic root. */
2
2
  export function resolveVisualCarrier(root, kind = 'semantic-root') {
3
3
  if(kind==='semantic-root')return root;
4
+ if(kind==='exclusive-parent'){
5
+ const parent=root.parentElement;
6
+ const marker=n=>Object.prototype.hasOwnProperty.call(n,'$$dataset')?n.$$dataset?.anchorId:n.getAttribute('data-anchor-id');
7
+ if(!marker(root)||!parent||['BODY','HTML'].includes(parent.tagName)||marker(parent))throw Error('Exclusive parent requires an unanchored non-document wrapper');
8
+ if(parent.children.length!==1||parent.firstElementChild!==root||[...parent.childNodes].some(n=>n.nodeType===3&&n.textContent.trim()))throw Error('Wrapper must exclusively own the component root');
9
+ return parent;
10
+ }
4
11
  if(!['source-content-group','owned-text-block'].includes(kind))throw Error('Unsupported visual carrier');
5
12
  const visible=element=>{
6
13
  for(let p=element;p;p=p.parentElement){const s=getComputedStyle(p);if(s.display==='none'||s.visibility!=='visible'||Number(s.opacity)===0)return false;}
@@ -23,7 +23,7 @@ export function validateVisualExpectation(value) {
23
23
  for (const key of Object.keys(value)) assert(top.has(key), `visual: unsupported ${key}`);
24
24
  number(value.designWidth,'visual.designWidth');
25
25
  assert(value.designWidth > 0,'visual.designWidth must be positive');
26
- if(value.carrier!==undefined)assert(['semantic-root','source-content-group','owned-text-block'].includes(value.carrier),'visual: unsupported carrier');
26
+ if(value.carrier!==undefined)assert(['semantic-root','source-content-group','owned-text-block','exclusive-parent'].includes(value.carrier),'visual: unsupported carrier');
27
27
  let checks = 0;
28
28
  if(value.viewportInsets!==undefined)checks+=keys(value.viewportInsets,new Set(['left','right','top','bottom']),'visual.viewportInsets',number);
29
29
  if (value.text !== undefined) {
@@ -4,6 +4,7 @@ import json
4
4
  from pathlib import Path
5
5
  from .iteration import Invalid
6
6
  from .diff_review import git
7
+ from tools.modules.minifish.component_index import parse_registry,validate
7
8
 
8
9
 
9
10
  def verify_compile(repo, head, bizdir, receipt_file):
@@ -14,7 +15,7 @@ def verify_compile(repo, head, bizdir, receipt_file):
14
15
  def check(ok, message):
15
16
  if not ok:
16
17
  raise Invalid(message)
17
- check(r.get('schemaVersion') == 1 and r.get('kind') == 'minifish-full-compile', 'Invalid compile receipt')
18
+ check(r.get('schemaVersion') == 2 and r.get('kind') == 'minifish-full-compile', 'Invalid compile receipt')
18
19
  check(r.get('head') == head and r.get('projectPath') == project, 'Stale or wrong-project compile receipt')
19
20
  check(r.get('projectTree') == git(repo, 'rev-parse', head + ':' + project).decode().strip(), 'Compile tree mismatch')
20
21
  def read_bound(key):
@@ -46,6 +47,33 @@ def verify_compile(repo, head, bizdir, receipt_file):
46
47
  covered += [x['sourceCaseId'] for x in pending]
47
48
  check(len(covered) == len(set(covered)) and len(official) == len(set(official)) and set(covered) == set(official), 'Incomplete catalog coverage')
48
49
  check(r.get('nonExecutable') == [{'id': x['sourceCaseId'], 'reason': x['reason']} for x in pending], 'Nonexecutable decisions mismatch')
50
+ inventory = read_bound('anchorInventory')
51
+ check(isinstance(config.get('anchorInventory'), dict), 'Missing configured source component inventory')
52
+ check(r['anchorInventory']['path'] == project + '/' + config['anchorInventory']['path'] and
53
+ r['anchorInventory']['sha256'] == config['anchorInventory']['sha256'], 'Unexpected source component inventory')
54
+ registry = r.get('registry', {})
55
+ registry_values=[]
56
+ for name in ('anchors.jsonc','anchors-retired.jsonc'):
57
+ entry=registry.get(name,{})
58
+ expected=bizdir+'/code/.registry/'+name
59
+ check(entry.get('path')==expected,'Unexpected registry location')
60
+ mode=git(repo,'ls-tree',head,'--',expected).decode()
61
+ check(mode.startswith('100644 '),'Registry must be a regular committed file')
62
+ raw=git(repo,'show',head+':'+expected)
63
+ check(hashlib.sha256(raw).hexdigest()==entry.get('sha256'),'Registry hash mismatch')
64
+ try: registry_values.append(parse_registry(raw.decode()))
65
+ except ValueError as e: raise Invalid(str(e)) from e
66
+ try: active=validate(registry_values[0],inventory,registry_values[1])
67
+ except ValueError as e: raise Invalid(str(e)) from e
68
+ source_ids=list(active)
69
+ check(all(isinstance(x.get('componentIds'), list) and all(isinstance(i, str) for i in x['componentIds']) for x in results), 'Missing compiled assertion identities')
70
+ asserted = sorted({i for row in results for i in row['componentIds']})
71
+ check(asserted == sorted(source_ids), 'Assertion component set differs from source')
72
+ coverage = r.get('anchorCoverage', {})
73
+ check(coverage.get('identityContract') == 'allocated-component-id' and
74
+ coverage.get('activeComponentIds') == sorted(source_ids) and
75
+ coverage.get('assertedComponentIds') == asserted and
76
+ coverage.get('passed') is True and coverage.get('missing') == [] and coverage.get('extra') == [], 'Invalid anchor coverage result')
49
77
  check(r.get('productExecution') is False, 'Not a static compilation receipt')
50
78
  check(r.get('total') == len(rows) and r.get('passed') == len(rows) and r.get('failed') == 0 and all(x.get('status') == 'passed' for x in results), 'Full compilation did not pass')
51
79
  return f"\n\nMT全量编译:{len(rows)}/{len(rows)} 通过;非执行责任 {len(pending)} 项保留独立处置。\n"