@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.
- package/CHANGELOG.md +30 -0
- package/README.md +4 -1
- package/dist/capabilities.js +6 -3
- package/dist/explore.js +51 -0
- package/dist/humanFormat.js +226 -0
- package/dist/outcomes.js +15 -14
- package/dist/server.js +110 -38
- package/dist/solutionPath.js +1 -1
- package/dist/tools/compliance.js +289 -35
- package/dist/tools/debug.js +83 -26
- package/dist/tools/design.js +24 -4
- package/dist/tools/project.js +26 -8
- package/dist/tools/sdkFacts.js +8 -1
- package/dist/uikitCustomization.js +19 -5
- package/package.json +1 -1
package/dist/tools/compliance.js
CHANGED
|
@@ -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([
|
|
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
|
-
...
|
|
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
|
|
862
|
-
const
|
|
863
|
-
|
|
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
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
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
|
|
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
|
|
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
|
}
|
package/dist/tools/debug.js
CHANGED
|
@@ -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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
62
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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) {
|