@amityco/social-plus-vise 1.2.0 → 1.3.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.
@@ -1,9 +1,10 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import { mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
3
+ import { existsSync } from "node:fs";
3
4
  import path from "node:path";
4
5
  import { fileURLToPath } from "node:url";
5
6
  import { applyBlockProvidedCompleteness, assessProjectCompleteness, assessProjectSelectedOptionalCapabilities, availableOptionalCapabilityIds, optionalCapabilityChecklist, platformCapabilityAvailability, selectedOptionalCapabilityIds, } from "../capabilities.js";
6
- import { getOutcomeDefinition, hasAnswer, planContextFor, resolveOutcome, } from "../outcomes.js";
7
+ import { CLASSIFY_ORDER, getOutcomeDefinition, hasAnswer, planContextFor, resolveOutcome, } from "../outcomes.js";
7
8
  import { contractRuleCandidatesForPublicId, hasMultipleContractRuleCandidates, productExpectationBindingForSensor, productExpectationTitle, publicProductRuleId, } from "../productExpectations.js";
8
9
  import { objectInput, optionalBooleanField, optionalStringField, stringField, textResult } from "../types.js";
9
10
  import { packageVersion } from "../version.js";
@@ -210,7 +211,7 @@ function stringArray(value) {
210
211
  return value.filter((item) => typeof item === "string" && item.trim() !== "");
211
212
  }
212
213
  const validTiers = new Set(["free", "pro", "partner"]);
213
- const validOutcomes = new Set(["setup-sdk", "setup-push", "setup-live-data", "add-feed", "add-comments", "add-moderation", "add-chat", "troubleshoot", "validate-setup", "unknown"]);
214
+ const validOutcomes = new Set([...CLASSIFY_ORDER, "unknown"]);
214
215
  export async function initEngagement(args) {
215
216
  const repoRoot = path.resolve(args.repoPath);
216
217
  const engagementFile = engagementPath(repoRoot);
@@ -289,6 +290,13 @@ function designReviewFor(repoRoot, contract, answers) {
289
290
  requiredForDesignConformance: true,
290
291
  };
291
292
  if (confirmation === "accepted") {
293
+ if (base.previewPath && !existsSync(base.previewPath)) {
294
+ return {
295
+ ...base,
296
+ status: "needs-confirmation",
297
+ nextStep: `Cannot accept the design contract: its preview (${base.previewPath}) does not exist, so it could not have been reviewed. Run \`vise design preview\` to regenerate it, review it, then re-confirm with --answer ${DESIGN_CONTRACT_CONFIRMATION_ANSWER_ID}=yes.`,
298
+ };
299
+ }
292
300
  return {
293
301
  ...base,
294
302
  status: "accepted",
@@ -341,6 +349,12 @@ export async function initCompliance(repoPath, request, surfacePath, answers = {
341
349
  capabilityAvailability,
342
350
  allowUnresolvedIntake: options.allowUnresolvedIntake === true,
343
351
  });
352
+ const intakeWarnings = validateIntakeAnswers(answers, {
353
+ request,
354
+ outcome,
355
+ platforms: inspection.platforms,
356
+ designSignals: inspection.designSignals,
357
+ });
344
358
  if (intake.remainingBlocking > 0 && !intake.acknowledged_unresolved_blocking) {
345
359
  const blockingIds = intake.questions
346
360
  .filter((question) => question.blocksImplementationWhenMissing)
@@ -351,6 +365,7 @@ export async function initCompliance(repoPath, request, surfacePath, answers = {
351
365
  exitCode: 7,
352
366
  outcome,
353
367
  intake,
368
+ ...(intakeWarnings.length > 0 && { warnings: intakeWarnings }),
354
369
  nextStep: "Run `vise plan` and surface the blocking intake questions to the customer. " +
355
370
  `Then re-run with their answers (one --answer per id): vise init --request <request> --answer ${answerExample}. ` +
356
371
  "Each --answer takes a single id=value; repeat the flag per id (do not comma-join). " +
@@ -401,8 +416,10 @@ export async function initCompliance(repoPath, request, surfacePath, answers = {
401
416
  status: checkSnapshot.status,
402
417
  summary: checkSnapshot.summary,
403
418
  rules: checkSnapshot.rules,
419
+ ...(checkSnapshot.completeness ? { completeness: checkSnapshot.completeness } : {}),
420
+ ...(checkSnapshot.selectedOptionalCapabilities ? { selectedOptionalCapabilities: checkSnapshot.selectedOptionalCapabilities } : {}),
404
421
  });
405
- const warnings = [];
422
+ const warnings = [...intakeWarnings];
406
423
  if (engagement && engagement.scope.outcomes.length > 0 && !engagement.scope.outcomes.includes(outcome)) {
407
424
  warnings.push(`Outcome "${outcome}" is not in the engagement scope (${engagement.scope.outcomes.join(", ")}). Compliance was still initialized; extend the scope in engagement.json or re-run vise engagement init.`);
408
425
  }
@@ -511,6 +528,44 @@ function intakeAuditFor(args) {
511
528
  acknowledged_unresolved_blocking: args.allowUnresolvedIntake && remainingBlocking > 0,
512
529
  };
513
530
  }
531
+ const APPENDED_INTAKE_QUESTION_IDS = new Set([
532
+ DESIGN_CONTRACT_CONFIRMATION_ANSWER_ID,
533
+ "design_source",
534
+ "confirm_design_source",
535
+ "primary_action_token",
536
+ "feed_optional_capabilities",
537
+ ]);
538
+ function validateIntakeAnswers(answers, ctx) {
539
+ const warnings = [];
540
+ const catalogCtx = planContextFor({
541
+ request: ctx.request,
542
+ outcome: ctx.outcome,
543
+ platform: preferredPlatform(ctx.platforms),
544
+ platforms: ctx.platforms,
545
+ designSignals: ctx.designSignals,
546
+ answers: {},
547
+ });
548
+ const catalog = getOutcomeDefinition(ctx.outcome).intakeQuestions(catalogCtx);
549
+ const byId = new Map(catalog.map((question) => [question.id, question]));
550
+ for (const [key, value] of Object.entries(answers)) {
551
+ const question = byId.get(key);
552
+ if (!question && !APPENDED_INTAKE_QUESTION_IDS.has(key)) {
553
+ warnings.push(`Intake answer "${key}" was ignored — it does not match any intake question for outcome "${ctx.outcome}". Run \`vise plan\` to see the valid question ids.`);
554
+ continue;
555
+ }
556
+ const optionsAreTokens = Array.isArray(question?.options) &&
557
+ question.options.length > 0 &&
558
+ question.options.every((option) => /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(option));
559
+ if (question && optionsAreTokens) {
560
+ const tokens = String(value).split(",").map((token) => token.trim()).filter(Boolean);
561
+ const invalid = tokens.filter((token) => !question.options.includes(token));
562
+ if (invalid.length > 0) {
563
+ warnings.push(`Intake answer "${key}=${value}" includes value(s) outside the allowed options for "${key}" (${question.options.join(", ")}). The unexpected value(s) [${invalid.join(", ")}] may be ignored; re-run with a valid option.`);
564
+ }
565
+ }
566
+ }
567
+ return warnings;
568
+ }
514
569
  function creativeSelectionSummary(selection) {
515
570
  return {
516
571
  status: selection.status,
@@ -680,7 +735,7 @@ async function ruleFreshness(rule) {
680
735
  ...(verifiedAgainst && { verified_against: verifiedAgainst }),
681
736
  };
682
737
  }
683
- export async function checkCompliance(repoPath) {
738
+ export async function checkCompliance(repoPath, options = {}) {
684
739
  const repoRoot = path.resolve(repoPath);
685
740
  const compliance = await readCompliance(repoRoot);
686
741
  const rules = await rulesById();
@@ -700,6 +755,7 @@ export async function checkCompliance(repoPath) {
700
755
  const recordedPlatforms = compliance.surface?.platforms || [];
701
756
  const platformsToValidate = Array.from(new Set([...detectedPlatforms, ...recordedPlatforms]));
702
757
  const platforms = platformsToValidate.length > 0 ? platformsToValidate : ["unknown"];
758
+ const { canonicalPlatform } = await import("./sdkFacts.js");
703
759
  const allFindings = await Promise.all(platforms.map((p) => validateSetup(inspection.effectiveRoot, p)));
704
760
  const findings = allFindings.flat();
705
761
  const findingsById = new Map();
@@ -740,11 +796,17 @@ export async function checkCompliance(repoPath) {
740
796
  const isInferential = !hasDeterministicChecks && !!rule.enforcement.inferential;
741
797
  const finding = hasDeterministicChecks ? deterministicFinding(rule, findingsById) : undefined;
742
798
  if (hasDeterministicChecks && !finding) {
799
+ const identity = checkRuleIdentity(rule.id);
800
+ const rulePlatform = identity.validator?.platform;
801
+ const passBasis = rulePlatform && !platforms.some((p) => canonicalPlatform(p) === canonicalPlatform(rulePlatform))
802
+ ? "undetected-platform"
803
+ : "absent";
743
804
  results.push({
744
- ...checkRuleIdentity(rule.id),
805
+ ...identity,
745
806
  title: rule.title,
746
807
  severity: rule.severity,
747
808
  status: "deterministic-pass",
809
+ pass_basis: passBasis,
748
810
  evidence: { source: "validate_setup", finding_absent: deterministicFindingIds(rule) },
749
811
  });
750
812
  continue;
@@ -858,39 +920,81 @@ export async function checkCompliance(repoPath) {
858
920
  }
859
921
  }
860
922
  const summary = summarize(results);
861
- const hasBlocked = results.some((result) => result.status === "blocked");
862
- const hasDeterministicFailure = results.some((result) => result.status === "deterministic-fail");
863
- const needsAttestation = results.some((result) => result.status === "attestation-needed" || result.status === "stale");
923
+ const deterministicPasses = results.filter((result) => result.status === "deterministic-pass");
924
+ const evidenceBasis = deterministicPasses.length > 0
925
+ ? {
926
+ absent: deterministicPasses.filter((result) => result.pass_basis !== "undetected-platform").length,
927
+ undetected_platform: deterministicPasses.filter((result) => result.pass_basis === "undetected-platform").length,
928
+ }
929
+ : undefined;
930
+ const evidenceBasisNote = evidenceBasis
931
+ ? `${deterministicPasses.length} of the passing rules passed by ABSENCE (the sensor found no violation, which is not positive proof the feature was built).${evidenceBasis.undetected_platform > 0
932
+ ? ` ${evidenceBasis.undetected_platform} of those are vacuous — the rule's platform was not detected in this project, so no sensor ran.`
933
+ : ""} Treat these as "no problem found", not "built and validated".`
934
+ : undefined;
864
935
  const sourceCompleteness = (await assessProjectCompleteness(inspection.effectiveRoot, compliance.outcome).catch(() => null)) ?? undefined;
865
936
  const blockProvided = sourceCompleteness && sourceCompleteness.missing.length > 0
866
937
  ? await installedBlockProvidedCapabilities(repoRoot).catch(() => [])
867
938
  : [];
868
939
  const completeness = sourceCompleteness ? applyBlockProvidedCompleteness(sourceCompleteness, blockProvided) : undefined;
869
- const hasCompletenessGap = (completeness?.missing.length ?? 0) > 0;
870
940
  const selectedOptionalCapabilities = (await assessProjectSelectedOptionalCapabilities(inspection.effectiveRoot, compliance.outcome, compliance.selected_optional_capabilities ?? []).catch(() => null)) ?? undefined;
871
- const hasSelectedOptionalFailures = ((selectedOptionalCapabilities?.failed.length ?? 0) > 0) || ((selectedOptionalCapabilities?.unknown.length ?? 0) > 0);
872
- const status = hasBlocked
873
- ? "blocked"
874
- : hasDeterministicFailure
875
- ? "deterministic-failures"
876
- : needsAttestation
877
- ? "needs-attestation"
878
- : hasCompletenessGap
879
- ? "completeness-gap"
880
- : hasSelectedOptionalFailures
881
- ? "selected-capability-failures"
882
- : "green";
883
- const exitCode = hasBlocked
884
- ? 3
885
- : hasDeterministicFailure
886
- ? 2
887
- : needsAttestation
888
- ? 1
889
- : hasCompletenessGap
890
- ? 5
891
- : hasSelectedOptionalFailures
892
- ? 6
893
- : 0;
941
+ const baselineFile = await readBaseline(repoRoot);
942
+ let baselineReport;
943
+ let gatedResults = results;
944
+ let gatedMissing = completeness?.missing ?? [];
945
+ let gatedSelectedFailed = selectedOptionalCapabilities?.failed ?? [];
946
+ let gatedSelectedUnknown = selectedOptionalCapabilities?.unknown ?? [];
947
+ if (options.newOnly) {
948
+ if (!baselineFile) {
949
+ baselineReport = { present: false, applied: false, reason: "No baseline recorded — `--new-only` gated on everything. Run `vise init --baseline` (or `vise baseline`) on the pre-existing codebase first." };
950
+ }
951
+ else if (baselineFile.ruleset_digest !== compliance.ruleset_digest) {
952
+ baselineReport = { present: true, applied: false, stale: true, baselined_at: baselineFile.baselined_at, reason: "Baseline is stale: the ruleset changed since it was recorded, so it was NOT applied (gating on everything). Re-run `vise baseline` to refresh it against the current contract." };
953
+ }
954
+ else {
955
+ const budget = new Map(Object.entries(baselineFile.findings));
956
+ for (const result of results) {
957
+ if (!BASELINE_NON_GREEN.has(result.status))
958
+ continue;
959
+ const key = baselineKeyFor(result);
960
+ const remaining = budget.get(key) ?? 0;
961
+ if (remaining > 0) {
962
+ budget.set(key, remaining - 1);
963
+ result.baselined = true;
964
+ }
965
+ }
966
+ const baselinedMissing = new Set(baselineFile.completeness_missing);
967
+ const baselinedSelected = new Set(baselineFile.selected_optional);
968
+ gatedResults = results.filter((result) => !result.baselined);
969
+ gatedMissing = (completeness?.missing ?? []).filter((item) => !baselinedMissing.has(item.id));
970
+ gatedSelectedFailed = (selectedOptionalCapabilities?.failed ?? []).filter((item) => !baselinedSelected.has(item.id));
971
+ gatedSelectedUnknown = (selectedOptionalCapabilities?.unknown ?? []).filter((id) => !baselinedSelected.has(id));
972
+ baselineReport = {
973
+ present: true,
974
+ applied: true,
975
+ baselined_at: baselineFile.baselined_at,
976
+ excluded: {
977
+ rules: results.filter((result) => result.baselined).length,
978
+ completeness: (completeness?.missing.length ?? 0) - gatedMissing.length,
979
+ selected_optional: ((selectedOptionalCapabilities?.failed.length ?? 0) - gatedSelectedFailed.length) +
980
+ ((selectedOptionalCapabilities?.unknown.length ?? 0) - gatedSelectedUnknown.length),
981
+ },
982
+ residual_caveat: "Baseline excludes pre-existing findings keyed by rule+file (count-based). A NEW violation of an already-baselined rule in the SAME file can be masked at this granularity — review the touched files directly for a precise per-change gate.",
983
+ };
984
+ }
985
+ }
986
+ else if (baselineFile) {
987
+ baselineReport = { present: true, applied: false, baselined_at: baselineFile.baselined_at, hint: "A brownfield baseline exists. Run `vise check --new-only` to gate only on findings introduced since the baseline (pre-existing findings are excluded)." };
988
+ }
989
+ const hasCompletenessGap = gatedMissing.length > 0;
990
+ const hasSelectedOptionalFailures = gatedSelectedFailed.length > 0 || gatedSelectedUnknown.length > 0;
991
+ const { status, exitCode } = deriveGate({
992
+ hasBlocked: gatedResults.some((result) => result.status === "blocked"),
993
+ hasDeterministicFailure: gatedResults.some((result) => result.status === "deterministic-fail"),
994
+ needsAttestation: gatedResults.some((result) => result.status === "attestation-needed" || result.status === "stale"),
995
+ hasCompletenessGap,
996
+ hasSelectedOptionalFailures,
997
+ });
894
998
  const uxHarness = await readUxHarness(repoRoot);
895
999
  const uxAssessment = await assessUxHarness(inspection.effectiveRoot, uxHarness, compliance.outcome);
896
1000
  const experienceReport = buildExperienceReport({
@@ -908,8 +1012,11 @@ export async function checkCompliance(repoPath) {
908
1012
  surfacePath: compliance.surface?.path,
909
1013
  summary,
910
1014
  rules: results,
1015
+ ...(evidenceBasis ? { evidence_basis: evidenceBasis } : {}),
1016
+ ...(evidenceBasisNote ? { evidence_basis_note: evidenceBasisNote } : {}),
911
1017
  ...(completeness && (completeness.missing.length > 0 || completeness.optedOut.length > 0 || completeness.present.length > 0) ? { completeness } : {}),
912
1018
  ...(selectedOptionalCapabilities ? { selectedOptionalCapabilities } : {}),
1019
+ ...(baselineReport ? { baseline: baselineReport } : {}),
913
1020
  uxHarness: uxAssessment,
914
1021
  experienceReport,
915
1022
  };
@@ -1028,7 +1135,12 @@ export async function syncCompliance(repoPath) {
1028
1135
  if (existing?.status === "attested") {
1029
1136
  continue;
1030
1137
  }
1031
- const attestation = buildAttestation(compliance, rule, "spf-deterministic", "high", undefined, "Deterministic check passed.", result.evidence ?? {});
1138
+ const passBasis = result.pass_basis ?? "absent";
1139
+ const confidence = passBasis === "undetected-platform" ? "low" : "medium";
1140
+ const rationale = passBasis === "undetected-platform"
1141
+ ? "Deterministic check passed by absence, but this rule targets a platform not detected in this project, so no sensor exercised it — treat as not-yet-validated."
1142
+ : "Deterministic check passed: the sensor ran and found no violation. Absence of a finding is not positive proof the feature was built.";
1143
+ const attestation = buildAttestation(compliance, rule, "spf-deterministic", confidence, undefined, rationale, { ...(result.evidence ?? {}), pass_basis: passBasis });
1032
1144
  await writeJson(filePath, attestation);
1033
1145
  written.push(filePath);
1034
1146
  }
@@ -1090,6 +1202,7 @@ export async function attestRule(args) {
1090
1202
  throw new Error(`Rule does not allow local human attestations: ${args.ruleId}`);
1091
1203
  }
1092
1204
  validateEvidence(rule, args.evidence);
1205
+ const sensorWarning = await liveSensorContradiction(repoRoot, compliance, rule);
1093
1206
  await mkdir(attestationsDir(repoRoot), { recursive: true });
1094
1207
  const sourceFingerprints = await collectSourceFingerprints(repoRoot, sourceRootForCompliance(repoRoot, compliance), args.evidence);
1095
1208
  const attestation = buildAttestation(compliance, rule, args.signer, args.confidence, args.identity, args.rationale, args.evidence, sourceFingerprints);
@@ -1101,6 +1214,7 @@ export async function attestRule(args) {
1101
1214
  destination: filePath,
1102
1215
  signer_claim: attestation.signer_claim,
1103
1216
  source_fingerprints: sourceFingerprints,
1217
+ ...(sensorWarning ? { sensor_warning: sensorWarning } : {}),
1104
1218
  };
1105
1219
  }
1106
1220
  export async function explainRule(ruleId) {
@@ -1124,6 +1238,9 @@ export async function explainRule(ruleId) {
1124
1238
  title: candidate.title,
1125
1239
  severity: candidate.severity,
1126
1240
  rationale: candidate.rationale,
1241
+ ...(candidate.feedforward ? { feedforward: candidate.feedforward } : {}),
1242
+ ...(candidate.symptoms && candidate.symptoms.length > 0 ? { symptoms: candidate.symptoms } : {}),
1243
+ ...(candidate.enforcement.inferential ? { inferential: candidate.enforcement.inferential } : {}),
1127
1244
  applies_when: candidate.applies_when,
1128
1245
  attestation: candidate.enforcement.attestation,
1129
1246
  summary: ruleSummary(candidate),
@@ -1142,6 +1259,9 @@ export async function explainRule(ruleId) {
1142
1259
  title: rule.title,
1143
1260
  severity: rule.severity,
1144
1261
  rationale: rule.rationale,
1262
+ ...(rule.feedforward ? { feedforward: rule.feedforward } : {}),
1263
+ ...(rule.symptoms && rule.symptoms.length > 0 ? { symptoms: rule.symptoms } : {}),
1264
+ ...(rule.enforcement.inferential ? { inferential: rule.enforcement.inferential } : {}),
1145
1265
  applies_when: rule.applies_when,
1146
1266
  compatible_with: rule.compatible_with,
1147
1267
  deterministic: rule.enforcement.deterministic ?? [],
@@ -1151,9 +1271,44 @@ export async function explainRule(ruleId) {
1151
1271
  freshness: await ruleFreshness(rule),
1152
1272
  };
1153
1273
  }
1154
- export async function statusCompliance(repoPath) {
1274
+ export async function listRules() {
1275
+ const rules = await rulesById();
1276
+ const byPublic = new Map();
1277
+ for (const rule of rules.values()) {
1278
+ const publicId = publicProductRuleId(rule.id);
1279
+ let entry = byPublic.get(publicId);
1280
+ if (!entry) {
1281
+ entry = { id: publicId, contract_rule_ids: new Set(), titles: new Set(), severities: new Set(), outcomes: new Set(), platforms: new Set() };
1282
+ byPublic.set(publicId, entry);
1283
+ }
1284
+ entry.contract_rule_ids.add(rule.id);
1285
+ entry.titles.add(rule.title);
1286
+ entry.severities.add(rule.severity);
1287
+ for (const outcome of rule.applies_when.outcomes ?? [])
1288
+ entry.outcomes.add(outcome);
1289
+ for (const platform of rule.applies_when.platforms ?? [])
1290
+ entry.platforms.add(platform);
1291
+ }
1292
+ const entries = [...byPublic.values()]
1293
+ .sort((a, b) => a.id.localeCompare(b.id))
1294
+ .map((entry) => ({
1295
+ id: entry.id,
1296
+ contract_rule_ids: [...entry.contract_rule_ids].sort(),
1297
+ title: [...entry.titles][0],
1298
+ severity: [...entry.severities].sort()[0],
1299
+ outcomes: [...entry.outcomes].sort(),
1300
+ platforms: [...entry.platforms].sort(),
1301
+ }));
1302
+ return {
1303
+ kind: "rule-index",
1304
+ count: entries.length,
1305
+ hint: "Run `vise explain <id>` with any id below (a public product id or a contract_rule_id) for the full rule, evidence, and attestation policy.",
1306
+ rules: entries,
1307
+ };
1308
+ }
1309
+ export async function statusCompliance(repoPath, options = {}) {
1155
1310
  const repoRoot = path.resolve(repoPath);
1156
- const check = await checkCompliance(repoPath);
1311
+ const check = await checkCompliance(repoPath, options);
1157
1312
  const engagement = await readEngagement(repoRoot);
1158
1313
  const compliance = await readCompliance(repoRoot).catch(() => null);
1159
1314
  return {
@@ -1162,6 +1317,10 @@ export async function statusCompliance(repoPath) {
1162
1317
  outcome: check.outcome,
1163
1318
  surfacePath: check.surfacePath,
1164
1319
  summary: check.summary,
1320
+ ...(check.evidence_basis ? { evidence_basis: check.evidence_basis } : {}),
1321
+ ...(check.evidence_basis_note ? { evidence_basis_note: check.evidence_basis_note } : {}),
1322
+ ...(check.completeness ? { completeness: check.completeness } : {}),
1323
+ ...(check.selectedOptionalCapabilities ? { selectedOptionalCapabilities: check.selectedOptionalCapabilities } : {}),
1165
1324
  ...(compliance?.design_contract && { design_contract: compliance.design_contract }),
1166
1325
  ...(engagement && {
1167
1326
  engagement: {
@@ -1340,6 +1499,36 @@ function deterministicFinding(rule, findingsById) {
1340
1499
  }
1341
1500
  return undefined;
1342
1501
  }
1502
+ async function liveSensorContradiction(repoRoot, compliance, rule) {
1503
+ if (deterministicFindingIds(rule).length === 0) {
1504
+ return undefined;
1505
+ }
1506
+ const inspection = await inspectProject(repoRoot, compliance.surface?.path === "." ? undefined : compliance.surface?.path);
1507
+ const platforms = Array.from(new Set([...inspection.platforms, ...(compliance.surface?.platforms ?? [])]));
1508
+ if (platforms.length === 0) {
1509
+ return undefined;
1510
+ }
1511
+ const allFindings = await Promise.all(platforms.map((platform) => validateSetup(inspection.effectiveRoot, platform)));
1512
+ const findingsById = new Map();
1513
+ for (const finding of allFindings.flat()) {
1514
+ findingsById.set(finding.ruleId, finding);
1515
+ if (finding.sensorId) {
1516
+ findingsById.set(finding.sensorId, finding);
1517
+ }
1518
+ }
1519
+ const finding = deterministicFinding(rule, findingsById);
1520
+ if (!finding) {
1521
+ return undefined;
1522
+ }
1523
+ return {
1524
+ message: "The live deterministic sensor STILL reports a violation for this rule. This attestation records your override; verify the source actually satisfies the rule before relying on it.",
1525
+ sensor_finding: {
1526
+ ruleId: finding.ruleId,
1527
+ ...(finding.sensorId ? { sensorId: finding.sensorId } : {}),
1528
+ ...(finding.recommendation ? { recommendation: finding.recommendation } : {}),
1529
+ },
1530
+ };
1531
+ }
1343
1532
  function deterministicFindingIds(rule) {
1344
1533
  return (rule.enforcement.deterministic ?? [])
1345
1534
  .filter((check) => check.check === "validator-finding-absent")
@@ -1678,6 +1867,71 @@ function attestationsDir(repoRoot) {
1678
1867
  function compliancePath(repoRoot) {
1679
1868
  return path.join(sidecarDir(repoRoot), "compliance.json");
1680
1869
  }
1870
+ const BASELINE_NON_GREEN = new Set(["deterministic-fail", "attestation-needed", "stale", "blocked"]);
1871
+ function baselineKeyFor(result) {
1872
+ const ruleId = result.contractRuleId ?? result.ruleId;
1873
+ return `${ruleId}@${result.finding?.file ?? "*"}`;
1874
+ }
1875
+ function baselinePath(repoRoot) {
1876
+ return path.join(sidecarDir(repoRoot), "baseline.json");
1877
+ }
1878
+ async function readBaseline(repoRoot) {
1879
+ return readJsonIfExists(baselinePath(repoRoot));
1880
+ }
1881
+ function deriveGate(flags) {
1882
+ if (flags.hasBlocked)
1883
+ return { status: "blocked", exitCode: 3 };
1884
+ if (flags.hasDeterministicFailure)
1885
+ return { status: "deterministic-failures", exitCode: 2 };
1886
+ if (flags.needsAttestation)
1887
+ return { status: "needs-attestation", exitCode: 1 };
1888
+ if (flags.hasCompletenessGap)
1889
+ return { status: "completeness-gap", exitCode: 5 };
1890
+ if (flags.hasSelectedOptionalFailures)
1891
+ return { status: "selected-capability-failures", exitCode: 6 };
1892
+ return { status: "green", exitCode: 0 };
1893
+ }
1894
+ export async function recordBaseline(repoPath) {
1895
+ const repoRoot = path.resolve(repoPath);
1896
+ const compliance = await readCompliance(repoRoot);
1897
+ const check = await checkCompliance(repoPath);
1898
+ if (check.status === "contract-drift") {
1899
+ throw new Error("Cannot record a baseline while the contract is drifted (run `vise init` to refresh the sidecar first).");
1900
+ }
1901
+ const findings = {};
1902
+ for (const result of check.rules) {
1903
+ if (!BASELINE_NON_GREEN.has(result.status))
1904
+ continue;
1905
+ const key = baselineKeyFor(result);
1906
+ findings[key] = (findings[key] ?? 0) + 1;
1907
+ }
1908
+ const completeness_missing = (check.completeness?.missing ?? []).map((item) => item.id);
1909
+ const selected_optional = [
1910
+ ...(check.selectedOptionalCapabilities?.failed ?? []).map((item) => item.id),
1911
+ ...(check.selectedOptionalCapabilities?.unknown ?? []),
1912
+ ];
1913
+ const baseline = {
1914
+ baselined_at: new Date().toISOString(),
1915
+ vise_version: packageVersion,
1916
+ outcome: compliance.outcome,
1917
+ ruleset_digest: compliance.ruleset_digest,
1918
+ findings,
1919
+ completeness_missing,
1920
+ selected_optional,
1921
+ };
1922
+ await writeJson(baselinePath(repoRoot), baseline);
1923
+ return {
1924
+ status: "baselined",
1925
+ baseline: complianceDirName + "/baseline.json",
1926
+ outcome: baseline.outcome,
1927
+ recorded: {
1928
+ findings: Object.values(findings).reduce((a, b) => a + b, 0),
1929
+ completeness_missing: completeness_missing.length,
1930
+ selected_optional: selected_optional.length,
1931
+ },
1932
+ nextStep: "Implement your change, then run `vise check --new-only` to gate only on findings introduced since this baseline. Re-run `vise baseline` after the contract or outcome changes.",
1933
+ };
1934
+ }
1681
1935
  function experienceReportPath(repoRoot) {
1682
1936
  return path.join(sidecarDir(repoRoot), experienceReportFileName);
1683
1937
  }
@@ -1,3 +1,5 @@
1
+ import { existsSync } from "node:fs";
2
+ import path from "node:path";
1
3
  import { objectInput, optionalBooleanField, stringField, textResult } from "../types.js";
2
4
  import { checkCompliance, rulesById } from "./compliance.js";
3
5
  import { inspectProject } from "./project.js";
@@ -23,48 +25,94 @@ export async function debugIssue(repoPath, errorMessage, options = {}) {
23
25
  if (errorMessage.includes("method not found") || errorMessage.includes("Unresolved reference")) {
24
26
  likelyCause = "Potential Version Mismatch: The codebase is using a newer API pattern against an older SDK version installed in the project.";
25
27
  }
28
+ const missingModuleMatch = /Cannot find module ['"]([^'"]+)['"]/.exec(errorMessage)
29
+ ?? (/\bMODULE_NOT_FOUND\b/.test(errorMessage) ? /['"]([^'"]+)['"]/.exec(errorMessage) : null);
30
+ const missingModulePkg = missingModuleMatch && missingModuleMatch[1] && !/^[./]/.test(missingModuleMatch[1])
31
+ ? missingModuleMatch[1]
32
+ : undefined;
26
33
  const correlatedRules = [];
27
34
  let checkResult = null;
28
35
  try {
29
36
  checkResult = await checkCompliance(repoPath);
30
37
  const rulesMap = await rulesById();
31
38
  for (const rule of checkResult.rules) {
39
+ const contractRuleId = rule.contractRuleId ?? rule.ruleId;
40
+ const ruleDef = rulesMap.get(contractRuleId);
41
+ const symptoms = ruleDef?.symptoms || [];
42
+ if (symptoms.length === 0 || !symptoms.some((s) => errorMessage.includes(s))) {
43
+ continue;
44
+ }
32
45
  if (rule.status === "deterministic-fail" || rule.status === "attestation-needed") {
33
- const contractRuleId = rule.contractRuleId ?? rule.ruleId;
34
- const ruleDef = rulesMap.get(contractRuleId);
35
- const symptoms = ruleDef?.symptoms || [];
36
- if (symptoms.some(s => errorMessage.includes(s))) {
37
- correlatedRules.push({
38
- ruleId: rule.ruleId,
39
- status: "failed",
40
- impact: `This rule is currently failing and its known symptoms match the crash log.`,
41
- });
42
- }
46
+ correlatedRules.push({
47
+ ruleId: rule.ruleId,
48
+ status: "failed",
49
+ impact: `This rule is currently failing and its known symptoms match the crash log.`,
50
+ });
43
51
  }
44
52
  else if (rule.status === "attested") {
45
- const contractRuleId = rule.contractRuleId ?? rule.ruleId;
46
- const ruleDef = rulesMap.get(contractRuleId);
47
- const symptoms = ruleDef?.symptoms || [];
48
- if (symptoms.some(s => errorMessage.includes(s))) {
49
- correlatedRules.push({
50
- ruleId: rule.ruleId,
51
- status: "attested",
52
- attestation: rule.attestation,
53
- impact: `Warning: This rule was attested as passing, but a matching runtime exception was detected. The custom implementation may be flawed.`,
54
- });
55
- }
53
+ correlatedRules.push({
54
+ ruleId: rule.ruleId,
55
+ status: "attested",
56
+ attestation: rule.attestation,
57
+ impact: `Warning: This rule was attested as passing, but a matching runtime exception was detected. The custom implementation may be flawed.`,
58
+ });
59
+ }
60
+ else {
61
+ correlatedRules.push({
62
+ ruleId: rule.ruleId,
63
+ status: rule.status,
64
+ impact: `This rule's known symptoms match the crash log even though its compliance status is "${rule.status}". The deterministic check did not catch this, so the failure is likely runtime-only or outside the sensor's reach — review the implementation.`,
65
+ });
56
66
  }
57
67
  }
58
68
  }
59
69
  catch (err) {
60
70
  }
61
- if (!likelyCause && correlatedRules.length > 0) {
62
- likelyCause = `The crash is likely caused by the non-compliant implementation of ${correlatedRules.map(r => r.ruleId).join(", ")}.`;
71
+ const failingCorrelated = correlatedRules.filter((r) => r.status === "failed" || r.status === "attested");
72
+ const moduleDiagnosis = missingModulePkg
73
+ ? (() => {
74
+ const projectRoot = path.resolve(repoPath);
75
+ const pkg = missingModulePkg;
76
+ if (!existsSync(path.join(projectRoot, "node_modules"))) {
77
+ return {
78
+ cause: `Dependencies are not installed — node_modules is absent at ${projectRoot}, so "${pkg}" cannot be resolved. This is a setup/environment issue, not a code defect.`,
79
+ hint: `Run \`npm install\` at the project root to install declared dependencies, then retry. If "${pkg}" is not yet a dependency, add it: \`npm install ${pkg}\`.`,
80
+ };
81
+ }
82
+ if (!existsSync(path.join(projectRoot, "node_modules", ...pkg.split("/")))) {
83
+ return {
84
+ cause: `The module "${pkg}" is not present in node_modules, so it cannot be resolved. This is a missing-dependency issue, not a code defect.`,
85
+ hint: `Install it at the project root: \`npm install ${pkg}\`, then retry. (If it should already be installed, delete node_modules and re-run \`npm install\`.)`,
86
+ };
87
+ }
88
+ return {
89
+ cause: `The module "${pkg}" could not be resolved at runtime even though it appears installed. Confirm it is declared in package.json and the install is intact.`,
90
+ hint: `Re-run \`npm install\` to repair the dependency tree; if "${pkg}" is missing from package.json, add it with \`npm install ${pkg}\`.`,
91
+ };
92
+ })()
93
+ : undefined;
94
+ let missingModuleHint;
95
+ let secondaryModuleNote;
96
+ if (!likelyCause && failingCorrelated.length > 0) {
97
+ likelyCause = `The crash is likely caused by the non-compliant implementation of ${failingCorrelated.map(r => r.ruleId).join(", ")}.`;
98
+ if (moduleDiagnosis)
99
+ secondaryModuleNote = moduleDiagnosis.hint;
100
+ }
101
+ else if (!likelyCause && moduleDiagnosis) {
102
+ likelyCause = moduleDiagnosis.cause;
103
+ missingModuleHint = moduleDiagnosis.hint;
104
+ }
105
+ else if (!likelyCause && correlatedRules.length > 0) {
106
+ likelyCause = `The crash symptoms match the known signatures of ${correlatedRules.map(r => r.ruleId).join(", ")}, but ${correlatedRules.length === 1 ? "that rule passes its" : "those rules pass their"} compliance check — the cause is likely runtime-only or outside the deterministic sensor's reach.`;
63
107
  }
64
108
  else if (!likelyCause) {
65
109
  likelyCause = `An unknown ${exceptionClass || "runtime"} error occurred.`;
110
+ if (moduleDiagnosis)
111
+ secondaryModuleNote = moduleDiagnosis.hint;
66
112
  }
67
- const suggestedRemediation = buildRemediation(correlatedRules, checkResult);
113
+ const suggestedRemediation = missingModuleHint
114
+ ? { action: missingModuleHint }
115
+ : buildRemediation(correlatedRules, checkResult);
68
116
  const repairBrief = buildRepairBrief(correlatedRules, checkResult, inspection.platforms, likelyCause);
69
117
  const payload = {
70
118
  correlatedRules,
@@ -73,6 +121,7 @@ export async function debugIssue(repoPath, errorMessage, options = {}) {
73
121
  extractedException: exceptionClass,
74
122
  suggestedRemediation,
75
123
  repairBrief,
124
+ ...(secondaryModuleNote ? { secondaryNotes: [secondaryModuleNote] } : {}),
76
125
  relevantDocs: [
77
126
  {
78
127
  title: "Troubleshooting",
@@ -85,6 +134,7 @@ export async function debugIssue(repoPath, errorMessage, options = {}) {
85
134
  likelyCause: payload.likelyCause,
86
135
  correlatedRules: payload.correlatedRules,
87
136
  repairBrief: payload.repairBrief,
137
+ ...(secondaryModuleNote ? { secondaryNotes: [secondaryModuleNote] } : {}),
88
138
  relevantDocs: payload.relevantDocs,
89
139
  };
90
140
  }
@@ -106,8 +156,12 @@ function buildRemediation(correlatedRules, checkResult) {
106
156
  docRef: `vise explain ${cr.ruleId}`,
107
157
  };
108
158
  });
159
+ const failing = correlatedRules.filter((cr) => cr.status === "failed" || cr.status === "attested");
160
+ const action = failing.length > 0
161
+ ? "Repair the compliance failure(s) identified above."
162
+ : "The correlated rule(s) pass their deterministic compliance checks; investigate the runtime behavior of the listed rule(s). The failure is likely runtime-only or outside the sensor's reach — do not change the compliant code paths.";
109
163
  return {
110
- action: "Repair the compliance failure(s) identified above.",
164
+ action,
111
165
  rules,
112
166
  };
113
167
  }
@@ -143,7 +197,10 @@ function buildRepairBrief(correlatedRules, checkResult, platforms, likelyCause)
143
197
  }
144
198
  function pickPrimaryRuleId(correlatedRules) {
145
199
  const failed = correlatedRules.find((rule) => rule.status === "failed");
146
- return failed?.ruleId ?? correlatedRules[0]?.ruleId ?? null;
200
+ if (failed)
201
+ return failed.ruleId;
202
+ const attested = correlatedRules.find((rule) => rule.status === "attested");
203
+ return attested?.ruleId ?? null;
147
204
  }
148
205
  function candidateFilesForRule(ruleId, checkResult) {
149
206
  if (!checkResult) {