@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.
- package/CHANGELOG.md +35 -10
- package/README.md +35 -31
- package/dist/capabilities.js +1 -1
- package/dist/flow.js +2 -2
- package/dist/humanFormat.js +2 -0
- package/dist/outcomes.js +34 -10
- package/dist/server.js +421 -62
- package/dist/tools/compliance.js +321 -12
- package/dist/tools/debug.js +16 -1
- package/dist/tools/design.js +47 -16
- package/dist/tools/integration.js +40 -7
- package/dist/tools/project.js +257 -10
- package/dist/tools/smoke.js +169 -1
- package/docs/vise-how-it-helps.html +318 -105
- package/package.json +1 -1
- package/rules/channel-archive.yaml +136 -0
- package/rules/feed.yaml +37 -15
- package/rules/for-you-feed.yaml +110 -0
- package/rules/live-data.yaml +10 -5
- package/rules/message-search.yaml +136 -0
- package/rules/sdk-lifecycle.yaml +23 -0
- package/sdk-surface/android.json +1305 -23
- package/sdk-surface/flutter.json +2542 -2582
- package/sdk-surface/ios.json +1182 -166
- package/sdk-surface/manifest.json +32 -32
- package/sdk-surface/models.android.json +14 -4
- package/sdk-surface/models.flutter.json +2 -2
- package/sdk-surface/models.ios.json +2 -2
- package/sdk-surface/models.typescript.json +145 -145
- package/sdk-surface/typescript.json +8337 -2443
- package/skills/social-plus-vise/SKILL.md +33 -13
package/dist/tools/compliance.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
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
|
-
|
|
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) {
|
package/dist/tools/debug.js
CHANGED
|
@@ -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
|
-
|
|
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
|
};
|
package/dist/tools/design.js
CHANGED
|
@@ -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
|
|
234
|
+
export function diagnoseDesignProfile(raw) {
|
|
235
235
|
if (!raw || typeof raw !== "object") {
|
|
236
|
-
return
|
|
236
|
+
return "not a JSON object";
|
|
237
237
|
}
|
|
238
238
|
const p = raw;
|
|
239
239
|
if (typeof p.schemaVersion !== "string") {
|
|
240
|
-
return
|
|
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
|
|
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)
|
|
249
|
-
return
|
|
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
|
|
259
|
+
return "`palette` must be an array of { role, hex } entries";
|
|
257
260
|
}
|
|
258
261
|
if (!p.typography || typeof p.typography !== "object") {
|
|
259
|
-
return
|
|
262
|
+
return "`typography` must be an object";
|
|
260
263
|
}
|
|
261
|
-
return
|
|
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)
|
|
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
|
-
|
|
2788
|
-
|
|
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,
|