@wix/himalaya-cli 0.854.0 → 0.856.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.
package/dist/cli.mjs CHANGED
@@ -832,19 +832,120 @@ var init_source_deny = __esm({
832
832
  });
833
833
 
834
834
  // src/mobile-ux-contract.ts
835
+ function mobileUxDocumentBody(mobile) {
836
+ const comments = /<!--[\s\S]*?(?:-->|$)/g;
837
+ let out = "";
838
+ let last = 0;
839
+ let blankBefore = true;
840
+ for (const match of mobile.matchAll(comments)) {
841
+ const offset = match.index ?? 0;
842
+ const between = mobile.slice(last, offset);
843
+ const newline = between.lastIndexOf("\n");
844
+ if (newline >= 0) blankBefore = between.slice(newline + 1).trim() === "";
845
+ else blankBefore = blankBefore && between.trim() === "";
846
+ out += between;
847
+ const ownsTheLine = offset === 0 || mobile[offset - 1] === "\n";
848
+ const precedesHeading = blankBefore && mobile.startsWith("#", offset + match[0].length);
849
+ out += ownsTheLine || precedesHeading ? "\n" : " ";
850
+ last = offset + match[0].length;
851
+ }
852
+ return out + mobile.slice(last);
853
+ }
854
+ function mobileUxSection(text2, heading) {
855
+ const match = new RegExp(`(?:^|\\n)##[ \\t]+${heading}\\b[^\\n]*([\\s\\S]*?)(?=\\n##[ \\t]|$)`, "i").exec(text2);
856
+ return match ? match[1] : null;
857
+ }
858
+ function sectionTables(markdown) {
859
+ const lines = markdown.split("\n").map((line) => line.trim());
860
+ const tables = [];
861
+ for (let i = 1; i < lines.length; i++) {
862
+ if (!DELIMITER_ROW.test(lines[i])) continue;
863
+ const header = lines[i - 1];
864
+ if (!header.includes("|")) continue;
865
+ const table = { header: splitCells(header), data: [] };
866
+ let j = i + 1;
867
+ for (; j < lines.length; j++) {
868
+ const line = lines[j];
869
+ if (!line.includes("|")) break;
870
+ if (DELIMITER_ROW.test(line)) continue;
871
+ const cells = splitCells(line);
872
+ if (cells.every((cell) => cell === "")) continue;
873
+ table.data.push(cells);
874
+ }
875
+ tables.push(table);
876
+ i = j;
877
+ }
878
+ return tables;
879
+ }
880
+ function namesOneCta(cell) {
881
+ if (!cell) return false;
882
+ return !/[,;]|\s\/\s/.test(cell);
883
+ }
884
+ function declaredStates(text2) {
885
+ const contract = section(text2, "Screen contract");
886
+ if (!contract) return [];
887
+ const states = [];
888
+ for (const bullet of contract.matchAll(/^[ \t]*[-*][ \t]*[^:\n]*\bstates?[ \t]*:[ \t]*(\S.*)$/gim)) {
889
+ const items = bullet[1].split(/[,;/]/).map((item) => item.trim().replace(/[.:]+$/, "")).filter(Boolean);
890
+ if (!items.length || items.some((item) => !/^[\w][\w.\-+]*$/.test(item))) continue;
891
+ states.push(...items);
892
+ }
893
+ return [...new Set(states.map((state) => state.toLowerCase()))];
894
+ }
895
+ function primaryCtaStructureGap(text2) {
896
+ const acceptance = section(text2, "Implementation acceptance");
897
+ if (acceptance === null) return "There is no `## Implementation acceptance` section to read it from.";
898
+ const tables = sectionTables(acceptance).filter((table) => table.data.length > 0);
899
+ if (!tables.length) return "Its acceptance table has no rows.";
900
+ const ctaTables = tables.map((table) => ({ table, cta: table.header.findIndex((cell) => /primary\s*(?:cta|call[- ]?to[- ]?action|action)/i.test(cell)) })).filter((entry) => entry.cta >= 0);
901
+ if (!ctaTables.length) return "Its acceptance table has no `Primary CTA` column.";
902
+ const unanswered = ctaTables.flatMap(({ table, cta }) => table.data.filter((row) => !namesOneCta(row[cta] ?? "")));
903
+ if (unanswered.length) {
904
+ return `${unanswered.length} acceptance row(s) do not name exactly one primary CTA (first: ${JSON.stringify(unanswered[0][0] ?? "")}).`;
905
+ }
906
+ const data = ctaTables.flatMap(({ table }) => table.data);
907
+ const flat = (value) => value.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
908
+ const rowText = data.map((row) => flat(row.join(" ")));
909
+ const missing = declaredStates(text2).filter((state) => !rowText.some((row) => row.includes(flat(state))));
910
+ if (missing.length) return `The acceptance table has no row for the declared state(s): ${missing.join(", ")}.`;
911
+ return null;
912
+ }
913
+ function hasUnterminatedComment(mobile) {
914
+ let i = 0;
915
+ for (; ; ) {
916
+ const open = mobile.indexOf("<!--", i);
917
+ if (open < 0) return false;
918
+ const close = mobile.indexOf("-->", open + 4);
919
+ if (close < 0) return true;
920
+ i = close + 3;
921
+ }
922
+ }
835
923
  function mobileUxMarkerFindings(mobile) {
924
+ const text2 = mobileUxDocumentBody(mobile);
836
925
  const findings = [];
837
- if (!/^## Implementation acceptance\b/im.test(mobile)) {
838
- findings.push({ code: "missing-acceptance-matrix", message: "MOBILE-UX.md must contain the exact heading `## Implementation acceptance`." });
926
+ if (!/^## Implementation acceptance\b/im.test(text2)) {
927
+ findings.push({ code: "missing-acceptance-matrix", message: "MOBILE-UX.md must contain the heading `## Implementation acceptance` at the start of a line. Case is not checked, but use this spelling: other tooling addresses the document by it." });
839
928
  }
840
- if (!/^## Evidence\b/im.test(mobile)) {
841
- findings.push({ code: "missing-evidence-section", message: "MOBILE-UX.md must contain the exact heading `## Evidence`." });
929
+ if (!/^## Evidence\b/im.test(text2)) {
930
+ findings.push({ code: "missing-evidence-section", message: "MOBILE-UX.md must contain the heading `## Evidence` at the start of a line. Case is not checked, but use this spelling: other tooling addresses the document by it." });
842
931
  }
843
- if (!/one primary CTA per state/i.test(mobile)) {
844
- findings.push({ code: "missing-primary-cta-rule", message: "MOBILE-UX.md must include the exact phrase `one primary CTA per state`." });
932
+ if (!/one primary CTA per state/i.test(text2)) {
933
+ const gap = primaryCtaStructureGap(text2);
934
+ if (gap) {
935
+ findings.push({
936
+ code: "missing-primary-cta-rule",
937
+ message: "MOBILE-UX.md must establish one primary CTA per state: give `## Implementation acceptance` a `Primary CTA` column with a row per declared state and exactly one CTA in each cell, or keep the phrase `one primary CTA per state`. " + gap
938
+ });
939
+ }
845
940
  }
846
- if (!hasWidthEvidence(mobile)) {
847
- findings.push({ code: "missing-width-evidence", message: widthEvidenceMessage(mobile) });
941
+ if (!hasWidthEvidence(text2)) {
942
+ findings.push({ code: "missing-width-evidence", message: widthEvidenceMessage(text2) });
943
+ }
944
+ if (findings.length && hasUnterminatedComment(mobile)) {
945
+ return [{
946
+ code: "unterminated-html-comment",
947
+ message: "MOBILE-UX.md has an HTML comment opened with `<!--` and never closed with `-->`, so everything after it is commented out \u2014 in this check and in any renderer. Close the comment and re-run: the headings and phrases below it are not being read."
948
+ }];
848
949
  }
849
950
  return findings;
850
951
  }
@@ -911,7 +1012,7 @@ function widthEvidenceMessage(mobile) {
911
1012
  }
912
1013
  return base;
913
1014
  }
914
- var SPEC_TEMPLATE, MOBILE_UX_TEMPLATE, WIDTH_LITERAL, COMPACT_MAX, WIDER_MIN, PLAUSIBLE_WIDTH, YEAR, inRange, looksLikeYear, UNIT_ATTACHED, UNIT_WORD_ATTACHED, WIDTH_WINDOW, EDGE_WINDOW, FILENAME_AFTER, ANTI_CUE_BEFORE, PAIR_HEIGHT_BEFORE, ANTI_CUE_AFTER, isCompact, isWider;
1015
+ var SPEC_TEMPLATE, MOBILE_UX_MARKER_NOTE, MOBILE_UX_TEMPLATE, section, DELIMITER_ROW, splitCells, WIDTH_LITERAL, COMPACT_MAX, WIDER_MIN, PLAUSIBLE_WIDTH, YEAR, inRange, looksLikeYear, UNIT_ATTACHED, UNIT_WORD_ATTACHED, WIDTH_WINDOW, EDGE_WINDOW, FILENAME_AFTER, ANTI_CUE_BEFORE, PAIR_HEIGHT_BEFORE, ANTI_CUE_AFTER, isCompact, isWider;
915
1016
  var init_mobile_ux_contract = __esm({
916
1017
  "src/mobile-ux-contract.ts"() {
917
1018
  SPEC_TEMPLATE = `# App specification
@@ -938,8 +1039,33 @@ var init_mobile_ux_contract = __esm({
938
1039
  - Required: iOS, Android
939
1040
  - Optional: web, desktop, watch, widgets
940
1041
  `;
1042
+ MOBILE_UX_MARKER_NOTE = `<!-- LOAD-BEARING \u2014 read this before you rewrite the file.
1043
+
1044
+ \`himi validate\` warns, and \`himi validate --strict\` / \`himi ux audit --strict\` REFUSE, a
1045
+ MOBILE-UX.md that satisfies none of the forms below. Everything else here is yours to
1046
+ replace freely. This comment is not itself an answer: HTML comments are stripped before the
1047
+ check runs, so keeping it satisfies nothing.
1048
+
1049
+ 1. The headings \`## Implementation acceptance\` and \`## Evidence\`, each at the start of
1050
+ a line. Case is not checked; use these spellings anyway, because other tooling
1051
+ addresses this document by them.
1052
+ 2. One primary CTA per state \u2014 EITHER the phrase \`one primary CTA per state\`, OR a
1053
+ \`Primary CTA\` column in the acceptance table below, with a row for every state you
1054
+ declare under \`## Screen contract\` and exactly one CTA named in each row's cell.
1055
+ 3. Width evidence \u2014 EITHER the scaffold's own sentence, \`compact (320/360)\` followed by
1056
+ \`expanded\`, OR two widths you actually rendered, EACH SPELLED AS A WIDTH ON ITSELF:
1057
+ one below 600px and one at 600px or wider, within about 400 characters of each other.
1058
+ A number is spelled as a width when it carries a unit (\`320px\`, \`834 pt\`,
1059
+ \`320 pixels\`, \`320 wide\`) or sits in a screenshot filename (\`home-320.png\`,
1060
+ \`home-390x844.png\`). Nothing is read from the words around the number, so a BARE
1061
+ figure never counts however you label it \u2014 \`Widths: 320, 390, 834\` and
1062
+ \`compact (320) and expanded (834)\` both name no width. One unit per number fixes it.
1063
+
1064
+ Full contract: \`himi docs mobile-ux\` -> "Strict audit contract". -->`;
941
1065
  MOBILE_UX_TEMPLATE = `# Mobile UX
942
1066
 
1067
+ ${MOBILE_UX_MARKER_NOTE}
1068
+
943
1069
  ## Flows
944
1070
  ### <Task>
945
1071
  1. Entry:
@@ -982,6 +1108,9 @@ in preview fixtures or labelled, capability-limited fallback states\u2014not ord
982
1108
  > Keep this document safe to publish with a release: do not include credentials, personal data,
983
1109
  > internal URLs, private research, or unreviewed notes. Read \`himi docs mobile-ux\` before filling it in.
984
1110
  `;
1111
+ section = mobileUxSection;
1112
+ DELIMITER_ROW = /^\|?\s*:?-+:?\s*(?:\|\s*:?-+:?\s*)+\|?$/;
1113
+ splitCells = (line) => line.replace(/^\|/, "").replace(/\|\s*$/, "").split("|").map((cell) => cell.trim());
985
1114
  WIDTH_LITERAL = /compact \(320\/360\)[\s\S]{0,400}?expanded/i;
986
1115
  COMPACT_MAX = 599;
987
1116
  WIDER_MIN = 600;
@@ -4729,7 +4858,6 @@ function resolveTranslation(value, catalog, sourceCatalog) {
4729
4858
  var BINDING_SIGILS, BINDING_SIGIL_RE, TRANSLATION_PREFIX, TRANSLATION_KEY_RE;
4730
4859
  var init_bindings = __esm({
4731
4860
  "../../core/ts/src/bindings.ts"() {
4732
- "use strict";
4733
4861
  BINDING_SIGILS = ["service", "flag", "item", "t"];
4734
4862
  BINDING_SIGIL_RE = new RegExp(
4735
4863
  `^\\$(?:${BINDING_SIGILS.filter((s) => s !== "t").join("|")})\\b|^\\$t:`
@@ -6606,7 +6734,7 @@ function validateWorkerAction(action, context) {
6606
6734
  }
6607
6735
  }
6608
6736
  if (context.actionCatalogWithheld) {
6609
- return actionIssue(action, "warning", `worker actionId "${action.actionId}" was not cross-checked: this surface has precomputed service state but no precomputed action catalog`);
6737
+ return actionIssue(action, "warning", `worker actionId "${action.actionId}" was not cross-checked: this surface has precomputed service state but no precomputed action catalog. This is a property of the surface doing the checking, not of your app \u2014 there is nothing to fix in the content package; re-run the check where the bundle can be inspected (\`himi validate\` / \`himi test\` locally) to have it cross-checked`);
6610
6738
  }
6611
6739
  const reason = context.bundleInspectionError ? `the bound bundle could not be inspected: ${context.bundleInspectionError}` : "no inspectable logic bundle is bound";
6612
6740
  return actionIssue(action, "error", `worker actionId "${action.actionId}" cannot resolve because ${reason}`);
@@ -26475,7 +26603,6 @@ function shouldRevealError(args) {
26475
26603
  var EMAIL_RE, URL_HOST_RE, INTEGER_RE, NUMBER_RE;
26476
26604
  var init_validation = __esm({
26477
26605
  "../../core/ts/src/validation.ts"() {
26478
- "use strict";
26479
26606
  EMAIL_RE = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,}$/;
26480
26607
  URL_HOST_RE = /^[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*(:[0-9]+)?$/;
26481
26608
  INTEGER_RE = /^[+-]?[0-9]+$/;
@@ -29898,7 +30025,6 @@ function validateWorkerSubtree(raw, slotId, policy, actionCatalog) {
29898
30025
  var WORKER_SUBTREE_CEILINGS, ID_PART, ACTION_ID;
29899
30026
  var init_worker_subtree = __esm({
29900
30027
  "../../core/ts/src/worker-subtree.ts"() {
29901
- "use strict";
29902
30028
  init_worker_subtree_safe_types_generated();
29903
30029
  WORKER_SUBTREE_CEILINGS = Object.freeze({
29904
30030
  maxEncodedBytes: 65536,
@@ -46300,9 +46426,9 @@ function ipv6Private(address) {
46300
46426
  const value = address.toLowerCase().replace(/^\[|\]$/g, "");
46301
46427
  const sections = value.split("::");
46302
46428
  if (sections.length > 2) return true;
46303
- const parse = (section) => {
46304
- if (!section) return [];
46305
- const parts = section.split(":");
46429
+ const parse = (section2) => {
46430
+ if (!section2) return [];
46431
+ const parts = section2.split(":");
46306
46432
  const last = parts.at(-1) ?? "";
46307
46433
  if (!last.includes(".")) return parts.map((part) => Number.parseInt(part, 16));
46308
46434
  parts.pop();
@@ -47927,6 +48053,7 @@ __export(native_decode_exports, {
47927
48053
  nativeDecodeAndroidHarnessDir: () => nativeDecodeAndroidHarnessDir,
47928
48054
  nativeDecodePackageDir: () => nativeDecodePackageDir,
47929
48055
  nativeDecodePlatforms: () => nativeDecodePlatforms,
48056
+ nativeOracleShipped: () => nativeOracleShipped,
47930
48057
  parseNativeDecodeOutput: () => parseNativeDecodeOutput,
47931
48058
  runAndroidNativeDecode: () => runAndroidNativeDecode,
47932
48059
  runNativeDecode: () => runNativeDecode
@@ -47942,6 +48069,12 @@ function nativeDecodePackageDir() {
47942
48069
  function nativeDecodeAndroidHarnessDir() {
47943
48070
  return fileURLToPath19(new URL("../../../tools/wix-sdk-android-tests/", import.meta.url));
47944
48071
  }
48072
+ function nativeOracleShipped() {
48073
+ return {
48074
+ ios: existsSync41(join41(nativeDecodePackageDir(), "Package.swift")),
48075
+ android: existsSync41(join41(nativeDecodeAndroidHarnessDir(), "gradlew"))
48076
+ };
48077
+ }
47945
48078
  function detectNativeDecode(options = {}) {
47946
48079
  const hostPlatform = options.hostPlatform ?? process.platform;
47947
48080
  const packageDir = options.packageDir ?? nativeDecodePackageDir();
@@ -48443,11 +48576,15 @@ __export(coverage_exports, {
48443
48576
  diffAndStoreObservations: () => diffAndStoreObservations,
48444
48577
  formatDelta: () => formatDelta,
48445
48578
  formatLedger: () => formatLedger,
48579
+ suppressedNativeKeys: () => suppressedNativeKeys,
48446
48580
  trendAndStore: () => trendAndStore
48447
48581
  });
48448
48582
  import { existsSync as existsSync43, mkdirSync as mkdirSync21, readFileSync as readFileSync50, writeFileSync as writeFileSync18 } from "node:fs";
48449
48583
  import { dirname as dirname25, join as join43 } from "node:path";
48450
- function assembleLedger(r) {
48584
+ function suppressedNativeKeys(oracle) {
48585
+ return [...oracle.ios ? [] : ["native-decode|ios"], ...oracle.android ? [] : ["native-decode|android"]];
48586
+ }
48587
+ function assembleLedger(r, options = {}) {
48451
48588
  const out = [];
48452
48589
  for (const c of r.crawled ?? []) {
48453
48590
  const noRow = (c.actionsSkipped ?? []).filter((s) => s.why === "no-row-value");
@@ -48569,8 +48706,9 @@ function assembleLedger(r) {
48569
48706
  remedy: "npx playwright install chromium (~170MB, one time) \u2014 then every himi test writes a PNG per screen to .himi/shots/"
48570
48707
  });
48571
48708
  }
48709
+ const oracle = options.nativeOracle ?? nativeOracleShipped();
48572
48710
  const nativeIos = r.nativeDecode?.ios;
48573
- if (!nativeIos || "unavailable" in nativeIos) {
48711
+ if (oracle.ios && (!nativeIos || "unavailable" in nativeIos)) {
48574
48712
  const reason = nativeIos && "unavailable" in nativeIos ? ` (${nativeIos.reason})` : "";
48575
48713
  out.push({
48576
48714
  key: "native-decode|ios",
@@ -48581,7 +48719,7 @@ function assembleLedger(r) {
48581
48719
  });
48582
48720
  }
48583
48721
  const nativeAndroid = r.nativeDecode?.android;
48584
- if (!nativeAndroid || "unavailable" in nativeAndroid) {
48722
+ if (oracle.android && (!nativeAndroid || "unavailable" in nativeAndroid)) {
48585
48723
  const reason = nativeAndroid && "unavailable" in nativeAndroid ? ` (${nativeAndroid.reason})` : "";
48586
48724
  out.push({
48587
48725
  key: "native-decode|android",
@@ -48611,7 +48749,7 @@ function assembleLedger(r) {
48611
48749
  }
48612
48750
  return out;
48613
48751
  }
48614
- function trendAndStore(dir, entries) {
48752
+ function trendAndStore(dir, entries, suppressed) {
48615
48753
  const p = join43(dir, LEDGER_FILE);
48616
48754
  let previous = null;
48617
48755
  try {
@@ -48622,19 +48760,21 @@ function trendAndStore(dir, entries) {
48622
48760
  } catch {
48623
48761
  }
48624
48762
  const current = entries.map((e) => e.key);
48763
+ const stored = [.../* @__PURE__ */ new Set([...current, ...(previous ?? []).filter((key2) => (suppressed ?? []).includes(key2))])];
48625
48764
  try {
48626
48765
  mkdirSync21(dirname25(p), { recursive: true });
48627
- writeFileSync18(p, JSON.stringify({ keys: current, ts: (/* @__PURE__ */ new Date()).toISOString() }, null, 2));
48766
+ writeFileSync18(p, JSON.stringify({ keys: stored, ts: (/* @__PURE__ */ new Date()).toISOString() }, null, 2));
48628
48767
  } catch {
48629
48768
  return null;
48630
48769
  }
48631
48770
  if (previous === null) return { opened: 0, closed: 0 };
48632
- const prev = new Set(previous);
48771
+ const baseline = previous.filter((key2) => !(suppressed ?? []).includes(key2));
48772
+ const prev = new Set(baseline);
48633
48773
  const cur = new Set(current);
48634
48774
  return {
48635
48775
  opened: current.filter((k) => !prev.has(k)).length,
48636
- closed: previous.filter((k) => !cur.has(k)).length,
48637
- previousTotal: previous.length
48776
+ closed: baseline.filter((k) => !cur.has(k)).length,
48777
+ previousTotal: baseline.length
48638
48778
  };
48639
48779
  }
48640
48780
  function formatLedger(entries, trend) {
@@ -48705,6 +48845,7 @@ function formatDelta(delta) {
48705
48845
  var LEDGER_FILE, OBS_FILE;
48706
48846
  var init_coverage = __esm({
48707
48847
  "src/coverage.ts"() {
48848
+ init_native_decode();
48708
48849
  LEDGER_FILE = join43(".himi", "test-ledger.json");
48709
48850
  OBS_FILE = join43(".himi", "test-observations.json");
48710
48851
  }
@@ -50923,7 +51064,7 @@ function scalar2(v) {
50923
51064
  }
50924
51065
  function parseStoreConfig(text2) {
50925
51066
  const cfg = {};
50926
- let section = null;
51067
+ let section2 = null;
50927
51068
  for (const raw of text2.split("\n")) {
50928
51069
  const line = strip2(raw);
50929
51070
  if (!line.trim()) continue;
@@ -50933,14 +51074,14 @@ function parseStoreConfig(text2) {
50933
51074
  const [, key2, rest] = kv;
50934
51075
  if (!indented) {
50935
51076
  if (rest.trim() === "") {
50936
- section = {};
50937
- cfg[key2] = section;
51077
+ section2 = {};
51078
+ cfg[key2] = section2;
50938
51079
  } else {
50939
- section = null;
51080
+ section2 = null;
50940
51081
  cfg[key2] = scalar2(rest);
50941
51082
  }
50942
- } else if (section) {
50943
- section[key2] = scalar2(rest);
51083
+ } else if (section2) {
51084
+ section2[key2] = scalar2(rest);
50944
51085
  }
50945
51086
  }
50946
51087
  return cfg;
@@ -53635,11 +53776,11 @@ function suggestKeys(args) {
53635
53776
  if (rule.tab && allowed2(rule) && rule.re.test(label2)) set(rule.tab, label2);
53636
53777
  }
53637
53778
  }
53638
- for (const section of sections) {
53779
+ for (const section2 of sections) {
53639
53780
  for (const rule of KEY_RULES) {
53640
- if (!allowed2(rule) || !rule.re.test(section.heading)) continue;
53641
- set(rule.title, section.heading);
53642
- if (section.body) set(rule.subtitle, section.body);
53781
+ if (!allowed2(rule) || !rule.re.test(section2.heading)) continue;
53782
+ set(rule.title, section2.heading);
53783
+ if (section2.body) set(rule.subtitle, section2.body);
53643
53784
  }
53644
53785
  }
53645
53786
  if (tagline) set("home.onboardingBody", tagline.text);
@@ -53672,12 +53813,12 @@ function buildCopyChapter(args) {
53672
53813
  pageUrl: page.url
53673
53814
  }))
53674
53815
  );
53675
- for (const section of candidates) {
53816
+ for (const section2 of candidates) {
53676
53817
  if (faq.length >= CAP.faq) break;
53677
- if (!section.heading || !section.body || !/\?\s*$/.test(section.heading)) continue;
53678
- if (about && section.body === about) continue;
53679
- if (faq.some((f) => f.q.toLowerCase() === section.heading.toLowerCase())) continue;
53680
- faq.push({ q: section.heading, a: section.body, pageUrl: section.pageUrl });
53818
+ if (!section2.heading || !section2.body || !/\?\s*$/.test(section2.heading)) continue;
53819
+ if (about && section2.body === about) continue;
53820
+ if (faq.some((f) => f.q.toLowerCase() === section2.heading.toLowerCase())) continue;
53821
+ faq.push({ q: section2.heading, a: section2.body, pageUrl: section2.pageUrl });
53681
53822
  }
53682
53823
  }
53683
53824
  const testimonials = dedupe2(pages.flatMap((p) => p.testimonials ?? []), CAP.testimonials);
@@ -60627,7 +60768,7 @@ function parseEvidence(path) {
60627
60768
  }
60628
60769
  }
60629
60770
  function blankMatrixRows(text2) {
60630
- const acceptance = text2.match(/## Implementation acceptance\s*([\s\S]*?)(?=\n##\s|$)/i)?.[1] ?? "";
60771
+ const acceptance = mobileUxSection(mobileUxDocumentBody(text2), "Implementation acceptance") ?? "";
60631
60772
  return /\|\s*(?:Ready|Empty|Add\s*\/\s*edit|Saved\s*\/\s*completed|Error\s*\/\s*recovery)\s*\|\s*\|\s*\|\s*\|\s*\|/i.test(acceptance);
60632
60773
  }
60633
60774
  function usesAppNetwork(source) {
@@ -61377,7 +61518,7 @@ var COMMANDS = [
61377
61518
  { name: "functions", summary: "inspect/invoke server functions and operate durable jobs or schedules", required: "list|describe|openapi|invoke | jobs list|get|cancel|retry|dlq | schedule describe|invoke [--app <a>] [--environment preview|production] [--release <id>]" },
61378
61519
  { name: "eject", summary: "take ownership of a shared flow module (auth|checkout|screens): copies its source into tier5-src/_ejected/ and repoints this package's imports at the copy. The ladder's escape hatch \u2014 for when the factory options cannot express what you need. AFTER EJECTING, CENTRAL FIXES NO LONGER REACH THIS APP, including future corrections to the fail-closed entitlement and confirm-the-session rules. The floor is unaffected: credentials still go only through the native auth actions and money only through the cashier rail.", required: "<auth|checkout|screens> [--content <dir>]" },
61379
61520
  { name: "validate", summary: "lint the content package (no push): fixed-contract TypeScript checking for tier5-src workers + schema/proto + token refs + component allowlist + $service bindings + strict-enum foot-guns + capability decls + worker boot dry-run + build/compile failures. Type issues are advisory warnings normally; --strict promotes them and hygiene warnings to errors. Exit 1 on any error. Off-repo, the CLI supplies its compiler and SDK declarations.", required: "[--content <dir>] [--strict]" },
61380
- { name: "test", summary: "DERIVED behavioral crawl \u2014 no test files to write. Boots each screen's real worker, replays its onAppear, then dispatches every action site the descriptor already declares (each from a fresh boot) and checks a narrow set of unambiguous invariants: no uncaught throw, never stuck on `loading`, no `$service:` token or NaN/undefined/[object Object] reaching visible text, no binding aimed at state the worker never defines, no nav param silently dropped, no dead nav target. Catches what validate/render cannot \u2014 a WRONG REDUCER passes both with ok:true. Runs the worker in a DEVICE-PARITY sandbox (no setTimeout/setInterval, as on JSC/WebView) so a bundle that would die on device fails here. Crawls on HERMETIC DATA by default \u2014 zero network: the app's own dev/net-mocks.json first (--net <file> overrides), then the config's CMS `seed:` rows, then a canned fixture of frozen real vertical responses shipped with the CLI (exercises code paths, not API-contract truth) \u2014 so the success/mapping path runs, not just the failure UI. Two boot-only HOSTILE passes (every list empty; every request 500) then check the failure/empty day \u2014 corpus-proven, so their findings are errors. NAV EDGES are followed too: every himi.open site ($item params resolved against row 0 of its list, $service against worker state) and worker _nav directive is harvested, and each baked target is booted WITH the params the source sends \u2014 detail screens finally run their content arm; edge findings are errors tagged edge:<source>. --native runs both real native decoders over the exact baked JSON bytes; --native ios or --native android selects one. It is explicit and needs the selected local toolchain. Default runs name missing native verdicts in the coverage ledger. --offline strips all data layers (the old reject-all axis); --no-hostile skips the hostile passes. --record (EXPLICIT, never automatic) refreshes dev/net-mocks.json from live reads first: it boots each screen (load + root onAppear ONLY \u2014 nothing the app's own launch would not fire; the tap/action crawl never sees the live transport, and only proven reads \u2014 GET, or read-shaped POST (/query, /search, /list*, \u2026) \u2014 ever reach the wire), freezes the round-trips, then crawls hermetically on the fresh fixture; record against a QA site, not production. Nodes it cannot judge headless are named, never silently passed. Exit 1 on any error. The CLI carries the authoring runtime off-repo; no per-app SDK install is required. --browser runs the whole loop (build \u2192 extract \u2192 validate \u2192 render \u2192 test) in YOUR browser instead of here, so the source never reaches a pod: it opens one screen's worker + descriptor in a tab, blocks on a loopback callback, and prints the report it posts back. One screen per tab \u2014 name it. --no-open prints the URL rather than launching a browser (SSH); --timeout <ms> bounds the wait, which matters because the page has no execution deadline of its own and an infinite loop in the worker hangs the tab.", required: "[<screenId>\u2026] [--content <dir>] [--net <file>] [--budget <ms>] [--deep [n]] [--record] [--native [ios|android]] [--no-shots] [--offline] [--no-hostile] [--all-screens] [--browser] [--no-open] [--timeout <ms>] [--json]" },
61521
+ { name: "test", summary: "DERIVED behavioral crawl \u2014 no test files to write. Boots each screen's real worker, replays its onAppear, then dispatches every action site the descriptor already declares (each from a fresh boot) and checks a narrow set of unambiguous invariants: no uncaught throw, never stuck on `loading`, no `$service:` token or NaN/undefined/[object Object] reaching visible text, no binding aimed at state the worker never defines, no nav param silently dropped, no dead nav target. Catches what validate/render cannot \u2014 a WRONG REDUCER passes both with ok:true. Runs the worker in a DEVICE-PARITY sandbox (no setTimeout/setInterval, as on JSC/WebView) so a bundle that would die on device fails here. Crawls on HERMETIC DATA by default \u2014 zero network: the app's own dev/net-mocks.json first (--net <file> overrides), then the config's CMS `seed:` rows, then a canned fixture of frozen real vertical responses shipped with the CLI (exercises code paths, not API-contract truth) \u2014 so the success/mapping path runs, not just the failure UI. Two boot-only HOSTILE passes (every list empty; every request 500) then check the failure/empty day \u2014 corpus-proven, so their findings are errors. NAV EDGES are followed too: every himi.open site ($item params resolved against row 0 of its list, $service against worker state) and worker _nav directive is harvested, and each baked target is booted WITH the params the source sends \u2014 detail screens finally run their content arm; edge findings are errors tagged edge:<source>. --native runs both real native decoders over the exact baked JSON bytes; --native ios or --native android selects one. It is explicit, runs only from a Himalaya SOURCE CHECKOUT (the decoder oracles are that tree's own HimalayaCore Swift package and Gradle harness, which a published install does not ship), and needs the selected local toolchain. From a source checkout, default runs name missing native verdicts in the coverage ledger; elsewhere the ledger stays silent about them rather than offering a remedy you cannot run. --offline strips all data layers (the old reject-all axis); --no-hostile skips the hostile passes. --record (EXPLICIT, never automatic) refreshes dev/net-mocks.json from live reads first: it boots each screen (load + root onAppear ONLY \u2014 nothing the app's own launch would not fire; the tap/action crawl never sees the live transport, and only proven reads \u2014 GET, or read-shaped POST (/query, /search, /list*, \u2026) \u2014 ever reach the wire), freezes the round-trips, then crawls hermetically on the fresh fixture; record against a QA site, not production. Nodes it cannot judge headless are named, never silently passed. Exit 1 on any error. The CLI carries the authoring runtime off-repo; no per-app SDK install is required. --browser runs the whole loop (build \u2192 extract \u2192 validate \u2192 render \u2192 test) in YOUR browser instead of here, so the source never reaches a pod: it opens one screen's worker + descriptor in a tab, blocks on a loopback callback, and prints the report it posts back. One screen per tab \u2014 name it. --no-open prints the URL rather than launching a browser (SSH); --timeout <ms> bounds the wait, which matters because the page has no execution deadline of its own and an infinite loop in the worker hangs the tab.", required: "[<screenId>\u2026] [--content <dir>] [--net <file>] [--budget <ms>] [--deep [n]] [--record] [--native [ios|android]] [--no-shots] [--offline] [--no-hostile] [--all-screens] [--browser] [--no-open] [--timeout <ms>] [--json]" },
61381
61522
  { name: "ux", summary: "audit or record mobile UX evidence. `audit` checks the public-safe contract, seeded blank acceptance rows, placeholder source copy, app-specific mocks, and iOS/Android audit records. It warns while existing templates migrate; `--strict` promotes missing evidence. `record` stores checks performed on an installed iOS/Android build: `--checks` is required for passed|failed and empty for unavailable, which drove nothing and needs a `--reason` instead.", required: "audit [--content <dir>] [--strict] [--json] | record --content <dir> --platform ios|android --status passed|failed|unavailable [--checks back,keyboard,...] [--reason <text>]" },
61382
61523
  { name: "render", summary: "headless-render a screen with the web renderer: render-tree + diagnostics, optional --png <file>, --scheme dark, --tree. Local by default (content dir). Pass --remote --app --release to hit GET /v1/preview/\u2026/render on the serve plane after a push. Boots the worker so $service bindings resolve; --static raw bindings; --live real Wix fetches; --state injects worker state; --input supplies pre-boot navigation params; --width/--device for PNG viewport; --rtl for dir=rtl + $env:isRTL.", required: "<screenId> [--content <dir>] [--scheme dark] [--png <file>] [--tree] [--static] [--live] [--state <json|file>] [--input <json|file>] [--width <px> | --device <name>] [--rtl] [--remote --app --release]" },
61383
61524
  { name: "cms", summary: "inspect, provision, and seed the app's CMS (Wix Data) collections, declared as a `cms` block in config.ts. `query` is READ-ONLY \u2014 by default it fetches ONLY the physical ids `config.ts` declares (item counts + schema drift) and reports `siteScanned: false`; it never lists the site, so an UNDECLARED `himi_<app>_` collection this app owns is not reached without `--all`. `--all` SCANS the site \u2014 every collection it lists is labeled `owned`/`adopted`/`foreign`, and it describes every foreign one \u2014 id, non-system field keys + types, and the site's read role \u2014 so an app with no `cms` block at all can DISCOVER a collection to adopt (`--all` is the one case where a missing `cms` block is not 'nothing to do'). Foreign collections are described up to a cap and are never item-counted (`itemCount: null`), because a row count is one query each; `foreign.count` is how many the scan LISTED \u2014 NOT a site census, since the list endpoint is paged and is not followed \u2014 and `foreign.truncated` says how many of those went undescribed, while `unlistedCollections` reports what the listing itself left out (from Wix's own paging total). The scan is OWNER-scoped \u2014 it needs --site <msid> (or WIX_SITE_ID) plus owner auth via `wix token`; a visitor token can read a public collection's ITEMS but can never enumerate collections, so there is no tokenless discovery path. `create` reconciles declared collections additively and idempotently, and REFUSES any id outside the app's `himi_<app>_` namespace, so it is safe against a production site. It reports declared-vs-site `permissionDrift` but never writes permissions unless asked with --apply-permissions, so a field reconcile can't silently revert access control changed in the dashboard. `seed` inserts the git-tracked `seed:` rows into EMPTY owned collections (--force to add to a non-empty one; --ai writes placeholder rows plus a refinement prompt for you to review). Adopted collections (declared by `wixId`) are read-only and never created, updated, or seeded. Site from --site <msid> or WIX_SITE_ID; owner auth via `wix token`.", required: "query|create|seed [--site <msid>] [--content <dir>] [--all] [--dry-run] [--force] [--allow-destructive] [--apply-permissions] [--only <id>] [--ai] [--count <n>]" },
@@ -61557,6 +61698,40 @@ function previewLogsUrl(serveBase, app, release) {
61557
61698
  function previewTelemetryUrl(serveBase, app, release) {
61558
61699
  return `${serveBase.replace(/\/$/, "")}/v1/telemetry/events?app=${encodeURIComponent(app)}&release=${encodeURIComponent(release)}`;
61559
61700
  }
61701
+ function diagnoseBody(body) {
61702
+ const findings = body.findings;
61703
+ if (!Array.isArray(findings)) return body;
61704
+ const messageOf2 = (entry) => {
61705
+ const message = entry && typeof entry === "object" ? entry.message : void 0;
61706
+ return typeof message === "string" ? message : void 0;
61707
+ };
61708
+ const shown2 = new Set(findings.map(messageOf2).filter((message) => message !== void 0));
61709
+ const folded = (value) => Array.isArray(value) && value.length > 0 && value.every((entry) => {
61710
+ const message = messageOf2(entry);
61711
+ return message !== void 0 && shown2.has(message);
61712
+ });
61713
+ const out = {};
61714
+ const emptied = [];
61715
+ for (const [key2, value] of Object.entries(body)) {
61716
+ const fold = (key2 === "issues" || key2 === "failures" || key2 === "warnings") && folded(value);
61717
+ if (fold) emptied.push(key2);
61718
+ out[key2] = fold ? [] : value;
61719
+ }
61720
+ if (emptied.length) out.foldedIntoFindings = emptied;
61721
+ return out;
61722
+ }
61723
+ var PREVIEW_TOKEN_NOTE = "the preview link carries a bearer with a limited lifetime, and this call has no usable expiry for it \u2014 the plane stated none, or the one it stated is unreadable, already past, or beyond the hour this CLI will reason about. Run the `previewRenew` command printed beside this note \u2014 from the same plane and credentials as this call (any `--serve-url`/`--control-url`/token flags you passed) \u2014 for a fresh one.";
61724
+ function previewTokenNote(expiresAt, now = Date.now()) {
61725
+ const at = expiresAt ? Date.parse(expiresAt) : NaN;
61726
+ if (!Number.isFinite(at)) return PREVIEW_TOKEN_NOTE;
61727
+ const seconds = (at - now) / 1e3;
61728
+ if (seconds <= 0 || seconds > 3600) return PREVIEW_TOKEN_NOTE;
61729
+ const minutes = Math.max(1, Math.round(seconds / 60));
61730
+ return `the preview link carries a bearer that expires at ${expiresAt} \u2014 about ${minutes} minute${minutes === 1 ? "" : "s"} from now; run the \`previewRenew\` command printed beside this note \u2014 from the same plane and credentials as this call (any \`--serve-url\`/\`--control-url\`/token flags you passed) \u2014 for a fresh one.`;
61731
+ }
61732
+ function previewRenewCommand(app, release, target, scope = {}) {
61733
+ return `himi preview --app ${app} --release ${release}` + (scope.screen ? ` --screen ${scope.screen}` : "") + (scope.surface ? ` --surface ${scope.surface}` : "") + (target !== "local" ? ` --target ${target}` : "");
61734
+ }
61560
61735
  function previewPlayerLink(app, release, screenId) {
61561
61736
  return buildPlayerPreviewUrl({ app, releaseId: release, screenId, source: "cli" });
61562
61737
  }
@@ -62569,7 +62744,21 @@ himi v${cliVersion()} \u2014 run \`himi --version\`. If authoring commands fail
62569
62744
  ...str5(flags.notes) ? { notes: str5(flags.notes) } : {},
62570
62745
  ...tplVis.value ? { visibility: tplVis.value } : {}
62571
62746
  };
62572
- const templateBodyBytes = publishReleaseBodyBytes(payload, publishMeta);
62747
+ let sendPayload = payload;
62748
+ let templateBodyBytes = publishReleaseBodyBytes(payload, publishMeta);
62749
+ let sourceGzChars;
62750
+ if (templateBodyBytes > MAX_PUBLISH_BODY_BYTES) {
62751
+ const { gzipSync: gzipSync2 } = await import("node:zlib");
62752
+ const { source: plainSource, ...rest } = payload;
62753
+ const sourceGz = gzipSync2(Buffer.from(JSON.stringify(plainSource), "utf8"), { level: 9 }).toString("base64");
62754
+ sourceGzChars = sourceGz.length;
62755
+ const gzipped = { ...rest, sourceGz };
62756
+ const gzBytes = publishReleaseBodyBytes(gzipped, publishMeta);
62757
+ if (gzBytes <= MAX_PUBLISH_BODY_BYTES) {
62758
+ sendPayload = gzipped;
62759
+ templateBodyBytes = gzBytes;
62760
+ }
62761
+ }
62573
62762
  if (templateBodyBytes > MAX_PUBLISH_BODY_BYTES) {
62574
62763
  io.error(JSON.stringify({
62575
62764
  ok: false,
@@ -62586,13 +62775,19 @@ himi v${cliVersion()} \u2014 run \`himi --version\`. If authoring commands fail
62586
62775
  // separately rather than summed into a figure that double-counts it.
62587
62776
  sourceChars: JSON.stringify(payload.source).length,
62588
62777
  appSourceBytes: templateSourceB64.length,
62589
- hint: "trim the template's source tree \u2014 a template carries its tree twice (the public catalog view and the private archive), and neither is optional, so there is no flag that publishes without them"
62778
+ // The catalog view is reported COMPRESSED as well, because "trim the tree" was the
62779
+ // wrong instruction and cost real time: the collector already drops binaries,
62780
+ // symlinks and dist/node_modules/.git/.himi, so there is rarely anything to trim.
62781
+ // What the two figures actually say is whether compression can still save you --
62782
+ // `sourceGzChars` is what this publish already tried and what remains after it.
62783
+ sourceGzChars,
62784
+ hint: "the catalog view is already sent gzipped when the plain body would not fit, and it still does not: the remainder is the release itself (screens, bundles, assets) plus the private archive, which no flag omits. Cut what the app ships, not the source tree \u2014 the collector already excludes binaries, symlinks, dist/, node_modules/, .git/ and .himi/."
62590
62785
  }));
62591
62786
  return 1;
62592
62787
  }
62593
62788
  let r;
62594
62789
  try {
62595
- r = await client3().publishRelease(payload, publishMeta);
62790
+ r = await client3().publishRelease(sendPayload, publishMeta);
62596
62791
  } catch (e) {
62597
62792
  const target = targetName();
62598
62793
  io.error(JSON.stringify(serveUnreachableError(target, serveBase(), e.message)));
@@ -62602,6 +62797,17 @@ himi v${cliVersion()} \u2014 run \`himi --version\`. If authoring commands fail
62602
62797
  print({ ok: false, status: r.status, body: r.json ?? r.text });
62603
62798
  return 1;
62604
62799
  }
62800
+ if (sendPayload.sourceGz && r.json?.template !== true) {
62801
+ io.error(JSON.stringify({
62802
+ ok: false,
62803
+ template: name,
62804
+ app: templateAppId,
62805
+ published: false,
62806
+ error: "serve_plane_ignored_source_gz",
62807
+ hint: `this plane accepted the publish but did not score it as a template, which means it predates compressed catalog views (\`sourceGz\`). A release WAS created for ${templateAppId} as an ordinary app \u2014 it carries no template source and will not appear in \`himi template list --remote\`. Upgrade the serve plane, then publish again; the republish repairs it.`
62808
+ }));
62809
+ return 1;
62810
+ }
62605
62811
  const releaseId = r.json?.releaseId ?? payload.manifest.releaseId;
62606
62812
  const targetFlag = str5(flags.target) ? ` --target ${str5(flags.target)}` : "";
62607
62813
  print({
@@ -63183,9 +63389,11 @@ himi v${cliVersion()} \u2014 run \`himi --version\`. If authoring commands fail
63183
63389
  result.shots = { ...sp.written.length ? { dir: sp.dir } : {}, written: sp.written.length, ...sp.unavailable ? { unavailable: true } : {} };
63184
63390
  }
63185
63391
  }
63186
- const { assembleLedger: assembleLedger2, trendAndStore: trendAndStore2, formatLedger: formatLedger2, diffAndStoreObservations: diffAndStoreObservations2, formatDelta: formatDelta2 } = await Promise.resolve().then(() => (init_coverage(), coverage_exports));
63187
- const ledger = assembleLedger2(result);
63188
- const trend = trendAndStore2(dir, ledger);
63392
+ const { assembleLedger: assembleLedger2, trendAndStore: trendAndStore2, formatLedger: formatLedger2, diffAndStoreObservations: diffAndStoreObservations2, formatDelta: formatDelta2, suppressedNativeKeys: suppressedNativeKeys2 } = await Promise.resolve().then(() => (init_coverage(), coverage_exports));
63393
+ const { nativeOracleShipped: nativeOracleShipped2 } = await Promise.resolve().then(() => (init_native_decode(), native_decode_exports));
63394
+ const nativeOracle = nativeOracleShipped2();
63395
+ const ledger = assembleLedger2(result, { nativeOracle });
63396
+ const trend = trendAndStore2(dir, ledger, suppressedNativeKeys2(nativeOracle));
63189
63397
  const delta = diffAndStoreObservations2(dir, result.observations ?? {});
63190
63398
  const legacy = { ...result, ledger, ...trend ? { trend } : {}, ...delta ? { delta } : {}, ...recorded ? { recorded } : {} };
63191
63399
  const release = await loadReleaseFromDir(app, out);
@@ -64017,7 +64225,16 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
64017
64225
  ...loopbackWarning ? { warning: loopbackWarning } : {},
64018
64226
  ...report.issues ? { issues: report.issues } : {},
64019
64227
  previewWeb: base ? previewWebUrl(base, app, release, void 0, void 0, previewToken) : null,
64020
- ...previewToken ? { previewTokenExpiresAt: previewTokenExpiresAt ?? null } : {},
64228
+ // A timestamp alone leaves the reader to work out both how long they have and how to
64229
+ // get another one. Print the duration and the re-mint command beside it (#2502 item 3).
64230
+ // No `mintedHere` guard is needed here, unlike `himi preview`: this bearer can only
64231
+ // have come from the publish response two hundred lines up, so THIS call minted it by
64232
+ // construction. `push` has no `--preview-token` to paste one in with.
64233
+ ...previewToken ? {
64234
+ previewTokenExpiresAt: previewTokenExpiresAt ?? null,
64235
+ previewRenew: previewRenewCommand(app, release, targetName()),
64236
+ previewTokenNote: previewTokenNote(previewTokenExpiresAt)
64237
+ } : {},
64021
64238
  // RFC 0051 — the surfaces THIS RELEASE targets, read from the manifest the
64022
64239
  // push just published (never a re-read of the local file: review found the
64023
64240
  // two can disagree — e.g. an older CLI whose local parser drops a key the
@@ -64478,6 +64695,8 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
64478
64695
  }
64479
64696
  let previewToken = str5(flags["preview-token"]);
64480
64697
  let previewTokenExpiresAt;
64698
+ let mintedHere = false;
64699
+ let mintFailure;
64481
64700
  if (!previewToken) {
64482
64701
  const target = resolveTarget({
64483
64702
  target: str5(flags.target),
@@ -64490,15 +64709,24 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
64490
64709
  telemetryToken: str5(flags["telemetry-token"]),
64491
64710
  controlToken: str5(flags["control-token"])
64492
64711
  });
64493
- if (target.controlToken) {
64712
+ if (!target.controlToken) {
64713
+ mintFailure = `this target has no stored control credential, so none was requested \u2014 run \`himi login --target ${targetName()}\`, or pass --control-token`;
64714
+ } else {
64494
64715
  try {
64495
64716
  const minted = await new ServeCliClient(target).previewToken(app, release);
64496
- if (minted.ok && minted.json && typeof minted.json === "object") {
64717
+ if (!minted.ok) {
64718
+ mintFailure = `the plane refused the mint request (HTTP ${minted.status}). Your credential reached it, so this is the plane's answer, not your session`;
64719
+ } else if (!minted.json || typeof minted.json !== "object") {
64720
+ mintFailure = "the plane accepted the mint request but its answer was not a JSON object, so no token could be read. Your credential is fine";
64721
+ } else {
64497
64722
  const body = minted.json;
64498
64723
  previewToken = typeof body.previewToken === "string" ? body.previewToken : void 0;
64499
64724
  previewTokenExpiresAt = typeof body.previewTokenExpiresAt === "string" ? body.previewTokenExpiresAt : void 0;
64725
+ mintedHere = previewToken !== void 0;
64726
+ if (!mintedHere) mintFailure = "the plane answered the mint request without a token \u2014 HIMALAYA_PREVIEW_TOKEN_SECRET may be unconfigured there. Your credential is fine";
64500
64727
  }
64501
- } catch {
64728
+ } catch (e) {
64729
+ mintFailure = `the mint request could not be sent: ${e.message}. Your credential was never judged`;
64502
64730
  }
64503
64731
  }
64504
64732
  }
@@ -64509,7 +64737,22 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
64509
64737
  ...screen ? { screen } : {},
64510
64738
  ...surface ? { surface } : {},
64511
64739
  previewWeb: base ? previewWebUrl(base, app, release, screen, surface, previewToken) : null,
64512
- ...previewTokenExpiresAt ? { previewTokenExpiresAt } : {},
64740
+ // Gated on the MINT, like the two fields below: a plane that returns an expiry but no
64741
+ // token would otherwise print a lifetime for a bearer the same envelope says does not
64742
+ // exist.
64743
+ ...mintedHere && previewTokenExpiresAt ? { previewTokenExpiresAt } : {},
64744
+ // Same as the push envelope: the expiry alone leaves the reader to work out both how
64745
+ // long they have and how to get another link. `himi preview` re-mints, so the renew is
64746
+ // this very command — say so rather than making them find it again (#2502 item 3).
64747
+ //
64748
+ // Gated on the MINT, not on the expiry: a plane that mints a bearer but returns no
64749
+ // `previewTokenExpiresAt` would otherwise withhold the guidance in exactly the case
64750
+ // where the reader has no timestamp to reason from — while a bearer the caller pasted
64751
+ // in with `--preview-token` is one this command cannot promise to reissue.
64752
+ ...mintedHere ? {
64753
+ previewRenew: previewRenewCommand(app, release, targetName(), { screen, surface }),
64754
+ previewTokenNote: previewTokenNote(previewTokenExpiresAt)
64755
+ } : previewToken || !mintFailure ? {} : { previewTokenNote: `no preview bearer was minted \u2014 ${mintFailure}. The link above carries no \`pvt\`, so it opens a LIVE release and will not open a draft.` },
64513
64756
  previewPlayerDebug: previewPlayerDebugLink(app, release, screen, previewToken),
64514
64757
  context: base ? previewContextUrl(base, app, release, screen) : null,
64515
64758
  diagnose: base && screen ? previewDiagnoseUrl(base, app, release, screen) : null,
@@ -64687,8 +64930,16 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
64687
64930
  if (!r.ok) throw Object.assign(new Error("crashes_read_failed"), { response: r });
64688
64931
  out.crashes = r.json?.crashes ?? [];
64689
64932
  }
64690
- if ((out.events?.length ?? 0) === 0 && (out.logs?.length ?? 0) === 0 && !includeSummary && !includeCrashes) {
64691
- out.note = "no telemetry yet \u2014 open the web preview or Himi Player link for this release, then retry or use --follow";
64933
+ const eventCount = out.events?.length ?? 0;
64934
+ const logCount = out.logs?.length ?? 0;
64935
+ const narrowed = !!(level || str5(flags.screen) || str5(flags.screenId) || str5(flags.type) || str5(flags.event) || flags["crashes-only"] === true || flags.crashesOnly === true || num(flags.limit) !== void 0 || eventSince !== void 0 || logSince !== void 0);
64936
+ const wholeChannels = includeEvents && includeLogs && !narrowed;
64937
+ if (wholeChannels && eventCount === 0 && logCount === 0 && !includeSummary && !includeCrashes) {
64938
+ out.note = "no events and no worker console output on this release \u2014 open the web preview or Himi Player link for it, then retry or use --follow";
64939
+ } else if (wholeChannels && eventCount > 0 && logCount === 0) {
64940
+ out.note = "events arrived but the worker console channel is empty \u2014 commonly because the worker never called `console.*`, though an empty channel can also mean the lines aged out or never made it through ingestion. What the CLI can say is that this channel returned nothing, not that the app was never opened";
64941
+ } else if (narrowed && eventCount === 0 && logCount === 0 && !includeSummary && !includeCrashes) {
64942
+ out.note = "nothing matched this query. Drop --screen/--type/--level/--since/--limit/--crashes-only to see the whole event and console channels for this release";
64692
64943
  }
64693
64944
  return out;
64694
64945
  };
@@ -64769,7 +65020,7 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
64769
65020
  if (r.status !== 422) return printServeFailure(r);
64770
65021
  }
64771
65022
  const body = r.json ?? {};
64772
- print(body);
65023
+ print(diagnoseBody(body));
64773
65024
  return body.ok === false ? 1 : 0;
64774
65025
  }
64775
65026
  case "deeplinks": {
@@ -64825,8 +65076,12 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
64825
65076
  print({ ok: false, status: r.status, body: r.json ?? r.text });
64826
65077
  return 1;
64827
65078
  }
64828
- const logs = byLevel(r.json?.logs ?? []);
64829
- print({ ok: true, logs, ...logs.length === 0 ? { note: "no logs yet \u2014 a worker emits telemetry only once someone opens the app/preview on a device or web with a session. Boot-time console.log is visible via `himi validate` / `himi render`." } : {} });
65079
+ const rawLogs = r.json?.logs ?? [];
65080
+ const logs = byLevel(rawLogs);
65081
+ const narrowed = !!(level || str5(flags.screen) || str5(flags.screenId) || num2(flags.since) !== void 0 || num2(flags.limit) !== void 0);
65082
+ const scope = release ? "this release" : "this app";
65083
+ const note = logs.length > 0 ? void 0 : narrowed ? `no worker \`console.*\` lines matched this query${level && rawLogs.length ? ` (${rawLogs.length} line(s) were hidden by --level ${level})` : ""}. Drop --level/--screen/--since/--limit to see the whole console channel for ${scope}; the telemetry EVENT stream is a separate channel, read with \`himi telemetry\`.` : `no worker \`console.*\` output on ${scope}. This is the console channel \u2014 distinct from the telemetry EVENT stream, which \`himi telemetry --app <app> --release <id>\` reads: ${scope} can carry many events and no console lines \u2014 commonly because the worker never logged any, though an empty channel can also mean the lines aged out or never made it through ingestion. Boot-time \`console.log\` is visible via \`himi validate\` / \`himi render\`.`;
65084
+ print({ ok: true, logs, ...note ? { note } : {} });
64830
65085
  return 0;
64831
65086
  }
64832
65087
  const interval = num2(flags.interval) ?? 2e3;
@@ -66108,12 +66363,14 @@ export {
66108
66363
  ConfigLoadError,
66109
66364
  PLACEHOLDER_CLIENT_ID,
66110
66365
  PLACEHOLDER_SITE_URL,
66366
+ PREVIEW_TOKEN_NOTE,
66111
66367
  PUBLIC_WIX_API_BASE,
66112
66368
  autoClaimSlug,
66113
66369
  buildDoctorReport,
66114
66370
  compareVersions2 as compareVersions,
66115
66371
  decodeControlTokenClaims,
66116
66372
  defaultTargetForCommand,
66373
+ diagnoseBody,
66117
66374
  doctorVersionReport,
66118
66375
  foreignPlaneError,
66119
66376
  formatSdkVersionLine,
@@ -66136,8 +66393,10 @@ export {
66136
66393
  previewPlayerDebugLink,
66137
66394
  previewPlayerLink,
66138
66395
  previewRenderUrl,
66396
+ previewRenewCommand,
66139
66397
  previewScreensUrl,
66140
66398
  previewTelemetryUrl,
66399
+ previewTokenNote,
66141
66400
  previewWebUrl,
66142
66401
  probeControlToken,
66143
66402
  probeReachable,
@@ -236,7 +236,7 @@ browser.
236
236
  | `--no-shots` | Skip the per-screen PNGs written to `.himi/shots/`. |
237
237
  | `--net <file>` | Use an explicit net-mocks file instead of the app's own. |
238
238
  | `--budget <ms>` | Per-action settle budget (default 2000). |
239
- | `--native` | Also decode every baked descriptor with the real native decoders; bare `--native` runs whichever toolchains are present, and `--native ios` or `--native android` selects one. |
239
+ | `--native` | Also decode every baked descriptor with the real native decoders; bare `--native` runs whichever toolchains are present, and `--native ios` or `--native android` selects one. **Source-checkout only** — the decoder oracles are that tree's own HimalayaCore Swift package and Gradle harness, which a published install does not ship, so it cannot run this at all (and its coverage ledger stays silent about native decoding rather than suggesting it). |
240
240
  | `--all-screens` | Crawl every templated instance instead of one representative per structure. |
241
241
  | `--browser` / `--no-open` / `--timeout <ms>` | Run the loop in your own browser tab (one screen per run; refuses record/net/offline/hostile/deep/budget/shots/native). |
242
242
  | `himi test <screenId>…` | Focus one or more screens — the record and shots passes scope with them. |
@@ -137,18 +137,32 @@ nothing, so it takes no checks and needs a `--reason` instead.
137
137
 
138
138
  ### Strict audit contract
139
139
 
140
- **Two commands read these markers.** `himi validate` reports a missing one as a **warning**, so a
141
- rewrite that drops a required string is caught in the loop you are already running rather than at
140
+ **Two commands read this contract.** `himi validate` reports a gap as a **warning**, so a rewrite
141
+ that drops something the gate needs is caught in the loop you are already running rather than at
142
142
  the end of it. `himi validate --strict` and `himi ux audit --strict` both promote that warning to a
143
- **release blocker**. Both read the same exact markers, and each gate **substring-matches** these
144
- three phrases — so use them verbatim: a semantically equivalent rewrite ("each state exposes exactly
145
- one primary call to action") fails the same as omitting them:
143
+ **release blocker**. Both read the same markers. The two HEADINGS are matched verbatim — other
144
+ tooling addresses the document by them, so a heading really is the thing itself:
146
145
 
147
146
  - The heading `## Implementation acceptance`.
148
147
  - The heading `## Evidence`.
149
- - The phrase `one primary CTA per state`.
150
148
 
151
- The fourth marker, **width evidence**, is graded by **content, not by phrase** — it is a
149
+ The **primary-CTA** marker is satisfied **either** structurally *or* by the legacy phrase,
150
+ because a semantically equivalent rewrite — "each state exposes exactly one primary call to
151
+ action" — used to fail exactly like omitting it, and four of five independent testers wrote a
152
+ better document, were refused, and pasted the sentence back:
153
+
154
+ - *structurally*: `## Implementation acceptance` carries a `Primary CTA` column, has a row for
155
+ every state you declare in a `… states:` bullet under `## Screen contract`, and names exactly one
156
+ CTA per row (a comma, a semicolon or a spaced slash reads as two; "Save and close", "Save &
157
+ Close" and "Save + Continue" are each one label and pass). The row check reads a `states:` bullet
158
+ only when every item is a **name** (`ready, empty, destructive-confirm`); a bullet answered with
159
+ prose declares nothing and the row check is skipped rather than guessed at — no rule can tell a
160
+ short description from a name, and inventing states from prose would fail exactly the documents
161
+ that took the most care.
162
+ - *or literally*: the phrase `one primary CTA per state` appears anywhere. It is not deprecated —
163
+ a document that passes today keeps passing.
164
+
165
+ The remaining marker, **width evidence**, is graded by **content, not by phrase** — it is a
152
166
  fact about what you rendered rather than a string, so there is nothing to paste. Satisfy it either
153
167
  way:
154
168
 
@@ -202,6 +216,13 @@ no wider width, two widths too far apart to be one statement, or no number spell
202
216
  says so alongside the height-axis rule if that is what discarded your numbers. Read the branch it
203
217
  gave you rather than pasting the example: the example is a form that works, not the only one.
204
218
 
219
+ **HTML comments are stripped before any of this runs.** The scaffold ships a `<!-- LOAD-BEARING -->`
220
+ note quoting the load-bearing strings verbatim, right beside the line telling you to replace the
221
+ example rows — so the note is guidance and never an answer: a document that kept the note and
222
+ deleted the prose satisfies nothing. An **unterminated** `<!--` swallows the rest of the file here
223
+ and in any renderer alike, so that is reported on its own rather than as four headings "missing"
224
+ that you can plainly see.
225
+
205
226
  A `MOBILE-UX.md` that is **absent or empty** is not four missing markers. `himi ux audit` reports it
206
227
  once, as a missing contract; `himi validate` stays silent about it entirely. Asking for the document
207
228
  itself is the audit's job — most apps never adopt this contract, and a warning on every one of them
@@ -326,6 +326,10 @@ himi test <screenId> --record # refresh dev/net-mocks.json from live boot rea
326
326
  himi test <screenId> --native # run BOTH real native decoders over the baked JSON bytes
327
327
  ```
328
328
 
329
+ `--native` runs only from a Himalaya **source checkout**: the decoder oracles are that tree's own HimalayaCore Swift
330
+ package and Gradle harness, which a published/off-repo install does not ship. Working off-repo,
331
+ skip it — the coverage ledger will not ask you for it either.
332
+
329
333
  **Then see it.** `himi render <screenId> --png <file>` per scheme is the fast visual check,
330
334
  and `himi push` gives you a draft preview to open in a browser. Neither drives a real
331
335
  device, which is what `himi ux record` is for: it records per platform that a drive
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/himalaya-cli",
3
- "version": "0.854.0",
3
+ "version": "0.856.0",
4
4
  "type": "module",
5
5
  "description": "himi — the Himalaya authoring CLI. Build a Tier-5 content package on your own machine, push it to the production serve as a draft, preview it on web + Himi Player, and read worker logs back — the remote dev loop. Wraps @himalaya/serve-cli for transport.",
6
6
  "keywords": [
@@ -63,5 +63,5 @@
63
63
  "artifactId": "himalaya-cli"
64
64
  }
65
65
  },
66
- "falconPackageHash": "5adfd6ba9551cfc305ff63e7d506c64fe0b3874c74bd3561fe465869"
66
+ "falconPackageHash": "82a824b69c0914106e80e0fb205b046bd8e669b53f46887b3adf0eed"
67
67
  }