@amityco/social-plus-vise 0.14.26 → 0.14.27

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.
@@ -10,8 +10,11 @@ import { packageVersion } from "../version.js";
10
10
  import { DESIGN_CONTRACT_CONFIRMATION_ANSWER_ID, buildDesignBrief, designContractConfirmationFromAnswers, designPreviewPath, readDesignContract, } from "./design.js";
11
11
  import { inspectProject, validateSetup } from "./project.js";
12
12
  import { readCreativeSelection } from "./creative.js";
13
+ import { assessUxHarness, buildExperienceReport, buildUxHarness, readUxHarness, } from "./uxHarness.js";
13
14
  const complianceDirName = "sp-vise";
14
15
  const attestationsDirName = "attestations";
16
+ const experienceReportFileName = "experience-report.json";
17
+ const experienceSensorsFileName = "experience-sensors.json";
15
18
  const schemaVersion = 1;
16
19
  const confidenceRank = { low: 0, medium: 1, high: 2 };
17
20
  export const initComplianceTool = {
@@ -57,6 +60,26 @@ export const checkComplianceTool = {
57
60
  return textResult(await checkCompliance(stringField(args, "repoPath")));
58
61
  },
59
62
  };
63
+ export const experienceReportTool = {
64
+ name: "experience_report",
65
+ description: "Generate the advisory dimensioned Experience Report from the current compliance, design, UX, and business-alignment evidence.",
66
+ inputSchema: {
67
+ type: "object",
68
+ properties: {
69
+ repoPath: { type: "string" },
70
+ write: {
71
+ type: "boolean",
72
+ description: "Whether to write sp-vise/experience-report.json. Defaults to true.",
73
+ },
74
+ },
75
+ required: ["repoPath"],
76
+ additionalProperties: false,
77
+ },
78
+ async call(input) {
79
+ const args = objectInput(input);
80
+ return textResult(await generateExperienceReport(stringField(args, "repoPath"), { write: optionalBooleanField(args, "write", true) }));
81
+ },
82
+ };
60
83
  function answersFromInput(raw) {
61
84
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
62
85
  return {};
@@ -303,6 +326,9 @@ export async function initCompliance(repoPath, request, surfacePath, answers = {
303
326
  const creativeSelection = storedCreativeSelection && creativeSelectionAppliesToRequest(storedCreativeSelection, request)
304
327
  ? storedCreativeSelection
305
328
  : undefined;
329
+ const uxHarness = creativeSelection
330
+ ? await buildUxHarness({ repoPath: repoRoot, surfacePath, selection: creativeSelection, write: true })
331
+ : undefined;
306
332
  const intake = intakeAuditFor({
307
333
  request,
308
334
  outcome,
@@ -352,6 +378,7 @@ export async function initCompliance(repoPath, request, surfacePath, answers = {
352
378
  request,
353
379
  outcome,
354
380
  ...(creativeSelection ? { creative_selection: creativeSelectionSummary(creativeSelection) } : {}),
381
+ ...(uxHarness ? { ux_harness: uxHarnessSummary(uxHarness) } : {}),
355
382
  design_review: designReview,
356
383
  ...intake,
357
384
  });
@@ -380,6 +407,7 @@ export async function initCompliance(repoPath, request, surfacePath, answers = {
380
407
  rules: refs.length,
381
408
  engagement_id: engagement?.engagement_id,
382
409
  ...(creativeSelection ? { creative_selection: creativeSelectionSummary(creativeSelection) } : {}),
410
+ ...(uxHarness ? { ux_harness: uxHarnessSummary(uxHarness) } : {}),
383
411
  ...(compliance.design_contract && { design_contract: compliance.design_contract }),
384
412
  ...(selectedOptionalCapabilities.length > 0 && { selected_optional_capabilities: selectedOptionalCapabilities }),
385
413
  intake: {
@@ -496,6 +524,17 @@ function creativeSelectionSummary(selection) {
496
524
  },
497
525
  };
498
526
  }
527
+ function uxHarnessSummary(harness) {
528
+ return {
529
+ status: harness.status,
530
+ harness_path: "sp-vise/ux-harness.json",
531
+ variant_id: harness.selectedVariant.id,
532
+ variant_title: harness.selectedVariant.title,
533
+ ux_pattern_ids: harness.uxPatterns.map((pattern) => pattern.id),
534
+ expectation_count: harness.uxPatterns.reduce((sum, pattern) => sum + pattern.expectations.length, 0),
535
+ advisory_only: "UX Harness guidance does not change deterministic compliance rules or exit codes in the MVP.",
536
+ };
537
+ }
499
538
  function creativeSelectionAppliesToRequest(selection, request) {
500
539
  const normalizedRequest = normalizeCreativeRequest(request);
501
540
  const original = normalizeCreativeRequest(selection.source.request);
@@ -715,25 +754,133 @@ export async function checkCompliance(repoPath) {
715
754
  : hasSelectedOptionalFailures
716
755
  ? "selected-capability-failures"
717
756
  : "green";
757
+ const exitCode = hasBlocked
758
+ ? 3
759
+ : hasDeterministicFailure
760
+ ? 2
761
+ : needsAttestation
762
+ ? 1
763
+ : hasCompletenessGap
764
+ ? 5
765
+ : hasSelectedOptionalFailures
766
+ ? 6
767
+ : 0;
768
+ const uxHarness = await readUxHarness(repoRoot);
769
+ const uxAssessment = await assessUxHarness(inspection.effectiveRoot, uxHarness, compliance.outcome);
770
+ const experienceReport = buildExperienceReport({
771
+ checkStatus: status,
772
+ exitCode,
773
+ summary,
774
+ designContract: compliance.design_contract,
775
+ uxAssessment,
776
+ uxHarness,
777
+ });
718
778
  return {
719
779
  status,
720
- exitCode: hasBlocked
721
- ? 3
722
- : hasDeterministicFailure
723
- ? 2
724
- : needsAttestation
725
- ? 1
726
- : hasCompletenessGap
727
- ? 5
728
- : hasSelectedOptionalFailures
729
- ? 6
730
- : 0,
780
+ exitCode,
731
781
  outcome: compliance.outcome,
732
782
  surfacePath: compliance.surface?.path,
733
783
  summary,
734
784
  rules: results,
735
785
  ...(completeness && (completeness.missing.length > 0 || completeness.optedOut.length > 0 || completeness.present.length > 0) ? { completeness } : {}),
736
786
  ...(selectedOptionalCapabilities ? { selectedOptionalCapabilities } : {}),
787
+ uxHarness: uxAssessment,
788
+ experienceReport,
789
+ };
790
+ }
791
+ export async function generateExperienceReport(repoPath, options = {}) {
792
+ const repoRoot = path.resolve(repoPath);
793
+ const check = await checkCompliance(repoRoot);
794
+ const baseReport = check.experienceReport ?? fallbackExperienceReport(check);
795
+ const experienceSensors = await readExperienceSensorsReportSnapshot(repoRoot);
796
+ const report = experienceSensors ? { ...baseReport, experienceSensors } : baseReport;
797
+ const result = {
798
+ status: options.write === false ? "generated" : "written",
799
+ checkStatus: check.status,
800
+ checkExitCode: check.exitCode,
801
+ advisoryOnly: "Experience Report is advisory. Use `vise check --ci` when the caller needs compliance exit-code semantics.",
802
+ report,
803
+ };
804
+ if (options.write !== false) {
805
+ const reportPath = experienceReportPath(repoRoot);
806
+ await writeJson(reportPath, report);
807
+ result.artifact = path.join(complianceDirName, experienceReportFileName);
808
+ }
809
+ return result;
810
+ }
811
+ async function readExperienceSensorsReportSnapshot(repoRoot) {
812
+ const sidecar = await readJsonIfExists(path.join(sidecarDir(repoRoot), experienceSensorsFileName));
813
+ if (!sidecar) {
814
+ return undefined;
815
+ }
816
+ return {
817
+ artifact: "sp-vise/experience-sensors.json",
818
+ status: typeof sidecar.status === "string" ? sidecar.status : "unknown",
819
+ dimensions: sensorDimensionStatuses(sidecar.dimensions),
820
+ reviewGaps: sensorReviewGaps(sidecar.dimensions),
821
+ score: null,
822
+ };
823
+ }
824
+ function sensorDimensionStatuses(raw) {
825
+ const dimensions = objectInput(raw);
826
+ const result = {};
827
+ for (const [dimensionId, value] of Object.entries(dimensions)) {
828
+ const dimension = objectInput(value);
829
+ result[dimensionId] = typeof dimension.status === "string" ? dimension.status : "unknown";
830
+ }
831
+ return result;
832
+ }
833
+ function sensorReviewGaps(raw) {
834
+ const dimensions = objectInput(raw);
835
+ const gaps = [];
836
+ for (const [dimensionId, value] of Object.entries(dimensions)) {
837
+ const dimension = objectInput(value);
838
+ const sensors = Array.isArray(dimension.sensors) ? dimension.sensors : [];
839
+ for (const rawSensor of sensors) {
840
+ const sensor = objectInput(rawSensor);
841
+ const status = typeof sensor.status === "string" ? sensor.status : "unknown";
842
+ if (status !== "needs-review" && status !== "needs-setup") {
843
+ continue;
844
+ }
845
+ gaps.push({
846
+ dimension: typeof sensor.dimension === "string" ? sensor.dimension : dimensionId,
847
+ id: typeof sensor.id === "string" ? sensor.id : "unknown",
848
+ status,
849
+ title: typeof sensor.title === "string" ? sensor.title : "Unknown sensor",
850
+ reason: typeof sensor.reason === "string" ? sensor.reason : "No reason recorded.",
851
+ });
852
+ }
853
+ }
854
+ return gaps.slice(0, 50);
855
+ }
856
+ function fallbackExperienceReport(check) {
857
+ const uxAssessment = check.uxHarness ?? {
858
+ status: "absent",
859
+ advisoryOnly: "UX Harness guidance is advisory in the MVP. It does not change deterministic compliance exit codes until benchmark evidence justifies gating.",
860
+ assessedExpectations: [],
861
+ summary: {},
862
+ nextStep: "No UX Harness assessment is available for this check result.",
863
+ };
864
+ return {
865
+ status: "advisory",
866
+ dimensions: {
867
+ technical: {
868
+ status: check.status,
869
+ summary: check.summary,
870
+ exitCode: check.exitCode,
871
+ },
872
+ design: {
873
+ status: "absent",
874
+ summary: "No accepted design contract is available for this report.",
875
+ },
876
+ ux: uxAssessment,
877
+ businessAlignment: {
878
+ status: "absent",
879
+ summary: "No accepted creative variant is available for business-alignment reporting.",
880
+ },
881
+ },
882
+ score: null,
883
+ nextStep: "Resolve any technical check issue first, then regenerate the report. Do not publish a single Experience Score until repeated dogfood and benchmark evidence calibrates it responsibly.",
737
884
  };
738
885
  }
739
886
  export async function syncCompliance(repoPath) {
@@ -1395,6 +1542,9 @@ function attestationsDir(repoRoot) {
1395
1542
  function compliancePath(repoRoot) {
1396
1543
  return path.join(sidecarDir(repoRoot), "compliance.json");
1397
1544
  }
1545
+ function experienceReportPath(repoRoot) {
1546
+ return path.join(sidecarDir(repoRoot), experienceReportFileName);
1547
+ }
1398
1548
  function attestationPathFor(ruleId) {
1399
1549
  return `${ruleId.replace(/[^a-zA-Z0-9_.-]/g, "_")}.json`;
1400
1550
  }