@floless/app 0.57.0 → 0.59.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.
@@ -53022,7 +53022,7 @@ function appVersion() {
53022
53022
  return resolveVersion({
53023
53023
  isSea: isSea2(),
53024
53024
  sqVersionXml: readSqVersionXml(),
53025
- define: true ? "0.57.0" : void 0,
53025
+ define: true ? "0.59.0" : void 0,
53026
53026
  pkgVersion: readPkgVersion()
53027
53027
  });
53028
53028
  }
@@ -53032,7 +53032,7 @@ function resolveChannel(s) {
53032
53032
  return "dev";
53033
53033
  }
53034
53034
  function appChannel() {
53035
- return resolveChannel({ isSea: isSea2(), define: true ? "0.57.0" : void 0 });
53035
+ return resolveChannel({ isSea: isSea2(), define: true ? "0.59.0" : void 0 });
53036
53036
  }
53037
53037
 
53038
53038
  // workflow-update.ts
@@ -53325,6 +53325,9 @@ function extractReportHtml(events) {
53325
53325
  return found;
53326
53326
  }
53327
53327
 
53328
+ // aisc-shapes.json
53329
+ var aisc_shapes_default = { "C10X15.3": 2.6, C10X20: 2.74, C10X25: 2.89, C10X30: 3.03, "C12X20.7": 2.94, C12X25: 3.05, C12X30: 3.17, "C15X33.9": 3.4, C15X40: 3.52, C15X50: 3.72, "C3X3.5": 1.37, "C3X4.1": 1.41, C3X5: 1.5, C3X6: 1.6, "C4X4.5": 1.52, "C4X5.4": 1.58, "C4X6.25": 1.65, "C4X7.25": 1.72, "C5X6.7": 1.75, C5X9: 1.89, "C6X10.5": 2.03, C6X13: 2.16, "C6X8.2": 1.92, "C7X12.25": 2.19, "C7X14.75": 2.3, "C7X9.8": 2.09, "C8X11.5": 2.26, "C8X13.75": 2.34, "C8X18.75": 2.53, "C9X13.4": 2.43, C9X15: 2.49, C9X20: 2.65, HP10X42: 10.1, HP10X57: 10.2, HP12X53: 12, HP12X63: 12.1, HP12X74: 12.2, HP12X84: 12.3, HP12X89: 12.3, HP14X102: 14.8, HP14X117: 14.9, HP14X73: 14.6, HP14X89: 14.7, HP16X101: 15.8, HP16X121: 15.9, HP16X141: 16, HP16X162: 16.1, HP16X183: 16.3, HP16X88: 15.7, HP18X135: 17.8, HP18X157: 17.9, HP18X181: 18, HP18X204: 18.1, HP8X36: 8.16, "M10X7.5": 2.69, M10X8: 2.69, M10X9: 2.69, "M12.5X11.6": 3.5, "M12.5X12.4": 3.75, M12X10: 3.25, "M12X10.8": 3.07, "M12X11.8": 3.07, "M3X2.9": 2.25, "M4X3.2": 2.25, "M4X3.45": 2.25, "M4X4.08": 2.25, M4X6: 3.8, "M5X18.9": 5, "M6X3.7": 2, "M6X4.4": 1.84, "M8X6.2": 2.28, "M8X6.5": 2.28, MC10X22: 3.32, MC10X25: 3.41, "MC10X28.5": 3.95, "MC10X33.6": 4.1, "MC10X41.1": 4.32, "MC10X6.5": 1.17, "MC10X8.4": 1.5, "MC12X10.6": 1.5, "MC12X14.3": 2.12, MC12X31: 3.67, MC12X35: 3.77, MC12X40: 3.89, MC12X45: 4.01, MC12X50: 4.14, "MC13X31.8": 4, MC13X35: 4.07, MC13X40: 4.19, MC13X50: 4.41, "MC18X42.7": 3.95, "MC18X45.8": 4, "MC18X51.9": 4.1, MC18X58: 4.2, "MC3X7.1": 1.94, "MC4X13.8": 2.5, MC6X12: 2.5, "MC6X15.1": 2.94, "MC6X15.3": 3.5, "MC6X16.3": 3, MC6X18: 3.5, "MC6X6.5": 1.85, MC6X7: 1.88, "MC7X19.1": 3.45, "MC7X22.7": 3.6, "MC8X18.7": 2.98, MC8X20: 3.03, "MC8X21.4": 3.45, "MC8X22.8": 3.5, "MC8X8.5": 1.87, "MC9X23.9": 3.45, "MC9X25.4": 3.5, "MT2.5X9.45": 5, MT2X3: 3.8, "MT3X1.85": 2, "MT3X2.2": 1.84, "MT4X3.1": 2.28, "MT4X3.25": 2.28, "MT5X3.75": 2.69, MT5X4: 2.69, "MT5X4.5": 2.69, "MT6.25X5.8": 3.5, "MT6.25X6.2": 3.75, MT6X5: 3.25, "MT6X5.4": 3.07, "MT6X5.9": 3.07, "S10X25.4": 4.66, S10X35: 4.94, "S12X31.8": 5, S12X35: 5.08, "S12X40.8": 5.25, S12X50: 5.48, "S15X42.9": 5.5, S15X50: 5.64, "S18X54.7": 6, S18X70: 6.25, S20X66: 6.26, S20X75: 6.39, S20X86: 7.06, S20X96: 7.2, S24X100: 7.25, S24X106: 7.87, S24X121: 8.05, S24X80: 7, S24X90: 7.13, "S3X5.7": 2.33, "S3X7.5": 2.51, "S4X7.7": 2.66, "S4X9.5": 2.8, S5X10: 3, "S6X12.5": 3.33, "S6X17.25": 3.57, "S8X18.4": 4, S8X23: 4.17, "ST1.5X2.85": 2.33, "ST1.5X3.75": 2.51, ST10X33: 6.26, "ST10X37.5": 6.39, ST10X43: 7.06, ST10X48: 7.2, ST12X40: 7, ST12X45: 7.13, ST12X50: 7.25, ST12X53: 7.87, "ST12X60.5": 8.05, "ST2.5X5": 3, "ST2X3.85": 2.66, "ST2X4.75": 2.8, "ST3X6.25": 3.33, "ST3X8.6": 3.57, "ST4X11.5": 4.17, "ST4X9.2": 4, "ST5X12.7": 4.66, "ST5X17.5": 4.94, "ST6X15.9": 5, "ST6X17.5": 5.08, "ST6X20.4": 5.25, ST6X25: 5.48, "ST7.5X21.45": 5.5, "ST7.5X25": 5.64, "ST9X27.35": 6, ST9X35: 6.25, W10X100: 10.3, W10X112: 10.4, W10X12: 3.96, W10X15: 4, W10X17: 4.01, W10X19: 4.02, W10X22: 5.75, W10X26: 5.77, W10X30: 5.81, W10X33: 7.96, W10X39: 7.99, W10X45: 8.02, W10X49: 10, W10X54: 10, W10X60: 10.1, W10X68: 10.1, W10X77: 10.2, W10X88: 10.3, W12X106: 12.2, W12X120: 12.3, W12X136: 12.4, W12X14: 3.97, W12X152: 12.5, W12X16: 3.99, W12X170: 12.6, W12X19: 4.01, W12X190: 12.7, W12X210: 12.8, W12X22: 4.03, W12X230: 12.9, W12X252: 13, W12X26: 6.49, W12X279: 13.1, W12X30: 6.52, W12X305: 13.2, W12X336: 13.4, W12X35: 6.56, W12X40: 8.01, W12X45: 8.05, W12X50: 8.08, W12X53: 10, W12X58: 10, W12X65: 12, W12X72: 12, W12X79: 12.1, W12X87: 12.1, W12X96: 12.2, W14X109: 14.6, W14X120: 14.7, W14X132: 14.7, W14X145: 15.5, W14X159: 15.6, W14X176: 15.7, W14X193: 15.7, W14X211: 15.8, W14X22: 5, W14X233: 15.9, W14X257: 16, W14X26: 5.03, W14X283: 16.1, W14X30: 6.73, W14X311: 16.2, W14X34: 6.75, W14X342: 16.4, W14X370: 16.5, W14X38: 6.77, W14X398: 16.6, W14X426: 16.7, W14X43: 8, W14X455: 16.8, W14X48: 8.03, W14X500: 17, W14X53: 8.06, W14X550: 17.2, W14X605: 17.4, W14X61: 10, W14X665: 17.7, W14X68: 10, W14X730: 17.9, W14X74: 10.1, W14X808: 18.6, W14X82: 10.1, W14X873: 18.8, W14X90: 14.5, W14X99: 14.6, W16X100: 10.4, W16X26: 5.5, W16X31: 5.53, W16X36: 6.99, W16X40: 7, W16X45: 7.04, W16X50: 7.07, W16X57: 7.12, W16X67: 10.2, W16X77: 10.3, W16X89: 10.4, W18X106: 11.2, W18X119: 11.3, W18X130: 11.2, W18X143: 11.2, W18X158: 11.3, W18X175: 11.4, W18X192: 11.5, W18X211: 11.6, W18X234: 11.7, W18X258: 11.8, W18X283: 11.9, W18X311: 12, W18X35: 6, W18X40: 6.02, W18X46: 6.06, W18X50: 7.5, W18X55: 7.53, W18X60: 7.56, W18X65: 7.59, W18X71: 7.64, W18X76: 11, W18X86: 11.1, W18X97: 11.1, W21X101: 12.3, W21X111: 12.3, W21X122: 12.4, W21X132: 12.4, W21X147: 12.5, W21X166: 12.4, W21X182: 12.5, W21X201: 12.6, W21X223: 12.7, W21X248: 12.8, W21X275: 12.9, W21X44: 6.5, W21X48: 8.14, W21X50: 6.53, W21X55: 8.22, W21X57: 6.56, W21X62: 8.24, W21X68: 8.27, W21X73: 8.3, W21X83: 8.36, W21X93: 8.42, W24X103: 9, W24X104: 12.8, W24X117: 12.8, W24X131: 12.9, W24X146: 12.9, W24X162: 13, W24X176: 12.9, W24X192: 13, W24X207: 13, W24X229: 13.1, W24X250: 13.2, W24X279: 13.3, W24X306: 13.4, W24X335: 13.5, W24X370: 13.7, W24X55: 7.01, W24X62: 7.04, W24X68: 8.97, W24X76: 8.99, W24X84: 9.02, W24X94: 9.07, W27X102: 10, W27X114: 10.1, W27X129: 10, W27X146: 14, W27X161: 14, W27X178: 14.1, W27X194: 14, W27X217: 14.1, W27X235: 14.2, W27X258: 14.3, W27X281: 14.4, W27X307: 14.4, W27X336: 14.6, W27X368: 14.7, W27X539: 15.3, W27X84: 10, W27X94: 10, W30X108: 10.5, W30X116: 10.5, W30X124: 10.5, W30X132: 10.5, W30X148: 10.5, W30X173: 15, W30X191: 15, W30X211: 15.1, W30X235: 15.1, W30X261: 15.2, W30X292: 15.3, W30X326: 15.4, W30X357: 15.5, W30X391: 15.6, W30X90: 10.4, W30X99: 10.5, W33X118: 11.5, W33X130: 11.5, W33X141: 11.5, W33X152: 11.6, W33X169: 11.5, W33X201: 15.7, W33X221: 15.8, W33X241: 15.9, W33X263: 15.8, W33X291: 15.9, W33X318: 16, W33X354: 16.1, W33X387: 16.2, W36X135: 12, W36X150: 12, W36X160: 12, W36X170: 12, W36X182: 12.1, W36X194: 12.1, W36X210: 12.2, W36X231: 16.5, W36X232: 12.1, W36X247: 16.5, W36X256: 12.2, W36X262: 16.6, W36X282: 16.6, W36X302: 16.7, W36X330: 16.6, W36X361: 16.7, W36X395: 16.8, W36X441: 17, W36X487: 17.1, W36X529: 17.2, W36X652: 17.6, W36X723: 17.8, W36X802: 18, W36X853: 18.2, W36X925: 18.6, W40X149: 11.8, W40X167: 11.8, W40X183: 11.8, W40X199: 15.8, W40X211: 11.8, W40X215: 15.8, W40X235: 11.9, W40X249: 15.8, W40X264: 11.9, W40X277: 15.8, W40X278: 12, W40X294: 12, W40X297: 15.8, W40X324: 15.9, W40X327: 12.1, W40X331: 12.2, W40X362: 16, W40X372: 16.1, W40X392: 12.4, W40X397: 16.1, W40X431: 16.2, W40X503: 16.4, W40X593: 16.7, W40X655: 16.9, W44X230: 15.8, W44X262: 15.8, W44X290: 15.8, W44X335: 15.9, W4X13: 4.06, W5X16: 5, W5X19: 5.03, W6X12: 4, W6X15: 5.99, W6X16: 4.03, W6X20: 6.02, W6X25: 6.08, "W6X8.5": 3.94, W6X9: 3.94, W8X10: 3.94, W8X13: 4, W8X15: 4.02, W8X18: 5.25, W8X21: 5.27, W8X24: 6.5, W8X28: 6.54, W8X31: 8, W8X35: 8.02, W8X40: 8.07, W8X48: 8.11, W8X58: 8.22, W8X67: 8.28, "WT10.5X100.5": 12.6, "WT10.5X111.5": 12.7, "WT10.5X124": 12.8, "WT10.5X137.5": 12.9, "WT10.5X22": 6.5, "WT10.5X24": 8.14, "WT10.5X25": 6.53, "WT10.5X27.5": 8.22, "WT10.5X28.5": 6.56, "WT10.5X31": 8.24, "WT10.5X34": 8.27, "WT10.5X36.5": 8.3, "WT10.5X41.5": 8.36, "WT10.5X46.5": 8.42, "WT10.5X50.5": 12.3, "WT10.5X55.5": 12.3, "WT10.5X61": 12.4, "WT10.5X66": 12.4, "WT10.5X73.5": 12.5, "WT10.5X83": 12.4, "WT10.5X91": 12.5, "WT12X103.5": 13, "WT12X114.5": 13.1, WT12X125: 13.2, "WT12X139.5": 13.3, WT12X153: 13.4, "WT12X167.5": 13.5, WT12X185: 13.7, "WT12X27.5": 7.01, WT12X31: 7.04, WT12X34: 8.97, WT12X38: 8.99, WT12X42: 9.02, WT12X47: 9.07, "WT12X51.5": 9, WT12X52: 12.8, "WT12X58.5": 12.8, "WT12X65.5": 12.9, WT12X73: 12.9, WT12X81: 13, WT12X88: 12.9, WT12X96: 13, "WT13.5X108.5": 14.1, "WT13.5X117.5": 14.2, "WT13.5X129": 14.3, "WT13.5X140.5": 14.4, "WT13.5X153.5": 14.4, "WT13.5X168": 14.6, "WT13.5X184": 14.7, "WT13.5X269.5": 15.3, "WT13.5X42": 10, "WT13.5X47": 10, "WT13.5X51": 10, "WT13.5X57": 10.1, "WT13.5X64.5": 10, "WT13.5X73": 14, "WT13.5X80.5": 14, "WT13.5X89": 14.1, "WT13.5X97": 14, "WT15X105.5": 15.1, "WT15X117.5": 15.1, "WT15X130.5": 15.2, WT15X146: 15.3, WT15X163: 15.4, "WT15X178.5": 15.5, "WT15X195.5": 15.6, WT15X45: 10.4, "WT15X49.5": 10.5, WT15X54: 10.5, WT15X58: 10.5, WT15X62: 10.5, WT15X66: 10.5, WT15X74: 10.5, "WT15X86.5": 15, "WT15X95.5": 15, "WT16.5X100.5": 15.7, "WT16.5X110.5": 15.8, "WT16.5X120.5": 15.9, "WT16.5X131.5": 15.8, "WT16.5X145.5": 15.9, "WT16.5X159": 16, "WT16.5X177": 16.1, "WT16.5X193.5": 16.2, "WT16.5X59": 11.5, "WT16.5X65": 11.5, "WT16.5X70.5": 11.5, "WT16.5X76": 11.6, "WT16.5X84.5": 11.5, WT18X105: 12.2, "WT18X115.5": 16.5, WT18X116: 12.1, "WT18X123.5": 16.5, WT18X128: 12.2, WT18X131: 16.6, WT18X141: 16.6, WT18X151: 16.7, WT18X165: 16.6, "WT18X180.5": 16.7, "WT18X197.5": 16.8, "WT18X220.5": 17, "WT18X243.5": 17.1, "WT18X264.5": 17.2, WT18X326: 17.6, "WT18X361.5": 17.8, WT18X401: 18, "WT18X426.5": 18.2, "WT18X462.5": 18.6, "WT18X67.5": 12, WT18X75: 12, WT18X80: 12, WT18X85: 12, WT18X91: 12.1, WT18X97: 12.1, "WT2.5X8": 5, "WT2.5X9.5": 5.03, "WT20X105.5": 11.8, "WT20X107.5": 15.8, "WT20X117.5": 11.9, "WT20X124.5": 15.8, WT20X132: 11.9, "WT20X138.5": 15.8, WT20X139: 12, WT20X147: 12, "WT20X148.5": 15.8, WT20X162: 15.9, "WT20X163.5": 12.1, "WT20X165.5": 12.2, WT20X181: 16, WT20X186: 16.1, WT20X196: 12.4, "WT20X198.5": 16.1, "WT20X215.5": 16.2, "WT20X251.5": 16.4, "WT20X296.5": 16.7, "WT20X327.5": 16.9, "WT20X74.5": 11.8, "WT20X83.5": 11.8, "WT20X91.5": 11.8, "WT20X99.5": 15.8, WT22X115: 15.8, WT22X131: 15.8, WT22X145: 15.8, "WT22X167.5": 15.9, "WT2X6.5": 4.06, WT3X10: 6.02, "WT3X12.5": 6.08, "WT3X4.25": 3.94, "WT3X4.5": 3.94, WT3X6: 4, "WT3X7.5": 5.99, WT3X8: 4.03, "WT4X10.5": 5.27, WT4X12: 6.5, WT4X14: 6.54, "WT4X15.5": 8, "WT4X17.5": 8.02, WT4X20: 8.07, WT4X24: 8.11, WT4X29: 8.22, "WT4X33.5": 8.28, WT4X5: 3.94, "WT4X6.5": 4, "WT4X7.5": 4.02, WT4X9: 5.25, WT5X11: 5.75, WT5X13: 5.77, WT5X15: 5.81, "WT5X16.5": 7.96, "WT5X19.5": 7.99, "WT5X22.5": 8.02, "WT5X24.5": 10, WT5X27: 10, WT5X30: 10.1, WT5X34: 10.1, "WT5X38.5": 10.2, WT5X44: 10.3, WT5X50: 10.3, WT5X56: 10.4, WT5X6: 3.96, "WT5X7.5": 4, "WT5X8.5": 4.01, "WT5X9.5": 4.02, WT6X105: 12.8, WT6X11: 4.03, WT6X115: 12.9, WT6X126: 13, WT6X13: 6.49, "WT6X139.5": 13.1, WT6X15: 6.52, "WT6X152.5": 13.2, WT6X168: 13.4, "WT6X17.5": 6.56, WT6X20: 8.01, "WT6X22.5": 8.05, WT6X25: 8.08, "WT6X26.5": 10, WT6X29: 10, "WT6X32.5": 12, WT6X36: 12, "WT6X39.5": 12.1, "WT6X43.5": 12.1, WT6X48: 12.2, WT6X53: 12.2, WT6X60: 12.3, WT6X68: 12.4, WT6X7: 3.97, WT6X76: 12.5, WT6X8: 3.99, WT6X85: 12.6, "WT6X9.5": 4.01, WT6X95: 12.7, "WT7X105.5": 15.8, WT7X11: 5, "WT7X116.5": 15.9, "WT7X128.5": 16, WT7X13: 5.03, "WT7X141.5": 16.1, WT7X15: 6.73, "WT7X155.5": 16.2, WT7X17: 6.75, WT7X171: 16.4, WT7X185: 16.5, WT7X19: 6.77, WT7X199: 16.6, "WT7X21.5": 8, WT7X213: 16.7, "WT7X227.5": 16.8, WT7X24: 8.03, WT7X250: 17, "WT7X26.5": 8.06, WT7X275: 17.2, "WT7X30.5": 10, "WT7X302.5": 17.4, "WT7X332.5": 17.7, WT7X34: 10, WT7X365: 17.9, WT7X37: 10.1, WT7X404: 18.6, WT7X41: 10.1, "WT7X436.5": 18.8, WT7X45: 14.5, "WT7X49.5": 14.6, "WT7X54.5": 14.6, WT7X60: 14.7, WT7X66: 14.7, "WT7X72.5": 15.5, "WT7X79.5": 15.6, WT7X88: 15.7, "WT7X96.5": 15.7, WT8X13: 5.5, "WT8X15.5": 5.53, WT8X18: 6.99, WT8X20: 7, "WT8X22.5": 7.04, WT8X25: 7.07, "WT8X28.5": 7.12, "WT8X33.5": 10.2, "WT8X38.5": 10.3, "WT8X44.5": 10.4, WT8X50: 10.4, "WT9X105.5": 11.6, WT9X117: 11.7, WT9X129: 11.8, "WT9X141.5": 11.9, "WT9X155.5": 12, "WT9X17.5": 6, WT9X20: 6.02, WT9X23: 6.06, WT9X25: 7.5, "WT9X27.5": 7.53, WT9X30: 7.56, "WT9X32.5": 7.59, "WT9X35.5": 7.64, WT9X38: 11, WT9X43: 11.1, "WT9X48.5": 11.1, WT9X53: 11.2, "WT9X59.5": 11.3, WT9X65: 11.2, "WT9X71.5": 11.2, WT9X79: 11.3, "WT9X87.5": 11.4, WT9X96: 11.5 };
53330
+
53328
53331
  // index.ts
53329
53332
  var import_node_crypto7 = require("node:crypto");
53330
53333
 
@@ -54002,6 +54005,9 @@ function expandBasePlate(joint, col) {
54002
54005
  const rows = intMin1(p, "boltRows", 2);
54003
54006
  const weldLeg = num(p, "weldLeg", 8);
54004
54007
  const projBelow = num(p, "projBelow", 75);
54008
+ const embedment = Math.max(0, num(p, "embedment", 0));
54009
+ const grout = Math.max(0, num(p, "grout", 0));
54010
+ const rodBelow = embedment > 0 ? grout + embedment : projBelow;
54005
54011
  const nutH = pos(p, "nutHeight", 0.8 * boltDia);
54006
54012
  const nutAF = pos(p, "nutAcrossFlats", 1.6 * boltDia);
54007
54013
  const wpSide = pos(p, "washerPlateSide", 2.5 * boltDia);
@@ -54016,14 +54022,16 @@ function expandBasePlate(joint, col) {
54016
54022
  const vs = spread(rows, Math.max(0, depth / 2 - edge));
54017
54023
  const holes = [];
54018
54024
  const kit = [];
54025
+ const wpBot = plateTop + SEAT, wpTop = wpBot + wpThick, washerTop = wpTop + washerH, fnTop = washerTop + nutH;
54026
+ const rodTopZ = fnTop + 0.4 * boltDia, rodBotZ = plateBottom - rodBelow;
54027
+ const concreteTopZ = plateBottom - (embedment > 0 ? grout : 0);
54019
54028
  let k = 0;
54020
54029
  for (const v of vs) {
54021
54030
  for (const u of us) {
54022
54031
  const ax = bx + u, ay = by + v, jid = joint.id;
54023
54032
  holes.push({ u, v, d: boltDia + clearance });
54024
- const wpBot = plateTop + SEAT, wpTop = wpBot + wpThick, washerTop = wpTop + washerH, fnTop = washerTop + nutH;
54025
54033
  kit.push(
54026
- { id: `${jid}:rod${k}`, group: "anchor", kind: "rod", from: [ax, ay, plateBottom - projBelow], to: [ax, ay, fnTop + 0.4 * boltDia], d: boltDia, meta: { label: `Anchor rod \u2300${boltDia}` } },
54034
+ { id: `${jid}:rod${k}`, group: "anchor", kind: "rod", from: [ax, ay, rodBotZ], to: [ax, ay, rodTopZ], d: boltDia, meta: { label: `Anchor rod \u2300${boltDia}` } },
54027
54035
  { id: `${jid}:wp${k}`, group: "washer-plate", kind: "plate", center: [ax, ay, wpBot + wpThick / 2], uDir: [1, 0, 0], vDir: [0, 1, 0], width: wpSide, depth: wpSide, thickness: wpThick, holes: [{ u: 0, v: 0, d: boltDia + 2 }], meta: { label: `Washer plate ${Math.round(wpSide)}\xD7${Math.round(wpSide)}\xD7${Math.round(wpThick)}` } },
54028
54036
  { id: `${jid}:wsh${k}`, group: "washer", kind: "washer", from: [ax, ay, wpTop], to: [ax, ay, washerTop], dOuter: washerOD, dInner: washerID, meta: { label: `Washer \u2300${Math.round(washerOD)}` } },
54029
54037
  { id: `${jid}:fn${k}`, group: "nut", kind: "nut", from: [ax, ay, washerTop], to: [ax, ay, fnTop], acrossFlats: nutAF, meta: { label: `Nut M${boltDia}` } }
@@ -54044,7 +54052,12 @@ function expandBasePlate(joint, col) {
54044
54052
  depth,
54045
54053
  thickness,
54046
54054
  holes,
54047
- meta: { label: `Base plate ${Math.round(width)}\xD7${Math.round(depth)}\xD7${Math.round(thickness)}` }
54055
+ meta: {
54056
+ label: `Base plate ${Math.round(width)}\xD7${Math.round(depth)}\xD7${Math.round(thickness)}`,
54057
+ // anchor depth for the view's PROJ/GROUT/EMBED overlay + the rod inspector. projection is above B.O.
54058
+ // plate; the grout pad sits B.O. plate → T.O. concrete; embedment is INTO the concrete (T.O. concrete → rod bottom).
54059
+ anchor: { rodTopZ, rodBotZ, plateBotZ: plateBottom, concreteTopZ, projMm: rodTopZ - plateBottom, groutMm: plateBottom - concreteTopZ, embedMm: concreteTopZ - rodBotZ, embedSet: embedment > 0 }
54060
+ }
54048
54061
  };
54049
54062
  const weld = {
54050
54063
  id: `${joint.id}:weld`,
@@ -54059,6 +54072,33 @@ function expandBasePlate(joint, col) {
54059
54072
  function webHalfThk(profile, w) {
54060
54073
  return profileKind(profile) === "I" ? Math.min(w * 0.5, Math.max(w * 0.1, 5)) / 2 : 0;
54061
54074
  }
54075
+ function distPointSeg(p, a, b) {
54076
+ const abx = b[0] - a[0], aby = b[1] - a[1], abz = b[2] - a[2];
54077
+ const apx = p[0] - a[0], apy = p[1] - a[1], apz = p[2] - a[2];
54078
+ const len2 = abx * abx + aby * aby + abz * abz;
54079
+ const t = len2 > 1e-9 ? Math.max(0, Math.min(1, (apx * abx + apy * aby + apz * abz) / len2)) : 0;
54080
+ return Math.hypot(apx - t * abx, apy - t * aby, apz - t * abz);
54081
+ }
54082
+ function unitDir(a, b) {
54083
+ const dx = b[0] - a[0], dy = b[1] - a[1], dz = b[2] - a[2];
54084
+ const L = Math.hypot(dx, dy, dz);
54085
+ return L > 1e-9 ? [dx / L, dy / L, dz / L] : null;
54086
+ }
54087
+ function inferSupport(beam, end, memberGeo) {
54088
+ const bd = unitDir(beam.from, beam.to);
54089
+ let best, bestD = Infinity;
54090
+ for (const m of memberGeo.values()) {
54091
+ if (m.id === beam.id || !finite3(m.from) || !finite3(m.to)) continue;
54092
+ const md = unitDir(m.from, m.to);
54093
+ if (bd && md && Math.abs(bd[0] * md[0] + bd[1] * md[1] + bd[2] * md[2]) > 0.966) continue;
54094
+ const d = distPointSeg(end, m.from, m.to);
54095
+ if (d < bestD) {
54096
+ bestD = d;
54097
+ best = m;
54098
+ }
54099
+ }
54100
+ return best && bestD <= Math.max(best.dims.w, best.dims.d) ? best : void 0;
54101
+ }
54062
54102
  function flangeThk(profile, d) {
54063
54103
  return profileKind(profile) === "I" ? Math.min(d * 0.5, Math.max(d * 0.1, 6)) : d / 2;
54064
54104
  }
@@ -54135,6 +54175,7 @@ function expandShearPlate(joint, beam, support) {
54135
54175
  const twHalf = webHalfThk(beam.profile, beam.dims.w);
54136
54176
  const webSide = num(p, "webSide", 1) >= 0 ? 1 : -1;
54137
54177
  const pOff = webSide * (twHalf + thickness / 2);
54178
+ const u0 = support ? webHalfThk(support.profile, support.dims.w) : 0;
54138
54179
  const edgeV = Math.min(edge, height * 0.4);
54139
54180
  const pV = rows > 1 ? Math.min(pitch, Math.max(0, height - 2 * edgeV) / (rows - 1)) : 0;
54140
54181
  const vOffs = Array.from({ length: rows }, (_, i) => (i - (rows - 1) / 2) * pV);
@@ -54162,9 +54203,9 @@ function expandShearPlate(joint, beam, support) {
54162
54203
  holes.push({ u: u - width / 2, v, d: boltDia + holeClr });
54163
54204
  bolts.push(
54164
54205
  // the shank = the AISC bolt LENGTH, from under the head (plate outer face) into the connection
54165
- { id: `${jid}:bolt${k}`, group: "bolt", kind: "rod", from: P(u, v, plateOuterN), to: P(u, v, plateOuterN - webSide * boltLen), d: boltDia, meta: { label: `Bolt ${callout}` } },
54166
- { id: `${jid}:head${k}`, group: "nut", kind: "nut", from: P(u, v, plateOuterN), to: P(u, v, headTipN), acrossFlats: nutAF, meta: { label: `Bolt head ${callout}` } },
54167
- { id: `${jid}:nut${k}`, group: "nut", kind: "nut", from: P(u, v, webFarN), to: P(u, v, nutTipN), acrossFlats: nutAF, meta: { label: `Bolt nut ${callout}` } }
54206
+ { id: `${jid}:bolt${k}`, group: "bolt", kind: "rod", from: P(u0 + u, v, plateOuterN), to: P(u0 + u, v, plateOuterN - webSide * boltLen), d: boltDia, meta: { label: `Bolt ${callout}` } },
54207
+ { id: `${jid}:head${k}`, group: "nut", kind: "nut", from: P(u0 + u, v, plateOuterN), to: P(u0 + u, v, headTipN), acrossFlats: nutAF, meta: { label: `Bolt head ${callout}` } },
54208
+ { id: `${jid}:nut${k}`, group: "nut", kind: "nut", from: P(u0 + u, v, webFarN), to: P(u0 + u, v, nutTipN), acrossFlats: nutAF, meta: { label: `Bolt nut ${callout}` } }
54168
54209
  );
54169
54210
  k++;
54170
54211
  }
@@ -54173,26 +54214,33 @@ function expandShearPlate(joint, beam, support) {
54173
54214
  id: `${joint.id}:plate`,
54174
54215
  group: "shear-plate",
54175
54216
  kind: "plate",
54176
- center: P(width / 2, 0, pOff),
54217
+ center: P(u0 + width / 2, 0, pOff),
54177
54218
  uDir,
54178
54219
  vDir,
54179
- // welded edge at u=0 (the support); lapped on the webSide face; cantilevered +uDir toward the beam
54220
+ // welded edge at the support web FACE (u0); lapped on the webSide face; cantilevered +uDir toward the beam
54180
54221
  width,
54181
54222
  depth: height,
54182
54223
  thickness,
54183
54224
  holes,
54184
54225
  meta: { label: `Shear plate ${Math.round(width)}\xD7${Math.round(height)}\xD7${Math.round(thickness)}` }
54185
54226
  };
54186
- const weld = {
54187
- id: `${joint.id}:weld`,
54227
+ const weldSide = p?.["weldSide"] === "near" || p?.["weldSide"] === "far" ? p["weldSide"] : "both";
54228
+ const nOuter = pOff + webSide * thickness / 2;
54229
+ const nInner = pOff - webSide * thickness / 2;
54230
+ const faceWeld = (suffix, n) => ({
54231
+ id: `${joint.id}:${suffix}`,
54188
54232
  group: "weld",
54189
54233
  kind: "weld",
54190
54234
  closed: false,
54191
- path: [P(0, -height / 2, pOff), P(0, height / 2, pOff)],
54235
+ path: [P(u0, -height / 2, n), P(u0, height / 2, n)],
54192
54236
  leg: weldLeg,
54193
- meta: { label: `Fillet ${weldLeg}` }
54194
- };
54195
- const parts = [plate, ...bolts, weld];
54237
+ // at the support web FACE
54238
+ meta: { label: `Fillet ${weldLeg} (${weldSide === "both" ? "both sides" : weldSide + " side"})` }
54239
+ });
54240
+ const welds = [];
54241
+ if (weldSide !== "far") welds.push(faceWeld("weld", nOuter));
54242
+ if (weldSide !== "near") welds.push(faceWeld("weld2", nInner));
54243
+ const parts = [plate, ...bolts, ...welds];
54196
54244
  if (stiffener) {
54197
54245
  if (support && support.role === "beam" && finite3(support.from) && finite3(support.to) && support.dims && support.dims.d > 0 && support.dims.w > 0) {
54198
54246
  const supPosOff = typeof support.posOffset === "number" && isFinite(support.posOffset) ? support.posOffset : 0;
@@ -54207,8 +54255,8 @@ function expandShearPlate(joint, beam, support) {
54207
54255
  parts.push({ id: `${joint.id}:stiffweld`, group: "weld", kind: "weld", closed: false, path: [[wfx, wfy, girderCentZ - stiffH / 2], [wfx, wfy, girderCentZ + stiffH / 2]], leg: weldLeg, meta: { label: `Fillet ${weldLeg}` } });
54208
54256
  } else {
54209
54257
  const oppOff = -webSide * (twHalf + thickness / 2);
54210
- parts.push({ id: `${joint.id}:stiff`, group: "stiffener", kind: "plate", center: P(width / 2, 0, oppOff), uDir, vDir, width, depth: height, thickness, holes: [], meta: { label: `Stiffener ${Math.round(width)}\xD7${Math.round(height)}\xD7${Math.round(thickness)}` } });
54211
- parts.push({ id: `${joint.id}:stiffweld`, group: "weld", kind: "weld", closed: false, path: [P(0, -height / 2, oppOff), P(0, height / 2, oppOff)], leg: weldLeg, meta: { label: `Fillet ${weldLeg}` } });
54258
+ parts.push({ id: `${joint.id}:stiff`, group: "stiffener", kind: "plate", center: P(u0 + width / 2, 0, oppOff), uDir, vDir, width, depth: height, thickness, holes: [], meta: { label: `Stiffener ${Math.round(width)}\xD7${Math.round(height)}\xD7${Math.round(thickness)}` } });
54259
+ parts.push({ id: `${joint.id}:stiffweld`, group: "weld", kind: "weld", closed: false, path: [P(u0, -height / 2, oppOff), P(u0, height / 2, oppOff)], leg: weldLeg, meta: { label: `Fillet ${weldLeg}` } });
54212
54260
  }
54213
54261
  }
54214
54262
  const cope = beamCopes(beam, support, joint.at ?? "end0", p);
@@ -54230,6 +54278,8 @@ function expandShearPlate(joint, beam, support) {
54230
54278
  depth: dec.depth,
54231
54279
  thickness: beam.dims.w + 2,
54232
54280
  radius: copeRadius,
54281
+ reentrantV: side === "top" ? -1 : 1,
54282
+ // web side = toward the beam centroid (top cope below, bottom above)
54233
54283
  meta: { label: `${side === "top" ? "Top" : "Bottom"} cope ${Math.round(dec.length)}\xD7${Math.round(dec.depth)}` }
54234
54284
  });
54235
54285
  };
@@ -54270,7 +54320,8 @@ function expandJoints(joints, memberGeo) {
54270
54320
  skipped.push(j.id);
54271
54321
  return;
54272
54322
  }
54273
- const support = j.secondaries && j.secondaries[0] ? memberGeo.get(j.secondaries[0]) : void 0;
54323
+ let support = j.secondaries && j.secondaries[0] ? memberGeo.get(j.secondaries[0]) : void 0;
54324
+ if (!support) support = inferSupport(beam, j.at === "end1" ? beam.to : beam.from, memberGeo);
54274
54325
  const parts = expandShearPlate(j, beam, support);
54275
54326
  if (!parts.length) {
54276
54327
  skipped.push(j.id);
@@ -54289,6 +54340,7 @@ function expandJoints(joints, memberGeo) {
54289
54340
  }
54290
54341
 
54291
54342
  // contract-to-scene.ts
54343
+ var BF_IN = aisc_shapes_default;
54292
54344
  var FT_TO_MM = 304.8;
54293
54345
  var IN_TO_MM = 25.4;
54294
54346
  function depthPosition(explicit, dzMm, horizMm) {
@@ -54313,6 +54365,8 @@ function profileDims(profile) {
54313
54365
  }
54314
54366
  if (/^(W|S|M|HP|C|MC|WT|ST|MT)$/.test(prefix)) {
54315
54367
  const d = nums[0] * IN_TO_MM;
54368
+ const bf = BF_IN[p.replace(/\s+/g, "")];
54369
+ if (bf !== void 0) return { d, w: bf * IN_TO_MM, approx: false };
54316
54370
  const ratio = prefix === "C" || prefix === "MC" ? 0.28 : 0.42;
54317
54371
  return { d, w: d * ratio, approx: true };
54318
54372
  }
@@ -59140,6 +59194,14 @@ function rollupDetails(details) {
59140
59194
  }
59141
59195
 
59142
59196
  // contract-score.ts
59197
+ function isFilterMissing(contract) {
59198
+ const f = contract?.filter;
59199
+ if (!f || typeof f !== "object") return true;
59200
+ const sheets = f.sheets;
59201
+ if (Array.isArray(sheets) && sheets.length > 0) return false;
59202
+ if (f.page) return false;
59203
+ return true;
59204
+ }
59143
59205
  var ORDER = { rfi: 0, low: 1, med: 2, high: 3, verified: 4 };
59144
59206
  var factor = (factors, key, state2) => factors.find((f) => f.key === key && (state2 === void 0 || f.state === state2));
59145
59207
  function reasonFor(m) {
@@ -59177,7 +59239,8 @@ function scoreContract2(contractInput) {
59177
59239
  members: s.byMember.length,
59178
59240
  rfiCount: s.overall.rfiCount,
59179
59241
  counts,
59180
- worklist
59242
+ worklist,
59243
+ filterMissing: isFilterMissing(contractInput)
59181
59244
  };
59182
59245
  }
59183
59246
 
@@ -63396,6 +63459,7 @@ async function startServer() {
63396
63459
  const apps = await aware.list();
63397
63460
  return { ok: true, apps: apps.map((a) => ({ ...a, provider: appProvider(a.id), baked: appBaked(a.id), ...appOrigin(a.id) })) };
63398
63461
  });
63462
+ app.get("/api/aisc-shapes", async () => ({ ok: true, shapes: Object.keys(aisc_shapes_default) }));
63399
63463
  app.get("/api/workflows/updates", async () => {
63400
63464
  const apps = await aware.list();
63401
63465
  const installed = [];
@@ -64445,7 +64509,9 @@ async function startServer() {
64445
64509
  const REBAKE_INSTRUCTION = "Re-read the attached drawing and re-bake the extracted values into this app's config.yaml (the literals the deterministic run uses), then `aware app compile` the app and tell me to approve the new lock. Do not change node logic \u2014 only the baked config values.";
64446
64510
  app.post(
64447
64511
  "/api/rebake",
64448
- { bodyLimit: 25 * 1024 * 1024 },
64512
+ // A drawing SET is now multiple files (up to 8) — a single 25 MB cap would 413 a legit multi-sheet
64513
+ // rebake. 96 MB fits several large structural PDFs (base64-inflated); localhost, single-user, transient.
64514
+ { bodyLimit: 96 * 1024 * 1024 },
64449
64515
  async (req, reply) => {
64450
64516
  const { appId, inputName, instruction, snapshots } = req.body ?? {};
64451
64517
  const sourceName = typeof req.body?.sourceName === "string" ? req.body.sourceName : void 0;
@@ -2,7 +2,7 @@
2
2
  name: floless-app-rebake
3
3
  description: This skill should be used when the user swaps the source drawing of a BAKED floless.app workflow and wants it re-read — the "Re-read & re-bake" (B3) loop for Visual Inputs. Triggers on a pending floless `rebake` request (queued from the app's "Re-read & re-bake ▸" button), or asks like "re-bake screenshot-to-saved-settings with this new schedule", "I swapped the drawing, re-read it", "re-extract and re-bake the config". It teaches the host AI to read the new image, re-extract its values, rewrite ONLY the app's config.yaml baked literals, recompile, and ask the user to approve — all at COMPOSE time (AWARE forbids reading a drawing during a run).
4
4
  metadata:
5
- version: 0.1.0
5
+ version: 0.2.0
6
6
  ---
7
7
 
8
8
  # Re-read & re-bake a baked Visual Input (B3)
@@ -31,10 +31,12 @@ up here.
31
31
 
32
32
  1. **Find the request.** `GET http://localhost:<port>/api/requests` → find the entry with
33
33
  `type: "rebake"`. It carries `appId`, the `inputName` (the baked input, e.g. `drawing`),
34
- an `instruction`, and `snapshots: ["<abs path to the new image>"]`. (The user may also just
35
- ask in prose with an image attached — same procedure, skip the request lookup.)
36
- 2. **Read the new drawing.** Open the image at `snapshots[0]` (a real PNG/JPEG/WebP on disk).
37
- Read it with your own vision this is the compose-time extraction.
34
+ an `instruction`, and `snapshots: ["<abs path>", …]` **one OR MORE** new files (the user can
35
+ attach a set). (The user may also just ask in prose with image(s) attached — same procedure, skip
36
+ the request lookup.)
37
+ 2. **Read the new drawing(s).** Open **every** file in `snapshots[]` (real PNG/JPEG/WebP/PDF on
38
+ disk) — not just the first. Read them with your own vision as one combined set — this is the
39
+ compose-time extraction.
38
40
  3. **Re-extract the same schema the app already bakes.** Open the app source +
39
41
  `config.yaml` under `~/.aware/apps/<appId>/` (and the editable copy under `demos/<appId>/`
40
42
  if present). Identify the baked literals (e.g. `config.schedule-rows`). Re-extract **the same
@@ -2,7 +2,7 @@
2
2
  name: floless-app-steel-takeoff
3
3
  description: Read a structural steel drawing into an editable steel.takeoff/v1 contract in floless.app — the "Steel Takeoff" reader. Triggers on a pending floless `rebake` request for the `steel-model` app (queued from its "Re-read & re-bake ▸" button), or asks like "read this framing plan into a takeoff", "turn this structural drawing into a takeoff", "bake my drawing into steel-model", "extract members from this PDF". Teaches the host AI to READ a steel drawing (PDF/image) with its own vision at COMPOSE time, build a steel.takeoff/v1 contract (members + profiles + elevations + weights from AISC lookup), write it to the contract store, and hand control to the editor — no model in the run path, no API key.
4
4
  metadata:
5
- version: 0.2.0
5
+ version: 0.3.0
6
6
  ---
7
7
 
8
8
  # Steel Takeoff — read a drawing into a steel.takeoff/v1 contract
@@ -31,15 +31,29 @@ but do not attempt them here.
31
31
 
32
32
  ## The loop
33
33
 
34
- ### 1. Find the drawing
34
+ ### 1. Find the drawing(s)
35
35
 
36
36
  From a `rebake` request: `GET http://localhost:<port>/api/requests` → the entry with
37
- `type:"rebake"` and `appId:"steel-model"` carries `snapshots: ["<abs path>"]` (the drawing
38
- file) and optionally `sourceName` (the original filename — use it for the `source.name`
39
- provenance field). If the user just attaches or points at a drawing in prose, skip the lookup
40
- and proceed from that path.
41
-
42
- ### 1b. Filter pre-stage isolate the steel linework FIRST (the `filter` node)
37
+ `type:"rebake"` and `appId:"steel-model"` carries `snapshots: ["<abs path>", …]` and optionally
38
+ `sourceName` (the original filename of the first file — use it for the `source.name` provenance
39
+ field). **`snapshots` may hold MULTIPLE files** the user can attach a whole set (e.g. separate
40
+ PDFs for S-201 / S-202 / S-301, or one multi-sheet PDF plus a detail sheet). **Read every file in
41
+ `snapshots[]`, not just the first** — treat them as one combined set: run the sheet index (step 2.1)
42
+ across all of them, and the per-sheet filter pass (1b) and member read span every file. If the user
43
+ just attaches or points at a drawing in prose, skip the lookup and proceed from that path (again,
44
+ handle all the files they gave you).
45
+
46
+ ### 1b. Filter step — isolate the steel linework FIRST (the `filter` node) — MANDATORY, DON'T SKIP
47
+
48
+ > 🚦 **This is a hard gate, not an optional pre-stage. You MUST populate the `filter` block and hand
49
+ > the Filter node to the user to Save BEFORE you read any members (step 2).** The member read is
50
+ > *downstream of* the filter — skipping straight to members is the #1 failure of this skill: the run
51
+ > then renders the shipped example filter (`S-ANNO-BEAM-STEL` / `S-ANNO-GRID` / …) as if it were the
52
+ > user's drawing, and the members get bound by a length heuristic instead of the real steel linework.
53
+ > The `/score` endpoint reports `filterMissing: true` until you've done this — treat that as a blocking
54
+ > worklist item (see step 3). Whenever the drawing has vector linework (any PDF `page.get_drawings()`
55
+ > returns paths), this step is required; a pure raster/photo with no vector paths is the only exception,
56
+ > and even then say so explicitly.
43
57
 
44
58
  Before the heavy read, do **one** deterministic PyMuPDF pass **per framing-plan sheet** that enumerates
45
59
  *every* drawing element by **layer · thickness · colour** and bakes it into the contract's `filter`
@@ -54,6 +68,11 @@ sheets (Foundation, each Floor, Roof). **Show the user the list and get an expli
54
68
  "I found these framing plans: S-201, S-202, S-301 — filter all of these?" — before the pass. (The full
55
69
  elevation/detail classification is the broader workflow; here we only need the plan set.)
56
70
 
71
+ ⚠️ **This ONE plan-scope confirm covers both the filter pass AND the member read — they run on the
72
+ same sheet set. Getting the OK does NOT mean "go read members"; the very next thing you do is the
73
+ filter pass below, then hand off the Filter node. Do not let the confirm gate double as permission to
74
+ skip the filter** (that collision is exactly how a real session read 28 members with no filter block).
75
+
57
76
  Then, for **each** confirmed plan sheet, one pass in display space (`page.rotation_matrix` — never mix
58
77
  native/display coords):
59
78
 
@@ -74,17 +93,23 @@ native/display coords):
74
93
  5. Set `filter.mode: "layer"`, `filter.active: 0`. **PUT** the contract
75
94
  (`PUT /api/contract/steel-model`). On a re-read, preserve any existing per-facet `on` flags.
76
95
 
77
- Then tell the user to open the **Filter** node, flip through the sheets, narrow to the steel, and
78
- **Save**. The saved `on` flags + `mode` are what step 2 reads — **per sheet** (each sheet's elements
79
- passing `mode` + the global `on` flags).
96
+ **Then STOP and hand off checkpoint before the member read.** Tell the user to open the **Filter**
97
+ node, flip through the sheets, narrow to the steel, and **Save**. The saved `on` flags + `mode` are
98
+ what step 2 reads — **per sheet** (each sheet's elements passing `mode` + the global `on` flags). Only
99
+ after the filter is baked (confirm with `GET /api/contract/steel-model` → a `filter` with `sheets`, or
100
+ `POST …/score` → `filterMissing: false`) do you proceed to the member read. The member read binds to
101
+ the user's kept steel; running it against a filter-less contract is the gap this checkpoint exists to
102
+ close.
80
103
 
81
104
  ### 2. Read it with your own vision — the heavy pipeline
82
105
 
83
- > **Candidate geometry comes from the filter (1b) when present.** If the contract carries a saved
84
- > `filter`, the member candidate set **for each sheet** is that sheet's **kept** line elements
85
- > (`filter.sheets[i].elements` whose layer/thickness/colour pass the GLOBAL `mode` + `on` flags)
86
- > bind labels to *those*, not to a length-filtered scan of the whole sheet. The length heuristic
87
- > below is the **fallback** only when no filter was saved.
106
+ > **Candidate geometry comes from the filter (1b) which you already baked.** By the time you reach
107
+ > here, step 1b has run and the user has Saved the Filter node, so the contract carries a `filter`: the
108
+ > member candidate set **for each sheet** is that sheet's **kept** line elements
109
+ > (`filter.sheets[i].elements` whose layer/thickness/colour pass the GLOBAL `mode` + `on` flags) bind
110
+ > labels to *those*, not to a length-filtered scan of the whole sheet. The length heuristic below is a
111
+ > **degraded fallback** for the raster-only exception in 1b (no vector paths to enumerate); if you find
112
+ > yourself using it on a vector PDF, you skipped 1b — go back and do it.
88
113
 
89
114
  Follow the methodology condensed here (full detail + every gotcha in
90
115
  `docs/superpowers/specs/2026-06-17-steel-takeoff-agent-guidance.md`):
@@ -185,9 +210,17 @@ Content-Type: application/json
185
210
  { "contract": <your draft contract>, "target": <target_confidence, e.g. 70> }
186
211
  ```
187
212
 
188
- Returns `{ score, target, meetsTarget, gap, rfiCount, counts, worklist }` — the SAME aggregate the
189
- editor's Confidence chip shows. `worklist` is the rfi/low/med members dragging the score, worst-first
190
- (heaviest first within a band), each with a `reason`. (Omit `contract` to score the stored one.)
213
+ Returns `{ score, target, meetsTarget, gap, rfiCount, counts, worklist, filterMissing }` — the SAME
214
+ aggregate the editor's Confidence chip shows. `worklist` is the rfi/low/med members dragging the score,
215
+ worst-first (heaviest first within a band), each with a `reason`. (Omit `contract` to score the stored
216
+ one.)
217
+
218
+ **`filterMissing` is the completeness gate — independent of the confidence score.** `true` means the
219
+ contract has no usable `filter` block: step 1b never ran, so the members were bound by the length
220
+ heuristic, not the user's steel. A filter-less contract can still hit `meetsTarget` (as one real
221
+ session did at 73%) — so **do not treat `meetsTarget` alone as "done."** If `filterMissing` is `true`,
222
+ go back to step 1b, bake the filter, and hand off the Filter node before continuing. Hand-off (step 6)
223
+ requires `filterMissing: false`.
191
224
 
192
225
  **The loop** — iterate at COMPOSE time until the goal is met:
193
226
 
@@ -404,6 +437,10 @@ offending fields, and re-PUT. You can read back the current contract with
404
437
 
405
438
  ### 6. Hand off to the user
406
439
 
440
+ **Precondition:** `POST …/score` returns `filterMissing: false` (the Filter node was baked in 1b and the
441
+ member read bound to it). If it's still `true`, you are not done — return to step 1b. Do not present a
442
+ takeoff whose filter was skipped as finished.
443
+
407
444
  Tell the user:
408
445
  - What you read (e.g. "read 14 beams and 6 columns on sheets S-201/S-202; 3 MF members await
409
446
  Frame-Elevation resolution; aggregate confidence 78% — above your 70% target").
package/dist/web/app.css CHANGED
@@ -2854,6 +2854,21 @@ body {
2854
2854
  .fm-file-replace, .fm-file-clear { font-size: 10px; padding: 3px 8px; background: var(--surface); border: 1px solid var(--border-strong); color: var(--text-muted); border-radius: 3px; cursor: pointer; font-family: var(--ui); }
2855
2855
  .fm-file-replace:hover { color: var(--text); border-color: var(--accent-dim); }
2856
2856
  .fm-file-clear:hover { color: var(--err); border-color: var(--err); }
2857
+ /* ===== Visual Input: MULTI image/PDF file field (e.g. a framing set) ===== */
2858
+ .fm-files { position: relative; }
2859
+ .fm-files .fm-file-input { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
2860
+ .fm-files .fm-drop.at-cap { cursor: not-allowed; border-style: solid; border-color: var(--border-strong); background: var(--surface-2); color: var(--text-dim); outline: none; }
2861
+ .fm-files-list { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
2862
+ .fm-files-list[hidden] { display: none; }
2863
+ .fm-filechip { display: inline-flex; align-items: center; gap: 6px; max-width: 100%; padding: 4px 6px 4px 8px; border: 1px solid var(--border-strong); border-radius: 4px; background: var(--surface-2); }
2864
+ .fm-filechip.is-reading { opacity: 0.6; }
2865
+ .fm-filechip-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: var(--mono); font-size: 11px; color: var(--text); }
2866
+ .fm-filechip-del { flex: none; width: 16px; height: 16px; padding: 0; display: inline-flex; align-items: center; justify-content: center; font-size: 11px; line-height: 1; background: transparent; border: 1px solid var(--border-strong); border-radius: 2px; color: var(--text-muted); cursor: pointer; }
2867
+ .fm-filechip-del:hover, .fm-filechip-del:focus-visible { color: var(--err); border-color: var(--err); }
2868
+ /* Re-bake "currently loaded" context row (inert — not a field) */
2869
+ .rebake-current { display: flex; align-items: center; gap: 8px; padding: 7px 10px; margin-bottom: 12px; background: var(--surface-2); border-radius: 4px; }
2870
+ .rebake-current-label { flex: none; text-transform: uppercase; letter-spacing: 0.08em; font-size: 10px; color: var(--text-muted); }
2871
+ .rebake-current .fm-file-name { flex: 1; }
2857
2872
  /* Visual Input chip on the canvas input node */
2858
2873
  .agent-card .node-inputs .ni-pair-file { align-items: center; }
2859
2874
  .agent-card .node-inputs .ni-file-glyph { display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 15px; border: 1px solid var(--accent-dim); border-radius: 3px; background: var(--accent-soft); color: var(--accent); font: 700 8px/1 var(--mono); letter-spacing: 0.04em; text-transform: uppercase; flex-shrink: 0; }