@amityco/social-plus-vise 1.4.6 → 1.6.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.
@@ -13,10 +13,12 @@ import { clarifyDimensions, clarifyGuidance } from "../requestReadiness.js";
13
13
  import { recommendSolutionPath } from "../solutionPath.js";
14
14
  import { recommendUIKitCustomization } from "../uikitCustomization.js";
15
15
  import { packageVersion } from "../version.js";
16
+ import { readFlowState } from "../flow.js";
16
17
  import { DESIGN_CONTRACT_CONFIRMATION_ANSWER_ID, buildDesignBrief, designContractConfirmationFromAnswers, designPreviewPath, readDesignContract, } from "./design.js";
17
18
  import { installedBlockProvidedCapabilities } from "./blocks.js";
18
19
  import { inspectProject, validateSetup } from "./project.js";
19
20
  import { readCreativeSelection } from "./creative.js";
21
+ import { readRuntimeProofWaiver, hasExecutableSmokeMarker } from "./smoke.js";
20
22
  import { assessUxHarness, buildExperienceReport, buildUxHarness, readUxHarness, } from "./uxHarness.js";
21
23
  const complianceDirName = "sp-vise";
22
24
  const attestationsDirName = "attestations";
@@ -337,7 +339,8 @@ export async function initCompliance(repoPath, request, surfacePath, answers = {
337
339
  const designReview = designReviewFor(repoRoot, designContract, answers);
338
340
  const acceptedDesignContract = designReview.status === "accepted" ? designContract : null;
339
341
  const storedCreativeSelection = await readCreativeSelection(repoRoot);
340
- const creativeSelection = storedCreativeSelection && creativeSelectionAppliesToRequest(storedCreativeSelection, request)
342
+ const flow = await readFlowState(repoRoot);
343
+ const creativeSelection = storedCreativeSelection && creativeSelectionAppliesToRequest(storedCreativeSelection, request, flow)
341
344
  ? storedCreativeSelection
342
345
  : undefined;
343
346
  if (options.allowUnresolvedIntake !== true) {
@@ -704,17 +707,32 @@ function uxHarnessSummary(harness) {
704
707
  advisory_only: "UX Harness guidance does not change deterministic compliance rules or exit codes in the MVP.",
705
708
  };
706
709
  }
707
- function creativeSelectionAppliesToRequest(selection, request) {
710
+ function creativeSelectionAppliesToRequest(selection, request, flow) {
708
711
  const normalizedRequest = normalizeCreativeRequest(request);
709
712
  const original = normalizeCreativeRequest(selection.source.request);
710
713
  const planRequest = normalizeCreativeRequest(selection.implementationContext.planRequest);
711
714
  if (!normalizedRequest || !original) {
712
715
  return false;
713
716
  }
717
+ const requestedOutcome = resolveOutcome(request, {});
718
+ if (omittedCreativeOutcomeSet(flow).has(requestedOutcome)) {
719
+ return false;
720
+ }
714
721
  return (normalizedRequest === original ||
715
722
  normalizedRequest === planRequest ||
716
723
  normalizedRequest.includes(original) ||
717
- original.includes(normalizedRequest));
724
+ original.includes(normalizedRequest) ||
725
+ (Boolean(selection.selectedVariant?.id) && creativeSelectionOutcomeSet(selection).has(requestedOutcome)));
726
+ }
727
+ function omittedCreativeOutcomeSet(flow) {
728
+ return new Set((flow?.blueprint?.omitted ?? []).map((item) => item.outcome).filter((outcome) => typeof outcome === "string"));
729
+ }
730
+ function creativeSelectionOutcomeSet(selection) {
731
+ const outcomes = new Set();
732
+ for (const hint of selection.implementationContext.surfaceHints ?? []) {
733
+ outcomes.add(hint.outcome);
734
+ }
735
+ return outcomes;
718
736
  }
719
737
  function normalizeCreativeRequest(value) {
720
738
  return value.toLowerCase().replace(/[^a-z0-9\s]+/g, " ").replace(/\s+/g, " ").trim();
@@ -941,6 +959,18 @@ export async function checkCompliance(repoPath, options = {}) {
941
959
  });
942
960
  continue;
943
961
  }
962
+ if (hasDeterministicChecks && deterministic?.waived) {
963
+ results.push({
964
+ ...checkRuleIdentity(rule.id),
965
+ title: rule.title,
966
+ severity: rule.severity,
967
+ status: "proof-waived",
968
+ reason: deterministic.reason,
969
+ evidence: deterministic.evidence,
970
+ rationale: rule.rationale,
971
+ });
972
+ continue;
973
+ }
944
974
  const attestation = attestations.get(rule.id);
945
975
  if (attestation) {
946
976
  if (attestation.status === "deterministic-pass") {
@@ -1149,15 +1179,22 @@ export async function checkCompliance(repoPath, options = {}) {
1149
1179
  const hasCompletenessGap = gatedMissing.length > 0;
1150
1180
  const hasSelectedOptionalFailures = gatedSelectedFailed.length > 0 || gatedSelectedUnknown.length > 0;
1151
1181
  const { status, exitCode } = deriveGate({
1182
+ hasNoPlatform: detectedPlatforms.length === 0,
1152
1183
  hasBlocked: gatedResults.some((result) => result.status === "blocked"),
1153
1184
  hasDeterministicFailure: gatedResults.some((result) => result.status === "deterministic-fail"),
1154
1185
  needsAttestation: gatedResults.some((result) => result.status === "attestation-needed" || result.status === "stale"),
1155
1186
  hasCompletenessGap,
1156
1187
  hasSelectedOptionalFailures,
1188
+ hasProofWaived: gatedResults.some((result) => result.status === "proof-waived"),
1189
+ allowProofWaiver: options.allowProofWaiver === true,
1157
1190
  });
1158
1191
  const uxHarness = await readUxHarness(repoRoot);
1159
1192
  const uxAssessment = await assessUxHarness(inspection.effectiveRoot, uxHarness, compliance.outcome);
1160
- const openReviewItems = await experienceOpenReviewItems(repoRoot, findings);
1193
+ const openReviewItems = await experienceOpenReviewItems(repoRoot, findings, {
1194
+ results,
1195
+ completeness,
1196
+ outcome: compliance.outcome,
1197
+ });
1161
1198
  const experienceReport = {
1162
1199
  ...buildExperienceReport({
1163
1200
  checkStatus: status,
@@ -1251,20 +1288,56 @@ function sensorReviewGaps(raw) {
1251
1288
  }
1252
1289
  return gaps.slice(0, 50);
1253
1290
  }
1254
- async function experienceOpenReviewItems(repoRoot, findings) {
1291
+ async function experienceOpenReviewItems(repoRoot, findings, context) {
1255
1292
  const intake = await readIntakeReviewItems(repoRoot);
1256
1293
  const validateFindings = validateFindingReviewItems(findings);
1294
+ const advisoryFindings = context.results
1295
+ .filter((result) => result.status === "advisory")
1296
+ .map((result) => ({ ruleId: result.ruleId, title: result.title, severity: result.severity }));
1297
+ const scopeReductions = (context.completeness?.optedOut ?? []).map((item) => ({
1298
+ id: item.id,
1299
+ kind: "scope-omit",
1300
+ reason: item.reason,
1301
+ }));
1302
+ const runtimeProof = await runtimeProofReviewItem(repoRoot, context.outcome);
1257
1303
  const needsReview = intake.unresolvedNonblockingIntake.length > 0 ||
1258
1304
  intake.acknowledgedBlockingIntake.length > 0 ||
1259
- validateFindings.length > 0;
1305
+ validateFindings.length > 0 ||
1306
+ scopeReductions.length > 0 ||
1307
+ runtimeProof.status === "not-proven";
1308
+ const nextStepParts = [];
1309
+ if (needsReview)
1310
+ nextStepParts.push("Surface these items in the final handoff. Answer or explicitly scope unresolved intake, and treat validate findings as remaining review work even when vise check is green.");
1311
+ if (scopeReductions.length > 0)
1312
+ nextStepParts.push(`Confirm the scope-omit decision(s) were intended: ${scopeReductions.map((item) => item.id).join(", ")}.`);
1313
+ if (runtimeProof.status === "not-proven")
1314
+ nextStepParts.push(runtimeProof.note);
1315
+ if (advisoryFindings.length > 0)
1316
+ nextStepParts.push(`Advisory findings do not gate but are still review debt: ${advisoryFindings.map((item) => item.ruleId).join(", ")}.`);
1260
1317
  return {
1261
1318
  status: needsReview ? "needs-review" : "clear",
1262
1319
  unresolvedNonblockingIntake: intake.unresolvedNonblockingIntake,
1263
1320
  acknowledgedBlockingIntake: intake.acknowledgedBlockingIntake,
1264
1321
  validateFindings,
1265
- nextStep: needsReview
1266
- ? "Surface these items in the final handoff. Answer or explicitly scope unresolved intake, and treat validate findings as remaining review work even when vise check is green."
1267
- : "No unresolved intake or validate findings were observed in the current report inputs.",
1322
+ ...(advisoryFindings.length > 0 ? { advisoryFindings } : {}),
1323
+ ...(scopeReductions.length > 0 ? { scopeReductions } : {}),
1324
+ runtimeProof,
1325
+ nextStep: nextStepParts.length > 0
1326
+ ? nextStepParts.join(" ")
1327
+ : "No unresolved intake, validate findings, scope-omits, or runtime-proof gaps were observed in the current report inputs.",
1328
+ };
1329
+ }
1330
+ async function runtimeProofReviewItem(repoRoot, outcome) {
1331
+ if (runtimeSmokeSurfaceForOutcome(outcome) === null) {
1332
+ return { status: "not-applicable", note: "This outcome does not back a live-data collection surface." };
1333
+ }
1334
+ const evidence = await readJsonIfExists(path.join(sidecarDir(repoRoot), "evidence", "runtime-smoke.json"));
1335
+ if (evidence?.passed === true) {
1336
+ return { status: "proven", note: "Runtime-smoke evidence recorded a passing mounted-app capture for the declared surface(s)." };
1337
+ }
1338
+ return {
1339
+ status: "not-proven",
1340
+ note: "This surface renders live SDK collections but has no passing runtime-smoke evidence — a green `vise check` does not prove the screen actually populated at runtime. Capture runtime proof (`vise smoke`) or state in the handoff that only static checks are claimed.",
1268
1341
  };
1269
1342
  }
1270
1343
  async function readIntakeReviewItems(repoRoot) {
@@ -1771,8 +1844,18 @@ async function assessDeterministicChecks(rule, findingsById, platforms, canonica
1771
1844
  continue;
1772
1845
  }
1773
1846
  if (check.check === "runtime-smoke-evidence-passed") {
1774
- const smoke = await assessRuntimeSmokeEvidence(repoRoot, check);
1847
+ const smoke = await assessRuntimeSmokeEvidence(repoRoot, check, rule);
1775
1848
  if (!smoke.passed) {
1849
+ const waiver = await readRuntimeProofWaiver(repoRoot);
1850
+ if (waiver) {
1851
+ return {
1852
+ passed: false,
1853
+ waived: true,
1854
+ evidence: { source: "runtime_proof_waiver", mode: waiver.mode, reason: waiver.reason, recorded_at: waiver.recorded_at },
1855
+ reason: `Runtime proof waived (${waiver.mode}): ${waiver.reason}. This surface is NOT runtime-proven — only static checks are claimed.`,
1856
+ recommendation: smoke.recommendation,
1857
+ };
1858
+ }
1776
1859
  return smoke;
1777
1860
  }
1778
1861
  if (smoke.evidence) {
@@ -1817,7 +1900,7 @@ async function assessDeterministicChecks(rule, findingsById, platforms, canonica
1817
1900
  evidence,
1818
1901
  };
1819
1902
  }
1820
- async function assessRuntimeSmokeEvidence(repoRoot, check) {
1903
+ async function assessRuntimeSmokeEvidence(repoRoot, check, rule) {
1821
1904
  const relativePath = check.path ?? "sp-vise/evidence/runtime-smoke.json";
1822
1905
  const evidencePath = path.resolve(repoRoot, relativePath);
1823
1906
  const displayPath = path.relative(repoRoot, evidencePath).split(path.sep).join(path.posix.sep);
@@ -1843,11 +1926,21 @@ async function assessRuntimeSmokeEvidence(repoRoot, check) {
1843
1926
  const results = Array.isArray(parsed.results) ? parsed.results : [];
1844
1927
  const surfaceResults = results.filter(isRuntimeSmokeSurfaceResult);
1845
1928
  const failing = surfaceResults.filter((result) => result.verdict !== "pass");
1929
+ const generatedAt = typeof parsed.generated_at === "string" ? parsed.generated_at : undefined;
1930
+ const logBinding = runtimeSmokeBinding(parsed.log);
1931
+ const smokeConfigBinding = runtimeSmokeBinding(parsed.smoke_config);
1932
+ const sourceFingerprints = runtimeSmokeSourceFingerprints(parsed.source_fingerprints);
1846
1933
  const evidence = {
1847
1934
  source: "runtime_smoke",
1848
1935
  file: displayPath,
1849
1936
  passed: parsed.passed === true,
1850
1937
  ...(typeof parsed.platform === "string" ? { platform: parsed.platform } : {}),
1938
+ ...(generatedAt ? { generated_at: generatedAt } : {}),
1939
+ ...(logBinding ? { log: logBinding } : {}),
1940
+ ...(smokeConfigBinding ? { smoke_config: smokeConfigBinding } : {}),
1941
+ ...(sourceFingerprints.length > 0
1942
+ ? { source_fingerprints: sourceFingerprints.map((item) => ({ path: item.path, sha256: item.sha256, surfaces: item.surfaces })) }
1943
+ : {}),
1851
1944
  results: surfaceResults,
1852
1945
  };
1853
1946
  if (parsed.passed !== true) {
@@ -1877,7 +1970,121 @@ async function assessRuntimeSmokeEvidence(repoRoot, check) {
1877
1970
  recommendation,
1878
1971
  };
1879
1972
  }
1880
- const smokeConfig = await readJsonIfExists(path.join(sidecarDir(repoRoot), "smoke.json"));
1973
+ if (!generatedAt || Number.isNaN(Date.parse(generatedAt))) {
1974
+ return {
1975
+ passed: false,
1976
+ evidence,
1977
+ reason: `Runtime smoke evidence at ${displayPath} is missing a valid generated_at capture timestamp; rerun \`vise smoke\` from a captured mounted-app log.`,
1978
+ recommendation,
1979
+ };
1980
+ }
1981
+ if (!logBinding) {
1982
+ return {
1983
+ passed: false,
1984
+ evidence,
1985
+ reason: `Runtime smoke evidence at ${displayPath} is missing the captured log digest; rerun \`vise smoke\` so the proof binds to a log artifact.`,
1986
+ recommendation,
1987
+ };
1988
+ }
1989
+ const logPath = path.resolve(repoRoot, logBinding.path);
1990
+ if (!pathInside(repoRoot, logPath)) {
1991
+ return {
1992
+ passed: false,
1993
+ evidence,
1994
+ reason: `Runtime smoke log path must stay inside the repository: ${logBinding.path}`,
1995
+ recommendation,
1996
+ };
1997
+ }
1998
+ let currentLogDigest;
1999
+ try {
2000
+ currentLogDigest = await digestFile(logPath);
2001
+ }
2002
+ catch {
2003
+ return {
2004
+ passed: false,
2005
+ evidence,
2006
+ reason: `Runtime smoke evidence at ${displayPath} references a missing captured log: ${logBinding.path}.`,
2007
+ recommendation,
2008
+ };
2009
+ }
2010
+ if (currentLogDigest !== logBinding.sha256) {
2011
+ return {
2012
+ passed: false,
2013
+ evidence: { ...evidence, log_status: { path: logBinding.path, expected_sha256: logBinding.sha256, actual_sha256: currentLogDigest } },
2014
+ reason: `Runtime smoke captured log changed since evidence was recorded: ${logBinding.path}. Rerun the mounted-app capture and \`vise smoke\`.`,
2015
+ recommendation,
2016
+ };
2017
+ }
2018
+ const smokeConfigPath = path.join(sidecarDir(repoRoot), "smoke.json");
2019
+ let smokeConfigText;
2020
+ let smokeConfig;
2021
+ try {
2022
+ smokeConfigText = await readFile(smokeConfigPath, "utf8");
2023
+ smokeConfig = JSON.parse(smokeConfigText);
2024
+ }
2025
+ catch {
2026
+ return {
2027
+ passed: false,
2028
+ evidence,
2029
+ reason: `Runtime smoke evidence at ${displayPath} cannot be checked because sp-vise/smoke.json is missing or unreadable.`,
2030
+ recommendation,
2031
+ };
2032
+ }
2033
+ if (!smokeConfigBinding) {
2034
+ return {
2035
+ passed: false,
2036
+ evidence,
2037
+ reason: `Runtime smoke evidence at ${displayPath} is missing the smoke.json digest; rerun \`vise smoke\` so the proof binds to the declared surfaces.`,
2038
+ recommendation,
2039
+ };
2040
+ }
2041
+ const smokeConfigBindingPath = path.resolve(repoRoot, smokeConfigBinding.path);
2042
+ if (!pathInside(repoRoot, smokeConfigBindingPath) || smokeConfigBindingPath !== smokeConfigPath) {
2043
+ return {
2044
+ passed: false,
2045
+ evidence,
2046
+ reason: `Runtime smoke evidence must bind to sp-vise/smoke.json, not ${smokeConfigBinding.path}.`,
2047
+ recommendation,
2048
+ };
2049
+ }
2050
+ const currentSmokeConfigDigest = `sha256:${createHash("sha256").update(smokeConfigText).digest("hex")}`;
2051
+ if (currentSmokeConfigDigest !== smokeConfigBinding.sha256) {
2052
+ return {
2053
+ passed: false,
2054
+ evidence: {
2055
+ ...evidence,
2056
+ smoke_config_status: { path: smokeConfigBinding.path, expected_sha256: smokeConfigBinding.sha256, actual_sha256: currentSmokeConfigDigest },
2057
+ },
2058
+ reason: `sp-vise/smoke.json changed since runtime smoke evidence was recorded. Rerun \`vise smoke\` for the current declared surfaces.`,
2059
+ recommendation,
2060
+ };
2061
+ }
2062
+ const expectedRulePlatform = platformFromRuleId(rule.id);
2063
+ const configPlatform = typeof smokeConfig.platform === "string" ? smokeConfig.platform : undefined;
2064
+ if (expectedRulePlatform && parsed.platform !== expectedRulePlatform) {
2065
+ return {
2066
+ passed: false,
2067
+ evidence,
2068
+ reason: `Runtime smoke evidence platform "${String(parsed.platform ?? "(missing)")}" does not match rule platform "${expectedRulePlatform}". Capture runtime proof on the platform being checked.`,
2069
+ recommendation,
2070
+ };
2071
+ }
2072
+ if (expectedRulePlatform && configPlatform !== expectedRulePlatform) {
2073
+ return {
2074
+ passed: false,
2075
+ evidence,
2076
+ reason: `sp-vise/smoke.json platform "${String(configPlatform ?? "(missing)")}" does not match rule platform "${expectedRulePlatform}". Declare and capture smoke evidence for the platform being checked.`,
2077
+ recommendation,
2078
+ };
2079
+ }
2080
+ if (!expectedRulePlatform && configPlatform && parsed.platform !== configPlatform) {
2081
+ return {
2082
+ passed: false,
2083
+ evidence,
2084
+ reason: `Runtime smoke evidence platform "${String(parsed.platform ?? "(missing)")}" does not match sp-vise/smoke.json platform "${configPlatform}".`,
2085
+ recommendation,
2086
+ };
2087
+ }
1881
2088
  const expectedSurfaceIds = Array.isArray(smokeConfig?.surfaces)
1882
2089
  ? smokeConfig.surfaces.map((surface) => surface.id).filter((id) => typeof id === "string" && id.length > 0)
1883
2090
  : [];
@@ -1892,6 +2099,15 @@ async function assessRuntimeSmokeEvidence(repoRoot, check) {
1892
2099
  recommendation,
1893
2100
  };
1894
2101
  }
2102
+ const provenance = await verifyRuntimeSmokeSourceProvenance(repoRoot, sourceFingerprints, expectedSurfaceIds);
2103
+ if (!provenance.passed) {
2104
+ return {
2105
+ passed: false,
2106
+ evidence: { ...evidence, source_fingerprint_status: provenance.status },
2107
+ reason: provenance.reason,
2108
+ recommendation,
2109
+ };
2110
+ }
1895
2111
  const expectedSurfacesById = new Map(smokeConfig?.surfaces
1896
2112
  ?.filter((surface) => typeof surface.id === "string" && surface.id.length > 0)
1897
2113
  .map((surface) => [surface.id, surface]) ?? []);
@@ -1914,6 +2130,92 @@ async function assessRuntimeSmokeEvidence(repoRoot, check) {
1914
2130
  evidence,
1915
2131
  };
1916
2132
  }
2133
+ function runtimeSmokeBinding(value) {
2134
+ if (!value || typeof value !== "object")
2135
+ return null;
2136
+ const item = value;
2137
+ if (typeof item.path !== "string" || typeof item.sha256 !== "string")
2138
+ return null;
2139
+ if (!item.sha256.startsWith("sha256:"))
2140
+ return null;
2141
+ return { path: item.path, sha256: item.sha256 };
2142
+ }
2143
+ function runtimeSmokeSourceFingerprints(value) {
2144
+ if (!Array.isArray(value))
2145
+ return [];
2146
+ return value.flatMap((entry) => {
2147
+ const binding = runtimeSmokeBinding(entry);
2148
+ if (!binding || !entry || typeof entry !== "object")
2149
+ return [];
2150
+ const surfaces = entry.surfaces;
2151
+ if (!Array.isArray(surfaces))
2152
+ return [];
2153
+ const normalizedSurfaces = surfaces.filter((surface) => typeof surface === "string" && surface.length > 0).sort();
2154
+ return normalizedSurfaces.length > 0 ? [{ ...binding, surfaces: normalizedSurfaces }] : [];
2155
+ });
2156
+ }
2157
+ async function verifyRuntimeSmokeSourceProvenance(repoRoot, fingerprints, expectedSurfaceIds) {
2158
+ if (fingerprints.length === 0) {
2159
+ return {
2160
+ passed: false,
2161
+ reason: "Runtime smoke evidence is missing source fingerprints for VISE_SMOKE emitters; rerun `vise smoke` after wiring markers into product source.",
2162
+ status: [],
2163
+ };
2164
+ }
2165
+ const surfacesWithEmitter = new Set();
2166
+ const status = [];
2167
+ for (const fingerprint of fingerprints) {
2168
+ const absolutePath = path.resolve(repoRoot, fingerprint.path);
2169
+ if (!pathInside(repoRoot, absolutePath)) {
2170
+ status.push({ path: fingerprint.path, expected_sha256: fingerprint.sha256, status: "outside-repo" });
2171
+ continue;
2172
+ }
2173
+ let content;
2174
+ let actualSha256;
2175
+ try {
2176
+ content = await readFile(absolutePath, "utf8");
2177
+ actualSha256 = `sha256:${createHash("sha256").update(content).digest("hex")}`;
2178
+ }
2179
+ catch {
2180
+ status.push({ path: fingerprint.path, expected_sha256: fingerprint.sha256, status: "missing" });
2181
+ continue;
2182
+ }
2183
+ const hasMarker = hasExecutableSmokeMarker(content);
2184
+ const declaredSurfaces = fingerprint.surfaces.filter((surface) => content.includes(surface));
2185
+ for (const surface of declaredSurfaces) {
2186
+ surfacesWithEmitter.add(surface);
2187
+ }
2188
+ status.push({
2189
+ path: fingerprint.path,
2190
+ expected_sha256: fingerprint.sha256,
2191
+ actual_sha256: actualSha256,
2192
+ status: actualSha256 === fingerprint.sha256 && hasMarker && declaredSurfaces.length === fingerprint.surfaces.length ? "match" : "changed",
2193
+ has_marker: hasMarker,
2194
+ surfaces: declaredSurfaces,
2195
+ });
2196
+ }
2197
+ const stale = status.filter((item) => item.status !== "match");
2198
+ if (stale.length > 0) {
2199
+ return {
2200
+ passed: false,
2201
+ reason: "Runtime smoke source emitters changed or disappeared since evidence was recorded. Rerun the mounted-app capture and `vise smoke`.",
2202
+ status,
2203
+ };
2204
+ }
2205
+ const missingSurfaces = expectedSurfaceIds.filter((surface) => !surfacesWithEmitter.has(surface));
2206
+ if (missingSurfaces.length > 0) {
2207
+ return {
2208
+ passed: false,
2209
+ reason: `Runtime smoke evidence is missing VISE_SMOKE source emitter fingerprints for declared surface(s): ${missingSurfaces.join(", ")}.`,
2210
+ status,
2211
+ };
2212
+ }
2213
+ return { passed: true, status };
2214
+ }
2215
+ function platformFromRuleId(ruleId) {
2216
+ const platform = ruleId.split(".")[0];
2217
+ return ["typescript", "react-native", "android", "ios", "flutter"].includes(platform) ? platform : null;
2218
+ }
1917
2219
  function runtimeSmokeCaptureRecommendationText() {
1918
2220
  return [
1919
2221
  "Declare collection surfaces in `sp-vise/smoke.json` and emit `VISE_SMOKE surface=<id> state=loading|empty|loaded count=N|error:<message>` from the mounted app's real SDK query lifecycle; `expect: \"populated\"` requires loaded count=N with N > 0.",
@@ -1998,6 +2300,9 @@ export const FILE_SCOPABLE_SENSOR_SUFFIXES = new Set([
1998
2300
  "live-collection.api-mismatch",
1999
2301
  "story.live-collection",
2000
2302
  "search.live-collection",
2303
+ "message-search.live-collection",
2304
+ "channel-archive.live-collection",
2305
+ "for-you-feed.live-collection",
2001
2306
  "notification-tray.live-collection",
2002
2307
  "event.live-collection",
2003
2308
  "blocked-users.live-collection",
@@ -2506,6 +2811,8 @@ async function readBaseline(repoRoot) {
2506
2811
  return readJsonIfExists(baselinePath(repoRoot));
2507
2812
  }
2508
2813
  function deriveGate(flags) {
2814
+ if (flags.hasNoPlatform)
2815
+ return { status: "no-platform", exitCode: 8 };
2509
2816
  if (flags.hasBlocked)
2510
2817
  return { status: "blocked", exitCode: 3 };
2511
2818
  if (flags.hasDeterministicFailure)
@@ -2516,6 +2823,8 @@ function deriveGate(flags) {
2516
2823
  return { status: "completeness-gap", exitCode: 5 };
2517
2824
  if (flags.hasSelectedOptionalFailures)
2518
2825
  return { status: "selected-capability-failures", exitCode: 6 };
2826
+ if (flags.hasProofWaived)
2827
+ return { status: "runtime-proof-waived", exitCode: flags.allowProofWaiver ? 0 : 9 };
2519
2828
  return { status: "green", exitCode: 0 };
2520
2829
  }
2521
2830
  export async function recordBaseline(repoPath) {
@@ -32,6 +32,7 @@ export async function debugIssue(repoPath, errorMessage, options = {}) {
32
32
  : undefined;
33
33
  const correlatedRules = [];
34
34
  let checkResult = null;
35
+ let correlationNote;
35
36
  try {
36
37
  checkResult = await checkCompliance(repoPath);
37
38
  const rulesMap = await rulesById();
@@ -39,7 +40,15 @@ export async function debugIssue(repoPath, errorMessage, options = {}) {
39
40
  const contractRuleId = rule.contractRuleId ?? rule.ruleId;
40
41
  const ruleDef = rulesMap.get(contractRuleId);
41
42
  const symptoms = ruleDef?.symptoms || [];
42
- if (symptoms.length === 0 || !symptoms.some((s) => errorMessage.includes(s))) {
43
+ const symptomMatches = symptoms.some((s) => {
44
+ if (!errorMessage.includes(s))
45
+ return false;
46
+ if (/^\d{3}$/.test(s.trim())) {
47
+ return /amity|social[.\s]?plus|@amityco|live[-\s]?collection|session|AmityClient|AccessToken/i.test(errorMessage);
48
+ }
49
+ return true;
50
+ });
51
+ if (symptoms.length === 0 || !symptomMatches) {
43
52
  continue;
44
53
  }
45
54
  if (rule.status === "deterministic-fail" || rule.status === "attestation-needed") {
@@ -67,6 +76,10 @@ export async function debugIssue(repoPath, errorMessage, options = {}) {
67
76
  }
68
77
  }
69
78
  catch (err) {
79
+ const reason = err instanceof Error ? err.message : String(err);
80
+ correlationNote = /compliance sidecar/i.test(reason)
81
+ ? "Compliance correlation was skipped: no sp-vise sidecar found. Run `vise init .` so `vise debug` can cross-reference this crash against the SDK rule signatures — until then this diagnosis is based only on the error text, not the project's compliance state."
82
+ : `Compliance correlation could not run (${sanitizeError(reason)}); this diagnosis is based only on the error text, not the project's compliance state.`;
70
83
  }
71
84
  const failingCorrelated = correlatedRules.filter((r) => r.status === "failed" || r.status === "attested");
72
85
  const moduleDiagnosis = missingModulePkg
@@ -121,6 +134,7 @@ export async function debugIssue(repoPath, errorMessage, options = {}) {
121
134
  extractedException: exceptionClass,
122
135
  suggestedRemediation,
123
136
  repairBrief,
137
+ ...(correlationNote ? { correlationNote } : {}),
124
138
  ...(secondaryModuleNote ? { secondaryNotes: [secondaryModuleNote] } : {}),
125
139
  relevantDocs: [
126
140
  {
@@ -134,6 +148,7 @@ export async function debugIssue(repoPath, errorMessage, options = {}) {
134
148
  likelyCause: payload.likelyCause,
135
149
  correlatedRules: payload.correlatedRules,
136
150
  repairBrief: payload.repairBrief,
151
+ ...(correlationNote ? { correlationNote } : {}),
137
152
  ...(secondaryModuleNote ? { secondaryNotes: [secondaryModuleNote] } : {}),
138
153
  relevantDocs: payload.relevantDocs,
139
154
  };
@@ -231,34 +231,40 @@ export const BRAND_PROFILE_PROTOCOL = [
231
231
  "Advisory — it grounds generation; it never gates `vise check`.",
232
232
  ].join("\n");
233
233
  const REQUIRED_PROFILE_STRINGS = ["essence", "voice", "density", "elevation", "motion", "imagery"];
234
- export function validateDesignProfile(raw) {
234
+ export function diagnoseDesignProfile(raw) {
235
235
  if (!raw || typeof raw !== "object") {
236
- return null;
236
+ return "not a JSON object";
237
237
  }
238
238
  const p = raw;
239
239
  if (typeof p.schemaVersion !== "string") {
240
- return null;
240
+ return "`schemaVersion` is missing or not a string";
241
241
  }
242
242
  for (const f of REQUIRED_PROFILE_STRINGS) {
243
243
  if (typeof p[f] !== "string" || p[f].trim().length === 0) {
244
- return null;
244
+ return `\`${f}\` is missing or empty (all of ${REQUIRED_PROFILE_STRINGS.join(", ")} must be non-empty strings)`;
245
245
  }
246
246
  }
247
247
  const isStrArr = (v) => Array.isArray(v) && v.every((x) => typeof x === "string");
248
- if (!isStrArr(p.personality) || !isStrArr(p.do) || !isStrArr(p.avoid)) {
249
- return null;
250
- }
248
+ if (!isStrArr(p.personality))
249
+ return "`personality` must be an array of strings";
250
+ if (!isStrArr(p.do))
251
+ return "`do` must be an array of strings";
252
+ if (!isStrArr(p.avoid))
253
+ return "`avoid` must be an array of strings";
251
254
  if (!Array.isArray(p.palette) ||
252
255
  !p.palette.every((e) => e &&
253
256
  typeof e === "object" &&
254
257
  typeof e.role === "string" &&
255
258
  typeof e.hex === "string")) {
256
- return null;
259
+ return "`palette` must be an array of { role, hex } entries";
257
260
  }
258
261
  if (!p.typography || typeof p.typography !== "object") {
259
- return null;
262
+ return "`typography` must be an object";
260
263
  }
261
- return raw;
264
+ return null;
265
+ }
266
+ export function validateDesignProfile(raw) {
267
+ return diagnoseDesignProfile(raw) === null ? raw : null;
262
268
  }
263
269
  export async function readDesignProfile(repoPath) {
264
270
  const target = path.join(path.resolve(repoPath), "sp-vise", DESIGN_PROFILE_FILENAME);
@@ -269,6 +275,25 @@ export async function readDesignProfile(repoPath) {
269
275
  return null;
270
276
  }
271
277
  }
278
+ export async function readDesignProfileDiagnostic(repoPath) {
279
+ const target = path.join(path.resolve(repoPath), "sp-vise", DESIGN_PROFILE_FILENAME);
280
+ let text;
281
+ try {
282
+ text = await readFile(target, "utf8");
283
+ }
284
+ catch {
285
+ return { present: false };
286
+ }
287
+ let parsed;
288
+ try {
289
+ parsed = JSON.parse(text);
290
+ }
291
+ catch {
292
+ return { present: true, reason: "the file is not valid JSON" };
293
+ }
294
+ const reason = diagnoseDesignProfile(parsed);
295
+ return reason === null ? { present: true } : { present: true, reason };
296
+ }
272
297
  export async function writeDesignProfile(repoPath, profile) {
273
298
  const sidecarDir = path.join(path.resolve(repoPath), "sp-vise");
274
299
  await mkdir(sidecarDir, { recursive: true });
@@ -409,7 +434,7 @@ export const designPreviewTool = {
409
434
  }
410
435
  : "no UI code scanned",
411
436
  ...(contract.source.self_referential
412
- ? { self_referential_note: "Contract derived from this project's own files (--from-project): conformance is circular and is NOT a design-quality signal. For a real design target, extract from an external prototype/theme." }
437
+ ? { self_referential_note: "Contract derived from this project's own files (--from-project). Vise cannot verify whether that design system PRE-EXISTED your work, so its strength is capped below \"strong\" as a guardrail: if you just wrote the feature's styling and then graded it against itself, high token coverage is circular, not a design-quality signal. If the design system genuinely pre-existed (a real host theme), the contract IS a legitimate grounding source — the cap is only a can't-verify precaution, not a verdict that your system is bad. For an unambiguous external target, extract from a customer prototype/theme." }
413
438
  : {}),
414
439
  note: "Open the HTML to visually compare the contract (and embedded reference, if HTML) against your app. A human or VLM judges the visual match — this is not an automated pixel diff.",
415
440
  where: written ? `Open ${written} in a browser.` : "Not written (write=false).",
@@ -2589,7 +2614,7 @@ function bestCandidate(a, b) {
2589
2614
  return b;
2590
2615
  return a.token.length <= b.token.length ? a : b;
2591
2616
  }
2592
- export function buildDesignBrief(contract, profile) {
2617
+ export function buildDesignBrief(contract, profile, profileProblem) {
2593
2618
  const strength = contract.stats.strength;
2594
2619
  const colorTokens = contract.tokens.filter((t) => t.category === "color" && t.name !== null);
2595
2620
  const roleMap = new Map();
@@ -2783,10 +2808,16 @@ export function buildDesignBrief(contract, profile) {
2783
2808
  avoid: profile.avoid,
2784
2809
  source: profile.capturedFrom ?? "sp-vise/design-profile.json",
2785
2810
  }
2786
- : {
2787
- status: "absent",
2788
- howTo: "These are tokens only. For richer brand grounding (personality, type system, density, voice, do/avoid), author sp-vise/design-profile.json from the host's real screens, then honor it per surface. See the 'Brand profile' subsection of the social.plus design skill for the schema.",
2789
- };
2811
+ : profileProblem
2812
+ ? {
2813
+ status: "invalid",
2814
+ reason: profileProblem,
2815
+ howTo: `sp-vise/design-profile.json EXISTS but was rejected: ${profileProblem}. FIX that one field in the existing file and re-run — do not re-author it from scratch. (A malformed profile is ignored, so it silently read as absent before this.) See the 'Brand profile' subsection of the social.plus design skill for the schema.`,
2816
+ }
2817
+ : {
2818
+ status: "absent",
2819
+ howTo: "These are tokens only. For richer brand grounding (personality, type system, density, voice, do/avoid), author sp-vise/design-profile.json from the host's real screens, then honor it per surface. See the 'Brand profile' subsection of the social.plus design skill for the schema.",
2820
+ };
2790
2821
  return {
2791
2822
  summary,
2792
2823
  strength,