@amityco/social-plus-vise 0.14.8 → 0.14.9

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 CHANGED
@@ -4,6 +4,12 @@ All notable changes to `@amityco/social-plus-vise` are documented in this file.
4
4
 
5
5
  The format is loosely based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## 0.14.9 — 2026-06-05
8
+
9
+ ### Changed
10
+ - **Product expectation bindings:** product-flow checks now surface platform-agnostic expectation IDs such as `chat.unread-visible`, `chat.message-order-explicit`, `feed.rich-post-composer-scope`, and `profile.social-counts` while retaining the concrete platform validator as `sensorId`/`validator` evidence.
11
+ - **Backwards-compatible compliance contracts:** existing Android rule contracts continue to sync, attest, and explain through their legacy contract IDs, while `vise plan` and `vise check` use the shared product expectation IDs as the public-facing rule IDs.
12
+
7
13
  ## 0.14.8 — 2026-06-05
8
14
 
9
15
  ### Added
package/dist/outcomes.js CHANGED
@@ -657,9 +657,9 @@ const addFeed = {
657
657
  `${platform}.feed.post-datatype-handled`,
658
658
  ...(platform === "android"
659
659
  ? [
660
- `${platform}.feed.rich-post-composer-surfaced`,
661
- `${platform}.comments.thread-ui-states-present`,
662
- `${platform}.profile.social-counts-from-sdk`,
660
+ "feed.rich-post-composer-scope",
661
+ "comments.thread-read-write",
662
+ "profile.social-counts",
663
663
  ]
664
664
  : []),
665
665
  ],
@@ -1027,7 +1027,7 @@ const addChat = {
1027
1027
  `${platform}.chat.message-observer-cleanup`,
1028
1028
  `${platform}.chat.send-error-handling`,
1029
1029
  `${platform}.chat.moderation-affordance-present`,
1030
- ...(platform === "android" ? [`${platform}.chat.unread-visible`, `${platform}.chat.sort-explicit`] : []),
1030
+ ...(platform === "android" ? ["chat.unread-visible", "chat.message-order-explicit"] : []),
1031
1031
  ],
1032
1032
  stopConditions: (ctx) => filterStops(ctx.answers, [
1033
1033
  { id: "chat_shape", text: "The chat shape is unknown; cannot implement without knowing 1:1, group, or community channel." },
@@ -1271,6 +1271,7 @@ const addFollow = {
1271
1271
  validation: () => [
1272
1272
  "follower/following lists use a Live Collection (not a one-shot query)",
1273
1273
  "follow model handled (automatic vs follow-request approval)",
1274
+ "profile.social-counts",
1274
1275
  "no invented userId",
1275
1276
  "relationship observer cleaned up on lifecycle end",
1276
1277
  "validate_setup",
@@ -0,0 +1,56 @@
1
+ export const PRODUCT_EXPECTATION_BINDINGS = [
2
+ {
3
+ expectationId: "feed.rich-post-composer-scope",
4
+ sensorId: "android.feed.rich-post-composer-surfaced",
5
+ platform: "android",
6
+ },
7
+ {
8
+ expectationId: "comments.thread-read-write",
9
+ sensorId: "android.comments.thread-ui-states-present",
10
+ platform: "android",
11
+ },
12
+ {
13
+ expectationId: "chat.unread-visible",
14
+ sensorId: "android.chat.unread-visible",
15
+ platform: "android",
16
+ },
17
+ {
18
+ expectationId: "chat.message-order-explicit",
19
+ sensorId: "android.chat.sort-explicit",
20
+ platform: "android",
21
+ },
22
+ {
23
+ expectationId: "profile.social-counts",
24
+ sensorId: "android.profile.social-counts-from-sdk",
25
+ platform: "android",
26
+ },
27
+ ];
28
+ const bindingsBySensorId = new Map(PRODUCT_EXPECTATION_BINDINGS.map((binding) => [binding.sensorId, binding]));
29
+ export function productExpectationBindingForSensor(sensorId) {
30
+ return bindingsBySensorId.get(sensorId);
31
+ }
32
+ export function productFindingIdentity(sensorId) {
33
+ const binding = productExpectationBindingForSensor(sensorId);
34
+ if (!binding) {
35
+ return { ruleId: sensorId };
36
+ }
37
+ return {
38
+ ruleId: binding.expectationId,
39
+ sensorId: binding.sensorId,
40
+ validator: {
41
+ platform: binding.platform,
42
+ sensorId: binding.sensorId,
43
+ },
44
+ };
45
+ }
46
+ export function publicProductRuleId(ruleId) {
47
+ return productExpectationBindingForSensor(ruleId)?.expectationId ?? ruleId;
48
+ }
49
+ export function findingMatchesId(finding, id) {
50
+ return finding.ruleId === id || finding.sensorId === id;
51
+ }
52
+ export function contractRuleCandidatesForPublicId(ruleId) {
53
+ return PRODUCT_EXPECTATION_BINDINGS
54
+ .filter((binding) => binding.expectationId === ruleId)
55
+ .map((binding) => binding.sensorId);
56
+ }
@@ -4,6 +4,7 @@ import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { assessProjectCompleteness, assessProjectSelectedOptionalCapabilities, availableOptionalCapabilityIds, optionalCapabilityChecklist, platformCapabilityAvailability, selectedOptionalCapabilityIds, } from "../capabilities.js";
6
6
  import { getOutcomeDefinition, hasAnswer, planContextFor, resolveOutcome, } from "../outcomes.js";
7
+ import { contractRuleCandidatesForPublicId, productExpectationBindingForSensor, publicProductRuleId, } from "../productExpectations.js";
7
8
  import { objectInput, optionalBooleanField, optionalStringField, stringField, textResult } from "../types.js";
8
9
  import { packageVersion } from "../version.js";
9
10
  import { DESIGN_CONTRACT_CONFIRMATION_ANSWER_ID, buildDesignBrief, designContractConfirmationFromAnswers, designPreviewPath, readDesignContract, } from "./design.js";
@@ -497,13 +498,19 @@ export async function checkCompliance(repoPath) {
497
498
  const platforms = platformsToValidate.length > 0 ? platformsToValidate : ["unknown"];
498
499
  const allFindings = await Promise.all(platforms.map((p) => validateSetup(inspection.effectiveRoot, p)));
499
500
  const findings = allFindings.flat();
500
- const findingsById = new Map(findings.map((finding) => [finding.ruleId, finding]));
501
+ const findingsById = new Map();
502
+ for (const finding of findings) {
503
+ findingsById.set(finding.ruleId, finding);
504
+ if (finding.sensorId) {
505
+ findingsById.set(finding.sensorId, finding);
506
+ }
507
+ }
501
508
  const attestations = await readAttestations(repoRoot);
502
509
  const results = [];
503
510
  for (const ref of compliance.rules) {
504
511
  const rule = rules.get(ref.rule_id);
505
512
  if (!rule) {
506
- results.push({ ruleId: ref.rule_id, title: ref.rule_id, severity: ref.severity, status: "stale", reason: "Rule is missing from installed Vise." });
513
+ results.push({ ...checkRuleIdentity(ref.rule_id), title: ref.rule_id, severity: ref.severity, status: "stale", reason: "Rule is missing from installed Vise." });
507
514
  continue;
508
515
  }
509
516
  // Blockers run first. If any external prerequisite is missing, the rule is
@@ -514,7 +521,7 @@ export async function checkCompliance(repoPath) {
514
521
  if (blockersFired.length > 0) {
515
522
  const attestable = rule.enforcement.attestation.allowed;
516
523
  results.push({
517
- ruleId: rule.id,
524
+ ...checkRuleIdentity(rule.id),
518
525
  title: rule.title,
519
526
  severity: rule.severity,
520
527
  status: "blocked",
@@ -532,7 +539,7 @@ export async function checkCompliance(repoPath) {
532
539
  const finding = hasDeterministicChecks ? deterministicFinding(rule, findingsById) : undefined;
533
540
  if (hasDeterministicChecks && !finding) {
534
541
  results.push({
535
- ruleId: rule.id,
542
+ ...checkRuleIdentity(rule.id),
536
543
  title: rule.title,
537
544
  severity: rule.severity,
538
545
  status: "deterministic-pass",
@@ -548,7 +555,7 @@ export async function checkCompliance(repoPath) {
548
555
  if (attestation.status === "deterministic-pass") {
549
556
  const failStatus = rule.advisory ? "advisory" : rule.enforcement.attestation.allowed ? "attestation-needed" : "deterministic-fail";
550
557
  results.push({
551
- ruleId: rule.id,
558
+ ...checkRuleIdentity(rule.id),
552
559
  title: rule.title,
553
560
  severity: rule.severity,
554
561
  status: failStatus,
@@ -574,7 +581,7 @@ export async function checkCompliance(repoPath) {
574
581
  if (staleFingerprints.length > 0) {
575
582
  const fingerprintStatus = rule.advisory ? "advisory" : rule.enforcement.attestation.allowed ? "attestation-needed" : "deterministic-fail";
576
583
  results.push({
577
- ruleId: rule.id,
584
+ ...checkRuleIdentity(rule.id),
578
585
  title: rule.title,
579
586
  severity: rule.severity,
580
587
  status: fingerprintStatus,
@@ -592,7 +599,7 @@ export async function checkCompliance(repoPath) {
592
599
  }
593
600
  const deprecated = grandfathered && (rule.deprecated_versions ?? []).includes(attestation.rule_version);
594
601
  results.push({
595
- ruleId: rule.id,
602
+ ...checkRuleIdentity(rule.id),
596
603
  title: rule.title,
597
604
  severity: rule.severity,
598
605
  status: "attested",
@@ -623,7 +630,7 @@ export async function checkCompliance(repoPath) {
623
630
  fallbackReason = "Deterministic check failed and no valid attestation exists.";
624
631
  }
625
632
  results.push({
626
- ruleId: rule.id,
633
+ ...checkRuleIdentity(rule.id),
627
634
  title: rule.title,
628
635
  severity: rule.severity,
629
636
  status: baseStatus,
@@ -692,9 +699,10 @@ export async function syncCompliance(repoPath) {
692
699
  const removed = [];
693
700
  await mkdir(attestationsDir(repoRoot), { recursive: true });
694
701
  for (const result of check.rules) {
695
- const filePath = path.join(attestationsDir(repoRoot), attestationPathFor(result.ruleId));
702
+ const contractRuleId = result.contractRuleId ?? result.ruleId;
703
+ const filePath = path.join(attestationsDir(repoRoot), attestationPathFor(contractRuleId));
696
704
  if (result.status === "deterministic-pass") {
697
- const rule = (await rulesById()).get(result.ruleId);
705
+ const rule = (await rulesById()).get(contractRuleId);
698
706
  if (!rule) {
699
707
  continue;
700
708
  }
@@ -727,17 +735,18 @@ export async function attestRule(args) {
727
735
  const repoRoot = path.resolve(args.repoPath);
728
736
  const compliance = await readCompliance(repoRoot);
729
737
  const rules = await rulesById();
730
- const rule = rules.get(args.ruleId);
731
- if (!rule || !compliance.rules.some((ref) => ref.rule_id === args.ruleId)) {
738
+ const contractRuleId = resolveRuleIdForContract(args.ruleId, compliance, rules);
739
+ const rule = contractRuleId ? rules.get(contractRuleId) : undefined;
740
+ if (!rule || !contractRuleId) {
732
741
  // Collect up to 8 applicable attestable rule ids from this contract for the error hint. Prefer
733
742
  // ids that share the bad id's non-wildcard prefix so the agent can narrow down quickly.
734
743
  const attestableIds = compliance.rules
735
744
  .map((ref) => rules.get(ref.rule_id))
736
745
  .filter((r) => r !== undefined && r.enforcement.attestation.allowed);
737
746
  const prefix = args.ruleId.replace(/\.\*$|\*$/, "");
738
- const prefixed = prefix !== args.ruleId ? attestableIds.filter((r) => r.id.startsWith(prefix) || r.id.includes(prefix)) : [];
747
+ const prefixed = prefix !== args.ruleId ? attestableIds.filter((r) => r.id.startsWith(prefix) || r.id.includes(prefix) || publicProductRuleId(r.id).startsWith(prefix)) : [];
739
748
  const candidates = prefixed.length > 0 ? prefixed : attestableIds;
740
- const hintIds = candidates.slice(0, 8).map((r) => r.id);
749
+ const hintIds = candidates.slice(0, 8).map((r) => publicProductRuleId(r.id));
741
750
  const hintSuffix = hintIds.length > 0 ? ` Applicable attestable rules: ${hintIds.join(", ")}.` : " Applicable attestable rules: none.";
742
751
  const preamble = args.ruleId.includes("*")
743
752
  ? `Wildcards are not supported — attest one rule at a time.`
@@ -757,11 +766,11 @@ export async function attestRule(args) {
757
766
  await mkdir(attestationsDir(repoRoot), { recursive: true });
758
767
  const sourceFingerprints = await collectSourceFingerprints(repoRoot, sourceRootForCompliance(repoRoot, compliance), args.evidence);
759
768
  const attestation = buildAttestation(compliance, rule, args.signer, args.confidence, args.identity, args.rationale, args.evidence, sourceFingerprints);
760
- const filePath = path.join(attestationsDir(repoRoot), attestationPathFor(args.ruleId));
769
+ const filePath = path.join(attestationsDir(repoRoot), attestationPathFor(contractRuleId));
761
770
  await writeJson(filePath, attestation);
762
771
  return {
763
772
  status: "attested",
764
- ruleId: args.ruleId,
773
+ ...checkRuleIdentity(contractRuleId),
765
774
  destination: filePath,
766
775
  signer_claim: attestation.signer_claim,
767
776
  source_fingerprints: sourceFingerprints,
@@ -769,12 +778,14 @@ export async function attestRule(args) {
769
778
  }
770
779
  export async function explainRule(ruleId) {
771
780
  const rules = await rulesById();
772
- const rule = rules.get(ruleId);
773
- if (!rule) {
781
+ const contractRuleId = resolveRuleIdForInstalledRules(ruleId, rules);
782
+ const rule = contractRuleId ? rules.get(contractRuleId) : undefined;
783
+ if (!rule || !contractRuleId) {
774
784
  throw new Error(`Unknown compliance rule: ${ruleId}`);
775
785
  }
776
786
  return {
777
- id: rule.id,
787
+ id: publicProductRuleId(rule.id),
788
+ ...(publicProductRuleId(rule.id) !== rule.id ? { contract_rule_id: rule.id } : {}),
778
789
  version: rule.version,
779
790
  title: rule.title,
780
791
  severity: rule.severity,
@@ -836,6 +847,34 @@ export async function rulesById() {
836
847
  const rules = (await loadRuleFiles()).flatMap((file) => file.rules);
837
848
  return new Map(rules.map((rule) => [rule.id, rule]));
838
849
  }
850
+ function checkRuleIdentity(contractRuleId) {
851
+ const binding = productExpectationBindingForSensor(contractRuleId);
852
+ if (!binding) {
853
+ return { ruleId: contractRuleId };
854
+ }
855
+ return {
856
+ ruleId: binding.expectationId,
857
+ contractRuleId,
858
+ validator: {
859
+ platform: binding.platform,
860
+ sensorId: binding.sensorId,
861
+ },
862
+ };
863
+ }
864
+ function resolveRuleIdForContract(ruleId, compliance, rules) {
865
+ if (rules.has(ruleId) && compliance.rules.some((ref) => ref.rule_id === ruleId)) {
866
+ return ruleId;
867
+ }
868
+ const candidates = contractRuleCandidatesForPublicId(ruleId).filter((candidate) => rules.has(candidate) && compliance.rules.some((ref) => ref.rule_id === candidate));
869
+ return candidates.length === 1 ? candidates[0] : null;
870
+ }
871
+ function resolveRuleIdForInstalledRules(ruleId, rules) {
872
+ if (rules.has(ruleId)) {
873
+ return ruleId;
874
+ }
875
+ const candidates = contractRuleCandidatesForPublicId(ruleId).filter((candidate) => rules.has(candidate));
876
+ return candidates.length === 1 ? candidates[0] : null;
877
+ }
839
878
  function ruleRef(rule) {
840
879
  return {
841
880
  rule_id: rule.id,
@@ -847,15 +886,21 @@ function ruleRef(rule) {
847
886
  // Extends ruleRef with the human-readable title for file output (compliance.json,
848
887
  // applicableRules in integration plans). Not used for digest computation.
849
888
  function ruleRefForFile(rule) {
850
- return { ...ruleRef(rule), title: rule.title };
889
+ const publicRuleId = publicProductRuleId(rule.id);
890
+ return {
891
+ ...ruleRef(rule),
892
+ ...(publicRuleId !== rule.id ? { public_rule_id: publicRuleId } : {}),
893
+ title: rule.title,
894
+ };
851
895
  }
852
896
  // Benchmark-measured friction: agents looped on attest dialect for ~25 min/cell when docs and SDK
853
897
  // disagreed on exact invocation syntax (capability-matrix 2026-06, Row 5). Hand them the exact incantation.
854
898
  function attestHint(rule) {
855
899
  const minConfidence = rule.enforcement.attestation.host_agent_min_confidence ?? "high";
856
900
  const fields = rule.enforcement.attestation.evidence_required ?? [];
901
+ const publicRuleId = publicProductRuleId(rule.id);
857
902
  return {
858
- attest_command: `vise attest --rule ${rule.id} --confidence ${minConfidence} --signer host-agent --evidence-file sp-vise/evidence/${rule.id}.json --rationale "<why this rule is satisfied (or cannot apply) in this codebase>"`,
903
+ attest_command: `vise attest --rule ${publicRuleId} --confidence ${minConfidence} --signer host-agent --evidence-file sp-vise/evidence/${rule.id}.json --rationale "<why this rule is satisfied (or cannot apply) in this codebase>"`,
859
904
  evidence_template: Object.fromEntries(fields.map((f) => [f.field, `<${f.description}>`])),
860
905
  };
861
906
  }
@@ -878,7 +923,7 @@ function contractDrift(compliance, rules) {
878
923
  for (const ref of compliance.rules) {
879
924
  const rule = rules.get(ref.rule_id);
880
925
  if (!rule) {
881
- results.push({ ruleId: ref.rule_id, title: ref.rule_id, severity: ref.severity, status: "stale", reason: "Rule is missing from installed Vise." });
926
+ results.push({ ...checkRuleIdentity(ref.rule_id), title: ref.rule_id, severity: ref.severity, status: "stale", reason: "Rule is missing from installed Vise." });
882
927
  continue;
883
928
  }
884
929
  const digest = digestRule(rule);
@@ -888,7 +933,7 @@ function contractDrift(compliance, rules) {
888
933
  ? `Rule digest changed since vise init (v${ref.rule_version} → v${rule.version}; new version is backwards-compatible — re-run vise init; existing attestations will be grandfathered).`
889
934
  : `Rule digest changed since vise init (v${ref.rule_version} → v${rule.version}).`;
890
935
  results.push({
891
- ruleId: ref.rule_id,
936
+ ...checkRuleIdentity(ref.rule_id),
892
937
  title: rule.title,
893
938
  severity: ref.severity,
894
939
  status: "stale",
@@ -42,7 +42,8 @@ export async function debugIssue(repoPath, errorMessage, options = {}) {
42
42
  for (const rule of checkResult.rules) {
43
43
  if (rule.status === "deterministic-fail" || rule.status === "attestation-needed") {
44
44
  // It failed. Check if symptoms match.
45
- const ruleDef = rulesMap.get(rule.ruleId);
45
+ const contractRuleId = rule.contractRuleId ?? rule.ruleId;
46
+ const ruleDef = rulesMap.get(contractRuleId);
46
47
  const symptoms = ruleDef?.symptoms || [];
47
48
  if (symptoms.some(s => errorMessage.includes(s))) {
48
49
  correlatedRules.push({
@@ -54,7 +55,8 @@ export async function debugIssue(repoPath, errorMessage, options = {}) {
54
55
  }
55
56
  else if (rule.status === "attested") {
56
57
  // Check for false-positive attestations
57
- const ruleDef = rulesMap.get(rule.ruleId);
58
+ const contractRuleId = rule.contractRuleId ?? rule.ruleId;
59
+ const ruleDef = rulesMap.get(contractRuleId);
58
60
  const symptoms = ruleDef?.symptoms || [];
59
61
  if (symptoms.some(s => errorMessage.includes(s))) {
60
62
  correlatedRules.push({
@@ -1,5 +1,6 @@
1
1
  import { access, readdir, readFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { productFindingIdentity } from "../productExpectations.js";
3
4
  import { objectInput, optionalStringField, stringField, textResult } from "../types.js";
4
5
  import { findCallExpressions, parse, tryParse, pickObjectProperty, resolveLiteralValue, stripComments } from "./ast.js";
5
6
  async function exists(filePath) {
@@ -1910,7 +1911,7 @@ function statusForFindings(findings) {
1910
1911
  return findings.length === 0 ? "no-obvious-issues" : "needs-review";
1911
1912
  }
1912
1913
  function finding(ruleId, severity, message, file, recommendation) {
1913
- return { ruleId, severity, message, file, recommendation };
1914
+ return { ...productFindingIdentity(ruleId), severity, message, file, recommendation };
1914
1915
  }
1915
1916
  async function existingFiles(root, relativeFiles) {
1916
1917
  const files = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amityco/social-plus-vise",
3
- "version": "0.14.8",
3
+ "version": "0.14.9",
4
4
  "description": "Skill-guided deterministic CLI for social.plus SDK integration assistance.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",