@tritard/waterbrother 0.8.38 → 0.8.40

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": "@tritard/waterbrother",
3
- "version": "0.8.38",
3
+ "version": "0.8.40",
4
4
  "description": "Waterbrother: Grok-powered coding CLI with local tools, sessions, operator modes, and approval controls",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -16,6 +16,7 @@ import { computeImpactMap } from "./impact.js";
16
16
  import { reviewTurn } from "./reviewer.js";
17
17
  import {
18
18
  buildFrontendExecutionContext,
19
+ getFrontendAcceptanceFailure,
19
20
  buildFrontendRebuildPrompt,
20
21
  buildFrontendRevisionPrompt,
21
22
  captureFrontendScreenshot,
@@ -1316,6 +1317,14 @@ async function maybeReviseInteractiveFrontend({
1316
1317
  };
1317
1318
  }
1318
1319
 
1320
+ const acceptanceFailure = getFrontendAcceptanceFailure({
1321
+ frontend: frontendExecutionContext.frontend || null,
1322
+ slop: artifacts.designSlop
1323
+ });
1324
+ if (acceptanceFailure) {
1325
+ throw new Error(acceptanceFailure.reason);
1326
+ }
1327
+
1319
1328
  return { response: activeResponse, receipt: activeReceipt, artifacts };
1320
1329
  }
1321
1330
 
package/src/frontend.js CHANGED
@@ -257,6 +257,67 @@ const BENCHMARK_STARTERS = {
257
257
  ]
258
258
  };
259
259
 
260
+ const BENCHMARK_STARTER_VARIANTS = {
261
+ blog: [
262
+ {
263
+ key: "architectural-journal",
264
+ label: "architectural journal",
265
+ starter: [
266
+ "Variant shape: severe image-text split with one offset note card and one lean secondary pair.",
267
+ "Palette should feel mineral, paper, and shadow-driven rather than warm lifestyle-editorial.",
268
+ "Finish with one restrained final gesture, not a generic footer label."
269
+ ]
270
+ },
271
+ {
272
+ key: "art-book-minimal",
273
+ label: "art-book minimal",
274
+ starter: [
275
+ "Variant shape: oversized white space, one dominant visual surface, one short text block, one caption-like secondary band.",
276
+ "Treat typography like exhibition text: sparse, quiet, and deliberate.",
277
+ "Avoid default editorial chrome entirely; let spacing and crop do the work."
278
+ ]
279
+ },
280
+ {
281
+ key: "review-folio",
282
+ label: "review folio",
283
+ starter: [
284
+ "Variant shape: folio-like review page with strong masthead mark, one lead essay, and one compact pair of related notes.",
285
+ "Use stronger typographic contrast and one unexpected compositional move, not a generic archive scaffold.",
286
+ "Closing section should feel like a designed coda rather than a footer."
287
+ ]
288
+ }
289
+ ],
290
+ store: [
291
+ {
292
+ key: "cold-premium-tech",
293
+ label: "cold premium tech",
294
+ starter: [
295
+ "Variant shape: hard-edged flagship PDP with dark hero, precise specification accents, and a compact proof/guarantee band.",
296
+ "Use crisp hierarchy, hard contrast, and performance language rather than luxury-lifestyle filler.",
297
+ "Cart UI should feel real and quiet; no gimmicks, no fake app chrome."
298
+ ]
299
+ },
300
+ {
301
+ key: "warm-object-craft",
302
+ label: "warm object craft",
303
+ starter: [
304
+ "Variant shape: tactile product story with warm neutral palette, close material crop, and trust/objection handling near the CTA.",
305
+ "Lead with material, construction, weight, care, and use-case details.",
306
+ "Keep the page commercially sharp, but let the object feel physical and desirable."
307
+ ]
308
+ },
309
+ {
310
+ key: "dense-conversion-pdp",
311
+ label: "dense conversion pdp",
312
+ starter: [
313
+ "Variant shape: stronger purchase stack with nearby shipping, returns, warranty, and one compact objection-handling block.",
314
+ "Use a more aggressive conversion rhythm than the minimalist variants, but keep it authored and clean.",
315
+ "Hero and cart item surfaces must both feel product-real, not abstract placeholders."
316
+ ]
317
+ }
318
+ ]
319
+ };
320
+
260
321
  const BENCHMARK_FEWSHOT_EXAMPLES = {
261
322
  blog: {
262
323
  good: [
@@ -351,6 +412,38 @@ function getBenchmarkStarter(siteType, benchmarkMode) {
351
412
  return BENCHMARK_STARTERS[siteType] || null;
352
413
  }
353
414
 
415
+ function stablePromptHash(text = "") {
416
+ const value = String(text || "");
417
+ let hash = 0;
418
+ for (let index = 0; index < value.length; index += 1) {
419
+ hash = ((hash * 31) + value.charCodeAt(index)) >>> 0;
420
+ }
421
+ return hash;
422
+ }
423
+
424
+ function chooseBenchmarkStarterVariant({ siteType, promptText = "", archetype = "" } = {}) {
425
+ const variants = BENCHMARK_STARTER_VARIANTS[siteType] || [];
426
+ if (variants.length === 0) return null;
427
+ const text = String(promptText || "").toLowerCase();
428
+ if (siteType === "blog") {
429
+ if (/\b(?:architecture|architectural|concrete|brutalis|ando|chapel|osaka|kyoto|sao paulo)\b/.test(text)) {
430
+ return variants.find((variant) => variant.key === "architectural-journal") || variants[0];
431
+ }
432
+ if (/\b(?:gallery|art book|art-book|folio|exhibition|museum)\b/.test(text)) {
433
+ return variants.find((variant) => variant.key === "art-book-minimal") || variants[0];
434
+ }
435
+ }
436
+ if (siteType === "store") {
437
+ if (/\b(?:audio|headphones|speaker|tech|wireless|anc|bluetooth)\b/.test(text) || archetype === "high-contrast-tech") {
438
+ return variants.find((variant) => variant.key === "cold-premium-tech") || variants[0];
439
+ }
440
+ if (/\b(?:lamp|chair|desk|leather|tote|wallet|wood|ceramic|object)\b/.test(text)) {
441
+ return variants.find((variant) => variant.key === "warm-object-craft") || variants[0];
442
+ }
443
+ }
444
+ return variants[stablePromptHash(text) % variants.length];
445
+ }
446
+
354
447
  function getBenchmarkExamples(siteType, benchmarkMode) {
355
448
  if (!benchmarkMode) return null;
356
449
  return BENCHMARK_FEWSHOT_EXAMPLES[siteType] || null;
@@ -499,6 +592,7 @@ export function buildFrontendExecutionContext({ promptText = "", profile = "code
499
592
  const assetPolicy = inferAssetPolicy(text);
500
593
  const layoutStarter = getLayoutStarter(siteType, archetype);
501
594
  const benchmarkStarter = getBenchmarkStarter(siteType, benchmarkMode);
595
+ const benchmarkVariant = chooseBenchmarkStarterVariant({ siteType, promptText: text, archetype });
502
596
  const benchmarkExamples = getBenchmarkExamples(siteType, benchmarkMode);
503
597
  const siteTypeRules = getSiteTypeRules(siteType);
504
598
  const benchmarkSiteTypeRules = getBenchmarkSiteTypeRules(siteType, benchmarkMode);
@@ -507,6 +601,7 @@ export function buildFrontendExecutionContext({ promptText = "", profile = "code
507
601
  `Content mode: ${contentMode}. Asset policy: ${assetPolicy}.`,
508
602
  layoutStarter ? `Layout starter: ${layoutStarter[0]}` : "",
509
603
  ...(benchmarkStarter ? [`Benchmark starter skeleton:\n- ${benchmarkStarter.join("\n- ")}`] : []),
604
+ ...(benchmarkVariant ? [`Benchmark starter variant: ${benchmarkVariant.label}\n- ${benchmarkVariant.starter.join("\n- ")}`] : []),
510
605
  ...(benchmarkExamples ? [
511
606
  `Benchmark few-shot guide:\nGood:\n- ${benchmarkExamples.good.join("\n- ")}\nBad:\n- ${benchmarkExamples.bad.join("\n- ")}`
512
607
  ] : []),
@@ -528,6 +623,8 @@ export function buildFrontendExecutionContext({ promptText = "", profile = "code
528
623
  assetPolicy,
529
624
  layoutStarter: layoutStarter ? layoutStarter[0] : null,
530
625
  benchmarkStarter: benchmarkStarter ? benchmarkStarter[0] : null,
626
+ benchmarkVariant: benchmarkVariant?.label || null,
627
+ benchmarkVariantStarter: benchmarkVariant?.starter || null,
531
628
  benchmarkMode,
532
629
  siteTypeRules,
533
630
  benchmarkSiteTypeRules
@@ -806,6 +903,24 @@ export function shouldForceFrontendRebuild({ frontend = null, slop = null, revis
806
903
  return revisionCount < 2;
807
904
  }
808
905
 
906
+ export function getFrontendAcceptanceFailure({ frontend = null, slop = null } = {}) {
907
+ if (!frontend?.benchmarkMode || !slop?.hardBlock) return null;
908
+ const flags = Array.isArray(slop.flags) ? slop.flags : [];
909
+ if (frontend.siteType === "store") {
910
+ const blocking = flags.filter((flag) =>
911
+ /Tailwind CDN|Font Awesome|placeholder product imagery|placeholder imagery|fake proof or badge chrome|fake keyboard or shortcut chrome|demo behavior|demo page explanation copy|fake cart or checkout fallback behavior|generic luxury-commerce copy/i.test(flag)
912
+ );
913
+ if (blocking.length > 0) {
914
+ return {
915
+ siteType: frontend.siteType,
916
+ reason: `benchmark store acceptance failed: ${blocking.join(", ")}`,
917
+ flags: blocking
918
+ };
919
+ }
920
+ }
921
+ return null;
922
+ }
923
+
809
924
  export function buildFrontendRevisionPrompt({
810
925
  originalPrompt = "",
811
926
  designReview = null,
@@ -814,6 +929,7 @@ export function buildFrontendRevisionPrompt({
814
929
  } = {}) {
815
930
  const siteType = pickByPattern(String(originalPrompt || ""), SITE_TYPES, "landing");
816
931
  const benchmarkMode = isBenchmarkFrontendTask(originalPrompt);
932
+ const benchmarkVariant = chooseBenchmarkStarterVariant({ siteType, promptText: originalPrompt });
817
933
  const benchmarkExamples = getBenchmarkExamples(siteType, benchmarkMode);
818
934
  const issues = Array.isArray(designReview?.issues) ? designReview.issues.slice(0, 6) : [];
819
935
  const nextPass = Array.isArray(designReview?.nextPass) ? designReview.nextPass.slice(0, 6) : [];
@@ -826,6 +942,7 @@ export function buildFrontendRevisionPrompt({
826
942
  issues.length > 0 ? `Problems to fix:\n- ${issues.join("\n- ")}` : "",
827
943
  visualIssues.length > 0 ? `Visible screenshot problems:\n- ${visualIssues.join("\n- ")}` : "",
828
944
  slopFlags.length > 0 ? `Deterministic slop flags:\n- ${slopFlags.join("\n- ")}` : "",
945
+ benchmarkVariant ? `Preserve this benchmark variant direction: ${benchmarkVariant.label}\n- ${benchmarkVariant.starter.join("\n- ")}` : "",
829
946
  benchmarkExamples ? `Benchmark example guide:\nGood:\n- ${benchmarkExamples.good.join("\n- ")}\nBad:\n- ${benchmarkExamples.bad.join("\n- ")}` : "",
830
947
  nextPass.length > 0 ? `Revision priorities:\n- ${nextPass.join("\n- ")}` : "",
831
948
  visualNextPass.length > 0 ? `Screenshot revision priorities:\n- ${visualNextPass.join("\n- ")}` : "",
@@ -862,6 +979,9 @@ export function buildFrontendRebuildPrompt({
862
979
  } = {}) {
863
980
  const siteType = pickByPattern(String(originalPrompt || ""), SITE_TYPES, "landing");
864
981
  const benchmarkMode = isBenchmarkFrontendTask(originalPrompt);
982
+ const benchmarkVariant = frontend?.benchmarkVariantStarter?.length
983
+ ? { label: frontend.benchmarkVariant || "selected benchmark variant", starter: frontend.benchmarkVariantStarter }
984
+ : chooseBenchmarkStarterVariant({ siteType, promptText: originalPrompt, archetype: frontend?.archetype || "" });
865
985
  const benchmarkExamples = getBenchmarkExamples(siteType, benchmarkMode);
866
986
  const slopFlags = Array.isArray(slop?.flags) ? slop.flags.slice(0, 8) : [];
867
987
  const issues = Array.isArray(designReview?.issues) ? designReview.issues.slice(0, 6) : [];
@@ -871,6 +991,7 @@ export function buildFrontendRebuildPrompt({
871
991
  "Rebuild the affected frontend files from scratch within the same contract.",
872
992
  `Original task: ${String(originalPrompt || "").trim()}`,
873
993
  starter ? `Required starter skeleton:\n- ${starter.join("\n- ")}` : "",
994
+ benchmarkVariant ? `Required benchmark variant direction: ${benchmarkVariant.label}\n- ${benchmarkVariant.starter.join("\n- ")}` : "",
874
995
  benchmarkExamples ? `Benchmark example guide:\nGood:\n- ${benchmarkExamples.good.join("\n- ")}\nBad:\n- ${benchmarkExamples.bad.join("\n- ")}` : "",
875
996
  slopFlags.length > 0 ? `Banned patterns that must be removed completely:\n- ${slopFlags.join("\n- ")}` : "",
876
997
  issues.length > 0 ? `Design issues to correct in the rebuild:\n- ${issues.join("\n- ")}` : "",
package/src/workflow.js CHANGED
@@ -3,6 +3,7 @@ import { computeImpactMap, summarizeImpactMap } from "./impact.js";
3
3
  import { reviewTurn, challengeReceipt } from "./reviewer.js";
4
4
  import {
5
5
  buildFrontendExecutionContext,
6
+ getFrontendAcceptanceFailure,
6
7
  buildFrontendRebuildPrompt,
7
8
  buildFrontendRevisionPrompt,
8
9
  captureFrontendScreenshot,
@@ -253,6 +254,14 @@ export async function runBuildWorkflow({
253
254
  };
254
255
  }
255
256
 
257
+ const acceptanceFailure = getFrontendAcceptanceFailure({
258
+ frontend: frontendCtx?.frontend || null,
259
+ slop: designSlop
260
+ });
261
+ if (acceptanceFailure) {
262
+ throw new Error(acceptanceFailure.reason);
263
+ }
264
+
256
265
  // Update receipt with impact and review
257
266
  const updates = {};
258
267
  if (impact) updates.impact = impact;