@floless/app 0.47.0 → 0.48.0

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.
@@ -52856,7 +52856,7 @@ function appVersion() {
52856
52856
  return resolveVersion({
52857
52857
  isSea: isSea2(),
52858
52858
  sqVersionXml: readSqVersionXml(),
52859
- define: true ? "0.47.0" : void 0,
52859
+ define: true ? "0.48.0" : void 0,
52860
52860
  pkgVersion: readPkgVersion()
52861
52861
  });
52862
52862
  }
@@ -52866,7 +52866,7 @@ function resolveChannel(s) {
52866
52866
  return "dev";
52867
52867
  }
52868
52868
  function appChannel() {
52869
- return resolveChannel({ isSea: isSea2(), define: true ? "0.47.0" : void 0 });
52869
+ return resolveChannel({ isSea: isSea2(), define: true ? "0.48.0" : void 0 });
52870
52870
  }
52871
52871
 
52872
52872
  // workflow-update.ts
@@ -54309,9 +54309,54 @@ function rollup(members, countWeighted = false) {
54309
54309
  }
54310
54310
  return { score: den > 0 ? Math.round(num2 / den * 100) : null, tons, counts };
54311
54311
  }
54312
+ function bandEnd(end, rows) {
54313
+ const row = end.conn ? rows.get(end.conn) : void 0;
54314
+ const complete = !!(row && row.type && row.detail && row.targets && Object.keys(row.targets).length > 0);
54315
+ const hasNote = !!(end.note && end.note.trim() !== "");
54316
+ const evidence = hasNote || row?.source === "drawing";
54317
+ if (end.connVerified === true) return { band: "verified", reason: "" };
54318
+ if (!end.conn && !hasNote) return { band: "rfi", reason: "no connection assigned" };
54319
+ if (complete && evidence) return { band: "high", reason: "" };
54320
+ if (complete) return { band: "med", reason: "connection type assumed (no drawing note)" };
54321
+ if (hasNote && end.detail && end.detail.trim() !== "") return { band: "med", reason: "free-text note + detail (not a mapped library row)" };
54322
+ if (end.conn && !row) return { band: "low", reason: "references an unknown connection row" };
54323
+ return { band: "low", reason: row ? "library row has a type only (no detail/component id)" : "connection type only (no detail)" };
54324
+ }
54325
+ function scoreConnections(contractInput) {
54326
+ const contract = contractInput ?? {};
54327
+ const rows = /* @__PURE__ */ new Map();
54328
+ for (const c of contract.connections ?? []) if (c && c.id) rows.set(c.id, c);
54329
+ const byEnd = [];
54330
+ (contract.plans ?? []).forEach((plan, planIdx) => {
54331
+ for (const m of plan.members ?? []) {
54332
+ if (m.role === "column") {
54333
+ const c = m.col ?? {};
54334
+ const { band, reason } = bandEnd(c, rows);
54335
+ byEnd.push({ id: `${m.id}#col`, planIdx, sheet: plan.sheet ?? "", memberId: m.id, which: "col", band, reason });
54336
+ } else {
54337
+ const ends = m.ends ?? [];
54338
+ for (const which of [0, 1]) {
54339
+ const { band, reason } = bandEnd(ends[which] ?? {}, rows);
54340
+ byEnd.push({ id: `${m.id}#${which}`, planIdx, sheet: plan.sheet ?? "", memberId: m.id, which, band, reason });
54341
+ }
54342
+ }
54343
+ }
54344
+ });
54345
+ const counts = emptyCounts();
54346
+ let num2 = 0, den = 0;
54347
+ for (const e of byEnd) {
54348
+ counts[e.band]++;
54349
+ if (e.band === "rfi") continue;
54350
+ num2 += BAND_WEIGHT[e.band];
54351
+ den += 1;
54352
+ }
54353
+ const category = { score: den > 0 ? Math.round(num2 / den * 100) : null, tons: 0, counts };
54354
+ return { byEnd, category };
54355
+ }
54312
54356
  function scoreContract(contractInput) {
54313
54357
  const contract = contractInput ?? {};
54314
54358
  const plans = contract.plans ?? [];
54359
+ const conn = scoreConnections(contract);
54315
54360
  const byMember = [];
54316
54361
  const byDetail = [];
54317
54362
  let totalSegments = 0;
@@ -54357,11 +54402,12 @@ function scoreContract(contractInput) {
54357
54402
  return {
54358
54403
  byMember,
54359
54404
  byDetail,
54405
+ byConnection: conn.byEnd,
54360
54406
  byCategory: {
54361
54407
  beams,
54362
54408
  columns,
54363
54409
  details,
54364
- connections: { score: null, note: "Not scored yet (reserved slot)" }
54410
+ connections: conn.category
54365
54411
  },
54366
54412
  coverage: {
54367
54413
  members: byMember.length,
@@ -38,6 +38,7 @@
38
38
  },
39
39
  "plans": { "type": "array", "items": { "$ref": "#/$defs/plan" } },
40
40
  "filter": { "$ref": "#/$defs/filter", "description": "Layer/thickness/colour filter pre-stage. `sheets[]` holds per-framing-plan geometry; the facets + `mode` + on-flag selection are GLOBAL across sheets. The saved selection scopes which lines the reader binds profile labels to per sheet; replaces the length heuristic, fallback when absent. CONFIDENTIAL: each sheet's `page.bg_b64` is machine-local, never committed." },
41
+ "connections": { "type": "array", "items": { "$ref": "#/$defs/connection" }, "description": "Vendor-neutral connection library: each row maps a connection type → the firm's design detail# → per-platform component ids. Member ends/columns reference a row by id via `ends[].conn` / `col.conn` (legacy `note`/`detail` = fallback when `conn` is empty)." },
41
42
  "dims3d": { "type": "array", "items": { "$ref": "#/$defs/dim3" }, "description": "Draft-only 3D dimensions (editor annotations, model-global). World-scene mm. NOT baked into the lock / 3D scene / IFC / BOM." }
42
43
  },
43
44
  "$defs": {
@@ -164,6 +165,19 @@
164
165
  }
165
166
  }
166
167
  },
168
+ "connection": {
169
+ "type": "object",
170
+ "required": ["id", "type"],
171
+ "additionalProperties": true,
172
+ "description": "One connection-library row. `detail` is the firm's VENDOR-NEUTRAL design detail#; `targets` is an open map platform→component id (tekla/sds2/revit/…), never hardcoded to one platform.",
173
+ "properties": {
174
+ "id": { "type": "string", "description": "Stable row id, e.g. cx-moment-1." },
175
+ "type": { "type": "string", "description": "moment | drag | shear | braced | pinned | fixed (free string allowed)." },
176
+ "detail": { "type": "string", "description": "Design detail number, e.g. 50S504 — the firm's document, platform-neutral." },
177
+ "targets": { "type": "object", "additionalProperties": { "type": "string" }, "description": "platform → component id, e.g. { \"tekla\": \"146\" }. Open map; add sds2/revit freely." },
178
+ "source": { "enum": ["company-rules", "user", "drawing"], "description": "Where the row came from (drives the connection confidence band)." }
179
+ }
180
+ },
167
181
  "point2": {
168
182
  "type": "array",
169
183
  "items": { "type": "number" },
@@ -313,7 +327,9 @@
313
327
  "tos": { "type": ["number", "null"], "description": "Top-of-steel elevation in inches at this end." },
314
328
  "note": { "type": "string", "description": "Connection note (e.g. moment, shear, braced)." },
315
329
  "tosDef": { "type": "boolean", "description": "True = follows the plan default; false = read from a drawing callout or set by hand." },
316
- "detail": { "type": "string", "description": "Connection detail reference, e.g. \"5-S504\"." }
330
+ "detail": { "type": "string", "description": "Connection detail reference, e.g. \"5-S504\"." },
331
+ "conn": { "type": "string", "description": "Connection-library row id this end uses (references `connections[].id`). Empty = fall back to `note`/`detail`." },
332
+ "connVerified": { "type": "boolean", "description": "True = a human confirmed this end's connection (scores Verified)." }
317
333
  }
318
334
  },
319
335
  "col": {
@@ -323,7 +339,9 @@
323
339
  "bos": { "type": ["number", "null"], "description": "Bottom-of-steel elevation in inches." },
324
340
  "tos": { "type": ["number", "null"], "description": "Top-of-steel elevation in inches." },
325
341
  "note": { "type": "string" },
326
- "detail": { "type": "string" }
342
+ "detail": { "type": "string" },
343
+ "conn": { "type": "string", "description": "Connection-library row id for this column's connection (references `connections[].id`)." },
344
+ "connVerified": { "type": "boolean", "description": "True = a human confirmed this column's connection (scores Verified)." }
327
345
  }
328
346
  }
329
347
  }
@@ -891,7 +891,7 @@ function panel(){
891
891
  else if(allBeam){const s0=beams.map(m=>m.ends[0]),s1=beams.map(m=>m.ends[1]);
892
892
  wMTos('tosA',s0);wMText('ntA',s0,(o,v)=>o.note=v);wMText('dtA',s0,(o,v)=>o.detail=v);
893
893
  wMTos('tosB',s1);wMText('ntB',s1,(o,v)=>o.note=v);wMText('dtB',s1,(o,v)=>o.detail=v);
894
- {const me=document.getElementById('matchEnds');if(me)me.onclick=()=>edit(()=>{for(const m of selArr())if(m.role!=='column')m.ends[1]={tos:m.ends[0].tos,note:m.ends[0].note,tosDef:m.ends[0].tosDef,detail:m.ends[0].detail};});}}
894
+ {const me=document.getElementById('matchEnds');if(me)me.onclick=()=>edit(()=>{for(const m of selArr())if(m.role!=='column')m.ends[1]={tos:m.ends[0].tos,note:m.ends[0].note,tosDef:m.ends[0].tosDef,detail:m.ends[0].detail,conn:m.ends[0].conn,connVerified:m.ends[0].connVerified};});}}
895
895
  return;}
896
896
  const m=ensureMeta(arr[0]), wpf=WT[m.profile], L=(len(m.wp[0],m.wp[1])/FT).toFixed(1), col=(m.role==='column'), pos=posDefault(m);
897
897
  const _lvl=({'S-202':'second','S-203':'roof'})[P.sheet];
@@ -951,7 +951,7 @@ function panel(){
951
951
  wireTos('tosB',m.ends[1]);
952
952
  document.getElementById('ntB').onchange=e=>edit(()=>{m.ends[1].note=e.target.value;});
953
953
  wireDet('dtB',m.ends[1]);
954
- document.getElementById('matchEnds').onclick=()=>edit(()=>{m.ends[1]={tos:m.ends[0].tos,note:m.ends[0].note,tosDef:m.ends[0].tosDef,detail:m.ends[0].detail};});
954
+ document.getElementById('matchEnds').onclick=()=>edit(()=>{m.ends[1]={tos:m.ends[0].tos,note:m.ends[0].note,tosDef:m.ends[0].tosDef,detail:m.ends[0].detail,conn:m.ends[0].conn,connVerified:m.ends[0].connVerified};});
955
955
  }
956
956
  document.getElementById('del').onclick=()=>edit(()=>{P.members=P.members.filter(x=>x.id!==m.id);selIds.clear();if(selDimIds.size){P.dims=P.dims.filter(d=>!selDimIds.has(d.id));selDimIds.clear();}}); // also drop any dims a mixed marquee selected
957
957
  {const vb=document.getElementById('verifyBtn');if(vb)vb.onclick=()=>edit(()=>{m.verified=!m.verified;});}
@@ -1527,7 +1527,29 @@ function _mTons(m,ptPerFt){const plf=_wt(m.profile);if(plf==null)return 0;let ft
1527
1527
  return ft*plf/2000;}
1528
1528
  function _detSheet(t){const m=String(t||'').toUpperCase().match(/S-?\s?\d{2,3}/);return m?m[0].replace(/\s/g,''):null;}
1529
1529
  const _cnt0=()=>({verified:0,high:0,med:0,low:0,rfi:0});
1530
+ function _scoreConnections(){
1531
+ const rows={};for(const c of (C.connections||[]))if(c&&c.id)rows[c.id]=c;
1532
+ const bandEnd=(end)=>{const row=end.conn?rows[end.conn]:null;
1533
+ const complete=!!(row&&row.type&&row.detail&&row.targets&&Object.keys(row.targets).length>0);
1534
+ const hasNote=!!(end.note&&end.note.trim()!=='');
1535
+ // Only a drawing callout counts as evidence — a company-rules/user row assignment is a mapping, not drawing-confirmed.
1536
+ const evidence=hasNote||(row&&row.source==='drawing');
1537
+ if(end.connVerified===true)return{band:'verified',reason:''};
1538
+ if(!end.conn&&!hasNote)return{band:'rfi',reason:'no connection assigned'};
1539
+ if(complete&&evidence)return{band:'high',reason:''};
1540
+ if(complete)return{band:'med',reason:'connection type assumed (no drawing note)'};
1541
+ if(hasNote&&end.detail&&end.detail.trim()!=='')return{band:'med',reason:'free-text note + detail (not a mapped library row)'};
1542
+ if(end.conn&&!row)return{band:'low',reason:'references an unknown connection row'};
1543
+ return{band:'low',reason:row?'library row has a type only (no detail/component id)':'connection type only (no detail)'};};
1544
+ const byEnd=[];
1545
+ (C.plans||[]).forEach((plan,pi)=>{for(const m of (plan.members||[])){
1546
+ if(m.role==='column'){const r=bandEnd(m.col||{});byEnd.push({id:m.id+'#col',pi,sheet:plan.sheet||'',memberId:m.id,which:'col',band:r.band,reason:r.reason});}
1547
+ else{const ends=m.ends||[];for(const w of [0,1]){const r=bandEnd(ends[w]||{});byEnd.push({id:m.id+'#'+w,pi,sheet:plan.sheet||'',memberId:m.id,which:w,band:r.band,reason:r.reason});}}}});
1548
+ const counts=_cnt0();let num=0,den=0;
1549
+ for(const e of byEnd){counts[e.band]++;if(e.band==='rfi')continue;num+=BANDW[e.band];den+=1;}
1550
+ return{cat:{score:den>0?Math.round(num/den*100):null,tons:0,counts},ends:byEnd};}
1530
1551
  function scoreContractJS(){const plans=C.plans||[];const byMember=[],byDetail=[];let segs=0;
1552
+ const _conn=_scoreConnections();
1531
1553
  const known=new Set();for(const p of plans)if(p.sheet)known.add(p.sheet.toUpperCase());for(const s of Object.keys(C.detail_bubbles||{}))known.add(s.toUpperCase());
1532
1554
  plans.forEach((plan,pi)=>{segs+=(plan.segments||[]).length;const ms=plan.members||[];const dup=_confDupIds(ms);
1533
1555
  for(const m of ms){const r=_scoreMember(m,dup);byMember.push({id:m.id,planIdx:pi,sheet:plan.sheet||'',role:m.role==='column'?'column':'beam',profile:m.profile||'',band:r.band,tons:r.band==='rfi'?0:_mTons(m,plan.pt_per_ft),factors:r.factors});}
@@ -1536,7 +1558,7 @@ function scoreContractJS(){const plans=C.plans||[];const byMember=[],byDetail=[]
1536
1558
  const dRoll=arr=>{const c=_cnt0();let n=0,d=0;for(const x of arr){c[x.band]++;n+=BANDW[x.band];d++;}return {score:d>0?Math.round(n/d*100):null,tons:0,counts:c};};
1537
1559
  const beams=roll(byMember.filter(m=>m.role==='beam'));const columns=roll(byMember.filter(m=>m.role==='column'));const details=dRoll(byDetail);
1538
1560
  const sc=byMember.filter(m=>m.band!=='rfi');const n=sc.reduce((s,m)=>s+m.tons*BANDW[m.band],0),d=sc.reduce((s,m)=>s+m.tons,0);
1539
- return {byMember,byDetail,byCategory:{beams,columns,details,connections:{score:null,note:'Not scored yet (reserved slot)',counts:_cnt0()}},coverage:{members:byMember.length,segments:segs,note:'Approximate in v1 — precise per-segment binding is Slice 2.'},overall:{score:d>0?Math.round(n/d*100):null,tons:d,rfiCount:byMember.filter(m=>m.band==='rfi').length}};}
1561
+ return {byMember,byDetail,byConnection:_conn.ends,byCategory:{beams,columns,details,connections:_conn.cat},coverage:{members:byMember.length,segments:segs,note:'Approximate in v1 — precise per-segment binding is Slice 2.'},overall:{score:d>0?Math.round(n/d*100):null,tons:d,rfiCount:byMember.filter(m=>m.band==='rfi').length}};}
1540
1562
  function bandColorForPct(p){return p==null?'var(--mut)':p>=95?BANDC.verified:p>=80?BANDC.high:p>=50?BANDC.med:BANDC.low;}
1541
1563
  // Effective target: the user's per-read override (in the contract) wins; else the app's
1542
1564
  // target_confidence input default (read in boot()); else none. Editing the override never touches
@@ -1569,7 +1591,7 @@ function renderConf(){const s=scoreContractJS();const cat=s.byCategory;
1569
1591
  '<div class=cc-score style="color:'+(stub?'var(--mut)':bandColorForPct(cs.score))+'">'+(stub||cs.score==null?'—':cs.score+'%')+'</div>'+
1570
1592
  _bar(stub?null:cs.score)+
1571
1593
  '<div class=cc-counts>'+(stub?'<span>reserved slot</span>':_countsHtml(cs.counts))+'</div></div>';
1572
- document.getElementById('confCats').innerHTML=card('beams','Beams',cat.beams)+card('columns','Columns',cat.columns)+card('details','Details',cat.details)+card('connections','Connections',cat.connections,true);
1594
+ document.getElementById('confCats').innerHTML=card('beams','Beams',cat.beams)+card('columns','Columns',cat.columns)+card('details','Details',cat.details)+card('connections','Connections',cat.connections);
1573
1595
  const bands=['all','verified','high','med','low','rfi'],cats=['all','beams','columns','details','connections'];
1574
1596
  document.getElementById('confFilter').innerHTML=
1575
1597
  '<span class=glab>Band</span><span class=grp>'+bands.map(b=>'<button data-band="'+b+'" class="'+(confBand===b?'on':'')+'">'+(b==='all'?'All':BANDLAB[b])+'</button>').join('')+'</span>'+
@@ -1579,7 +1601,21 @@ function renderConf(){const s=scoreContractJS();const cat=s.byCategory;
1579
1601
  wireConf();}
1580
1602
  function confBodyHtml(s){
1581
1603
  if(!s.byMember.length&&!s.byDetail.length)return '<div class=hint style="padding:14px 0">No members in the takeoff yet.</div>';
1582
- if(confCat==='connections')return '<div class=hint style="padding:14px 0">Connection scoring is not built yet (reserved slot).</div>';
1604
+ if(confCat==='connections'){
1605
+ // worst-first so the RFIs / unmapped ends surface at the top (the "what to fix next" view)
1606
+ const _ord={rfi:0,low:1,med:2,high:3,verified:4};
1607
+ let conn=s.byConnection.slice().sort((a,b)=>_ord[a.band]-_ord[b.band]);
1608
+ if(confBand!=='all')conn=conn.filter(e=>e.band===confBand);
1609
+ if(!conn.length)return '<div class=hint style="padding:14px 0">Nothing matches this filter.</div>';
1610
+ const whichLabel=w=>w==='col'?'Column':w===0?'End 1':'End 2';
1611
+ let h='<table class=ftab><thead><tr><th>Band</th><th>Member</th><th>End</th><th>Sheet</th><th>Reason</th><th></th></tr></thead><tbody>';
1612
+ conn.forEach(e=>{h+='<tr><td><span class=pill style="'+BANDPILL[e.band]+'">'+BANDLAB[e.band]+'</span></td>'+
1613
+ '<td>'+esc(e.memberId)+'</td>'+
1614
+ '<td>'+whichLabel(e.which)+'</td>'+
1615
+ '<td>'+esc(e.sheet)+'</td>'+
1616
+ '<td><span class=hint>'+esc(e.reason||'—')+'</span></td>'+
1617
+ '<td><button class=ghost data-loc="'+esc(e.memberId)+'" data-pi="'+e.pi+'">Locate</button></td></tr>';});
1618
+ h+='</tbody></table>';return h;}
1583
1619
  let mem=s.byMember.slice();
1584
1620
  if(confCat==='beams')mem=mem.filter(m=>m.role==='beam');else if(confCat==='columns')mem=mem.filter(m=>m.role==='column');else if(confCat==='details')mem=[];
1585
1621
  if(confBand!=='all')mem=mem.filter(m=>m.band===confBand);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floless/app",
3
- "version": "0.47.0",
3
+ "version": "0.48.0",
4
4
  "type": "module",
5
5
  "description": "Thin localhost host for floless.app — serves web/ and shells the aware CLI. No engine, no LLM.",
6
6
  "bin": {