@amityco/social-plus-vise 0.14.2 → 0.14.3

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,26 @@ 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.3 — 2026-06-04
8
+
9
+ **Theme:** Completeness-gap enforcement and add-feed guidance hardening (image upload + poll creation + pagination build step).
10
+
11
+ ### Added
12
+ - **`completeness-gap` exit code 5:** `vise check` now exits 5 when capabilities from the completeness checklist are neither built nor explicitly opted-out. Precedence: `blocked(3) > deterministic-failures(2) > needs-attestation(1) > completeness-gap(5) > green(0)`. Agents that silently skip capabilities now see exit 5 rather than 0, making the gap visible in the loop's stop condition. The existing `// vise: scope-omit <id> — <reason>` marker remains the correct opt-out path; a recorded reason moves the capability to `opted-out` and does not trigger the exit.
13
+ - **Image upload build step in `add-feed`:** implementation guidance now covers the two-step upload chain — `FileRepository.uploadImage(file)` followed by `PostRepository.createPost({ …, attachments: [{ fileId, fileType: 'image' }] })` — and explicitly flags the common failure mode (calling `uploadImage` but discarding the `fileId` or omitting `attachments`).
14
+ - **Poll creation build step in `add-feed`:** guidance now covers the `PollRepository.createPoll` → `PostRepository.createPost({ data: { pollId } })` creation chain, distinct from the existing poll-rendering (voting) step. Both halves must be present for a working poll composer.
15
+ - **Pagination build step in `add-feed`:** explicit greenfield build step for wiring `onNextPage` / `hasNextPage`, including the ref-capture pattern (`loadMoreRef.current = onNextPage`). Separate from the existing repair/refactor guard, which continues to protect brownfield edits.
16
+ - **`bench:symbols-drift` wired into `npm run validate`:** the SDK symbol drift check now runs as part of the standard gate, surfacing silent SDK renames before they darken rules.
17
+ ### Fixed
18
+ - **`targetType` rule recommendation tightened:** the `validateFeedTargetTypeExplicit` validator recommendation now explicitly calls for binding `targetType` to the intake-resolved feed target (e.g. `communityId` from route params, `userId` from auth context) rather than simply avoiding a literal. Prevents agents from introducing an unbound variable that satisfies the literal check while leaving the same intent gap.
19
+ - **Completeness assessment note language corrected:** `vise plan` completeness note and `assessCompleteness` output no longer say "advisory — never fails the check". They now accurately state that missing items cause `vise check` to exit `completeness-gap` (exit code 5).
20
+
21
+ ### Changed
22
+ - **`SKILL.md` scope-omit enforcement:** the Required Loop section now treats missing completeness items as a stop condition, not a suggestion. Agents must either implement or `// vise: scope-omit <id> — <reason>` each missing capability before reporting done.
23
+ - **`rules/feed.yaml` — `symbol_anchored` annotations:** 4 rules whose validators key on specific SDK symbol or parameter names (`targetType`, `dataType`, `addReaction`/`removeReaction`, `.dataType` field access) are annotated with `symbol_anchored: true` to mark them for review after SDK major releases. Described in `docs/ARCHITECTURE.md` under "SDK-Version Coupling."
24
+
25
+ ---
26
+
7
27
  ## 0.14.2 — 2026-06-03
8
28
 
9
29
  **Theme:** SDK facts bridge for social.plus Block Factory.
package/README.md CHANGED
@@ -158,7 +158,8 @@ A failing feature without Vise is *invisible* until a user hits it: the code com
158
158
  - **Vise-arm passes were deterministic-pass**, not attestation exceptions — agents fixed the code. (The grader applies a narrow, *symmetric* auto-attestation for absence / type-stub findings across **all** arms including the controls; it cannot satisfy the acceptance patterns, so it does not tilt the result toward Vise.)
159
159
  - **Three arms, separate tooling.** The Rules-as-Markdown arm has no Vise checker available — it cannot run `vise check`.
160
160
  - **Built from scratch** (greenfield seed), capable models with prior SDK familiarity. A complementary **bug-fix** benchmark showed **no Vise advantage** — the loop helps on greenfield integration, not local bug hunts.
161
- - **N=1 per cell.** A strong directional signal (the Chat/Moderation/Push mechanism reproduces across both models), **not** a statistically settled finding; repeatability seeds are pending.
161
+ - **N=1 per cell.** A strong directional signal (the Chat/Moderation/Push mechanism reproduces across both models), **not** a statistically settled finding.
162
+ - **Follow-up evidence cuts both ways.** The pre-registered [Capability Matrix](#benchmark-capability-matrix-v1-pre-registered) (n=3, arm-independent grading, different briefs) found **no Row 1 claim** on chat/moderation/push — chat and moderation tied, push +1, below the registered margin — and a new, registered win on feature completeness instead. Read the two together, not Phase 1 alone.
162
163
  - **Full methodology, per-cell analysis, and threats to validity:** [the Commune paper](docs/commune-paper-2026-05-30.md). The [`benchmarks/FINDINGS.html`](benchmarks/FINDINGS.html) and [`benchmarks/RULES_AS_MARKDOWN.html`](benchmarks/RULES_AS_MARKDOWN.html) files are **summary report tables**, not raw transcripts or workspace diffs.
163
164
 
164
165
  ### Which mode should I use?
@@ -171,6 +172,28 @@ A failing feature without Vise is *invisible* until a user hits it: the code com
171
172
 
172
173
  ---
173
174
 
175
+ ## Benchmark: Capability Matrix v1 (pre-registered)
176
+
177
+ > One row won, one tied, one was withheld on a technicality. The registered protocol requires all three to be reported with equal prominence — so here they are.
178
+
179
+ Phase 1 measured one thing (secondary-compliance gaps) under a harness that overlaps Vise's own ruleset. The **Capability Matrix** is the stricter follow-up: a [pre-registered protocol](benchmarks/capability-matrix/PROTOCOL.md) frozen before any cell ran (7 dated amendments, each committed before the data it governs), **firewalled grading instruments** (authors barred from Vise's rules and config — [authorship record](benchmarks/capability-matrix/AUTHORSHIP.md)), **blind structural judges**, and 27 fresh isolated cells: n=3 seeds per cell, Cursor / Composer 2.5, docs-only control vs Vise loop, identical offline docs bundle in both arms. Full numbers, per-behavior provenance, and the rig-integrity record: [RESULTS.md](benchmarks/capability-matrix/RESULTS.md).
180
+
181
+ | Capability claim | Registered outcome | What the data says |
182
+ |---|---|---|
183
+ | **Feature completeness** — open feed request scored against an 11-item firewalled ground truth | ✅ **Claim ships** (won 3/3 seed pairs and the mean) | The Vise loop roughly **halved silently-dropped SDK capabilities: 4.0 vs 7.67 of 11** — by *building more of the SDK surface*, not by surfacing trade-offs. Mechanism not isolated: plan-time capability enumeration, persistence against the check gate, and ~1.8× time-on-task are all live candidates. |
184
+ | **SDK compliance** — chat / moderation / push slices | ➖ **No claim** (7/9 vs 6/9; push +1, below the registered ≥+2 margin) | Brief-explicit behaviors tie, as pre-registered. One level down the defects are *symmetric*: 2/3 control cells shipped ban gates reading a field that doesn't exist on the real SDK (compiles, never fires; 0/3 Vise cells) — while 2/3 Vise cells over-parameterized `targetType` and never bound the brief's value, plausibly steered by Vise's own rule. |
185
+ | **Design conformance** — ambiguous brief, brand-token usage | ⚠️ **Withheld on a technicality** | By-name token usage **+18.1 points** for the contract+check loop (90.3 vs 72.2 mean) ✓ — but the second registered condition (hex literals strictly *lower*) tied at 0–0, so the claim is withheld and reported descriptively. Vise controls are reused from an earlier round (cross-temporal). |
186
+
187
+ **Registered negative results** (the protocol requires these in any publication of the matrix):
188
+
189
+ - **Push stop-condition non-convergence.** Where docs and SDK disagree (push registration — the docs themselves carry a gap warning), "iterate until `vise check` is green" did not converge within the 30-minute cap in *any* Vise cell: agents looped on attestation dialect. Shipped behavior still passed 3/3 — the cost is wall-clock, not correctness. (2/3 control cells also hit the cap doing SDK archaeology.)
190
+ - **Scope-omit affordance unused.** No agent in any cell used the `// vise: scope-omit` surfacing marker or wrote a qualifying scope note. The advisory surfacing mechanism, as shipped in 0.14.1, influenced zero cells.
191
+ - Unchanged from prior rounds: **no Vise advantage on day-2 bug fixes**; **enumerative plan-time design guidance** measured negative twice and was retracted in 0.14.1.
192
+
193
+ All Capability Matrix findings are directional at n=3 under one model and one executor — not settled statistics.
194
+
195
+ ---
196
+
174
197
  ## Supported Platforms
175
198
 
176
199
  | Platform | Coverage | Sensors |
@@ -386,11 +409,12 @@ jobs:
386
409
 
387
410
  | Code | Meaning |
388
411
  |---|---|
389
- | `0` | All rules pass (deterministic or attested) |
412
+ | `0` | All rules pass and all expected capabilities are either present or opted-out |
390
413
  | `1` | One or more rules need attestation |
391
414
  | `2` | One or more rules have deterministic failures |
392
415
  | `3` | One or more blockers fired (missing prerequisite, e.g. `google-services.json`) |
393
416
  | `4` | Contract drift — rules in `sp-vise/compliance.json` no longer match the current ruleset |
417
+ | `5` | One or more expected capabilities are neither implemented nor opted-out — add the capability or place `// vise: scope-omit <id> — <reason>` |
394
418
 
395
419
  `vise check --ci` is read-only. It never updates `sp-vise/`. The JSON output includes a `ci` block with structured details for pipeline logs.
396
420
 
@@ -367,7 +367,7 @@ export const CAPABILITIES = [
367
367
  hint: "respect Amity's server-side notification settings/preferences (getSettings)",
368
368
  },
369
369
  ];
370
- const ADVISORY_NOTE = "Advisory completeness — NEVER fails `vise check`. Build each missing capability, or opt out with a recorded reason: `// vise: scope-omit <id> <reason>`.";
370
+ const ADVISORY_NOTE = "Build each missing capability, or opt out with a recorded reason: `// vise: scope-omit <id> <reason>`. Missing capabilities that are neither built nor opted-out cause `vise check` to exit with status `completeness-gap` (exit code 5).";
371
371
  /** The Vise-authored capability checklist for an outcome (for `vise plan` feed-forward). */
372
372
  export function capabilityChecklist(outcome) {
373
373
  return CAPABILITIES.filter((c) => c.outcomes.includes(outcome)).map((c) => ({ id: c.id, label: c.label, hint: c.hint }));
package/dist/outcomes.js CHANGED
@@ -517,6 +517,13 @@ const addFeed = {
517
517
  "requiredInputs.target screen or route",
518
518
  ],
519
519
  },
520
+ {
521
+ step: "Bind targetType and targetId in createPost (and query) calls to the intake-resolved feed target — e.g. communityId from route params, userId from auth context, or a prop passed from the screen's caller. A variable or prop that is declared but never wired to the actual target is the same intent gap as a hardcoded literal: the validator fires on literals; the correctness gap fires on unbound variables.",
522
+ evidence: [
523
+ "requiredInputs.concrete feed target",
524
+ "requiredInputs.feed scope",
525
+ ],
526
+ },
520
527
  {
521
528
  step: "Fetch the canonical social/feed docs and use platform-appropriate live collection patterns.",
522
529
  evidence: [
@@ -524,6 +531,13 @@ const addFeed = {
524
531
  "social-plus-sdk/core-concepts/realtime-communication/live-objects-collections/overview",
525
532
  ],
526
533
  },
534
+ {
535
+ step: "Wire feed pagination: after the initial page loads, expose a 'Load more' action (button, scroll trigger, or infinite-query) that calls the collection's next-page method (loadMore() / nextPage() / onNextPage()). Check hasMore / hasNextPage before showing the control so it disappears when the list is exhausted. Use only opaque cursor tokens returned by the SDK — never construct numeric page offsets. On React/TypeScript, useAmityElement with an infinite-query hook is the idiomatic pattern; on Flutter, a ScrollController + loadMore callback; on Android, PagingData with a LazyColumn scroll trigger.",
536
+ evidence: [
537
+ "social-plus-sdk/core-concepts/realtime-communication/live-objects-collections/overview",
538
+ "requiredInputs.feed scope",
539
+ ],
540
+ },
527
541
  {
528
542
  step: "When repairing or refactoring a feed query, preserve existing pagination inputs and state (for example pageToken, nextPage, hasMore/loadMore, or infinite-query wiring) unless the customer explicitly changes feed behavior.",
529
543
  evidence: [
@@ -531,6 +545,13 @@ const addFeed = {
531
545
  "implementationRules.file-specific edits",
532
546
  ],
533
547
  },
548
+ {
549
+ step: "If the feed includes a post composer, support image/file attachments: (1) let the user pick a file, (2) upload it with FileRepository.uploadImage(file) — returns a File object with a fileId, (3) pass the fileId in the attachments array of createPost: `PostRepository.createPost({ ..., attachments: [{ fileId, fileType: 'image' }] })`. Display the uploaded image in the new post using the returned fileUrl. A composer that shows an image picker but never calls uploadImage, or calls uploadImage but discards the fileId and omits attachments, produces a broken upload flow — both halves must be present.",
550
+ evidence: [
551
+ "social-plus-sdk/social/content-management/posts/creation/image-post",
552
+ "social-plus-sdk/core-concepts/file-handling/upload",
553
+ ],
554
+ },
534
555
  { step: "Reuse the host app's existing visual system for the social surface.", evidence: designEvidence },
535
556
  { step: "Implement loading, empty, error, and data states.", evidence: ["implementationRules.file-specific edits"] },
536
557
  {
@@ -569,6 +590,13 @@ const addFeed = {
569
590
  step: "If the feed includes poll posts, render each answer by branching on answer.dataType: for 'text' use answer.data directly as a string (PollAnswer.data is the text, not an object); for 'image' render answer.image?.fileUrl. Support voting via PollRepository.votePoll(pollId, [answerId]) and PollRepository.unvotePoll(pollId). When poll.status === 'closed' or poll.isVoted is true, display voteCount percentage bars instead of vote buttons. Show the poll's time status from poll.closedAt (when it ended) or poll.closedIn (when it will end) so users know if voting is open.",
570
591
  evidence: ["social-plus-sdk/social/content-management/posts/creation/poll-post"],
571
592
  },
593
+ {
594
+ step: "If the post composer supports poll creation, implement the two-step creation chain: (1) `PollRepository.createPoll({ question, answers: [{ data: text }], answerType: 'single'|'multiple', closedIn? })` — returns a Poll with a pollId; (2) `PostRepository.createPost({ targetType, targetId, data: { text: '', pollId } })` — links the poll to the feed post. Rendering poll answers (votePoll/unvotePoll) is the read-side; without both creation steps the poll composer silently does nothing. Offer a dedicated poll-builder UI (question input + dynamic answer list) so users can author polls inline.",
595
+ evidence: [
596
+ "social-plus-sdk/social/content-management/posts/creation/poll-post",
597
+ "social-plus-sdk/social/posts",
598
+ ],
599
+ },
572
600
  {
573
601
  step: "In post card headers, show the post's target context when post.targetType === 'community': display 'Author.displayName › Community.displayName' by subscribing to CommunityRepository.getCommunity(post.targetId, cb) for the live community name.",
574
602
  evidence: ["social-plus-sdk/social/communities", "social-plus-sdk/social/posts"],
@@ -375,6 +375,7 @@ export async function checkCompliance(repoPath) {
375
375
  recommendation: finding?.recommendation,
376
376
  rationale: rule.rationale,
377
377
  current_rule: ruleSummary(rule),
378
+ ...(failStatus === "attestation-needed" && rule.enforcement.attestation.allowed && attestHint(rule)),
378
379
  });
379
380
  continue;
380
381
  }
@@ -385,11 +386,12 @@ export async function checkCompliance(repoPath) {
385
386
  const sourceFingerprintStatus = await checkSourceFingerprints(repoRoot, inspection.effectiveRoot, attestation.source_fingerprints ?? []);
386
387
  const staleFingerprints = sourceFingerprintStatus.filter((item) => item.status !== "match");
387
388
  if (staleFingerprints.length > 0) {
389
+ const fingerprintStatus = rule.advisory ? "advisory" : rule.enforcement.attestation.allowed ? "attestation-needed" : "deterministic-fail";
388
390
  results.push({
389
391
  ruleId: rule.id,
390
392
  title: rule.title,
391
393
  severity: rule.severity,
392
- status: rule.advisory ? "advisory" : rule.enforcement.attestation.allowed ? "attestation-needed" : "deterministic-fail",
394
+ status: fingerprintStatus,
393
395
  reason: rule.advisory
394
396
  ? "Advisory: informational only — does not affect compliance status."
395
397
  : "Recorded attestation source fingerprints changed. Re-check the evidence and record a fresh attestation.",
@@ -398,6 +400,7 @@ export async function checkCompliance(repoPath) {
398
400
  rationale: rule.rationale,
399
401
  current_rule: ruleSummary(rule),
400
402
  source_fingerprint_status: sourceFingerprintStatus,
403
+ ...(fingerprintStatus === "attestation-needed" && rule.enforcement.attestation.allowed && attestHint(rule)),
401
404
  });
402
405
  continue;
403
406
  }
@@ -442,7 +445,8 @@ export async function checkCompliance(repoPath) {
442
445
  finding,
443
446
  recommendation: finding?.recommendation,
444
447
  current_rule: ruleSummary(rule),
445
- ...(isInferential && { inferential_prompt: rule.enforcement.inferential?.prompt })
448
+ ...(isInferential && { inferential_prompt: rule.enforcement.inferential?.prompt }),
449
+ ...(baseStatus === "attestation-needed" && rule.enforcement.attestation.allowed && attestHint(rule)),
446
450
  });
447
451
  }
448
452
  const summary = summarize(results);
@@ -450,12 +454,13 @@ export async function checkCompliance(repoPath) {
450
454
  const hasDeterministicFailure = results.some((result) => result.status === "deterministic-fail");
451
455
  // "advisory" status is intentionally excluded — advisory rules surface but never block.
452
456
  const needsAttestation = results.some((result) => result.status === "attestation-needed" || result.status === "stale");
453
- // Precedence: blocked (exit 3) > deterministic-failures (2) > needs-attestation (1) > green (0).
457
+ // Precedence: blocked (3) > deterministic-failures (2) > needs-attestation (1) > completeness-gap (5) > green (0).
454
458
  // Contract drift (exit 4) is handled earlier and short-circuits the loop.
455
- // Advisory feature-completeness surfaced but NEVER part of status/exitCode
456
- // (completeness is a "this is missing" claim, structurally FP-prone; see the
457
- // validation-boundaries principle). Failure to assess is silently ignored.
459
+ // Completeness-gap: capabilities that are neither present nor opted-out require an explicit decision
460
+ // (build it, or place // vise: scope-omit <id> — <reason>). The scope-omit escape hatch keeps this
461
+ // FP-free — any capability can be excluded with a recorded reason. Failure to assess is silently ignored.
458
462
  const completeness = (await assessProjectCompleteness(inspection.effectiveRoot, compliance.outcome).catch(() => null)) ?? undefined;
463
+ const hasCompletenessGap = (completeness?.missing.length ?? 0) > 0;
459
464
  // Blocked wins because the agent cannot proceed without customer input;
460
465
  // surfacing a smaller failure first would distract from the real blocker.
461
466
  return {
@@ -465,8 +470,10 @@ export async function checkCompliance(repoPath) {
465
470
  ? "deterministic-failures"
466
471
  : needsAttestation
467
472
  ? "needs-attestation"
468
- : "green",
469
- exitCode: hasBlocked ? 3 : hasDeterministicFailure ? 2 : needsAttestation ? 1 : 0,
473
+ : hasCompletenessGap
474
+ ? "completeness-gap"
475
+ : "green",
476
+ exitCode: hasBlocked ? 3 : hasDeterministicFailure ? 2 : needsAttestation ? 1 : hasCompletenessGap ? 5 : 0,
470
477
  outcome: compliance.outcome,
471
478
  surfacePath: compliance.surface?.path,
472
479
  summary,
@@ -519,7 +526,20 @@ export async function attestRule(args) {
519
526
  const rules = await rulesById();
520
527
  const rule = rules.get(args.ruleId);
521
528
  if (!rule || !compliance.rules.some((ref) => ref.rule_id === args.ruleId)) {
522
- throw new Error(`Rule is not applicable in this compliance contract: ${args.ruleId}`);
529
+ // Collect up to 8 applicable attestable rule ids from this contract for the error hint. Prefer
530
+ // ids that share the bad id's non-wildcard prefix so the agent can narrow down quickly.
531
+ const attestableIds = compliance.rules
532
+ .map((ref) => rules.get(ref.rule_id))
533
+ .filter((r) => r !== undefined && r.enforcement.attestation.allowed);
534
+ const prefix = args.ruleId.replace(/\.\*$|\*$/, "");
535
+ const prefixed = prefix !== args.ruleId ? attestableIds.filter((r) => r.id.startsWith(prefix) || r.id.includes(prefix)) : [];
536
+ const candidates = prefixed.length > 0 ? prefixed : attestableIds;
537
+ const hintIds = candidates.slice(0, 8).map((r) => r.id);
538
+ const hintSuffix = hintIds.length > 0 ? ` Applicable attestable rules: ${hintIds.join(", ")}.` : " Applicable attestable rules: none.";
539
+ const preamble = args.ruleId.includes("*")
540
+ ? `Wildcards are not supported — attest one rule at a time.`
541
+ : `Rule is not applicable in this compliance contract: ${args.ruleId}.`;
542
+ throw new Error(`${preamble}${hintSuffix}`);
523
543
  }
524
544
  if (!rule.enforcement.attestation.allowed) {
525
545
  throw new Error(`Rule does not allow attestation: ${args.ruleId}`);
@@ -626,6 +646,16 @@ function ruleRef(rule) {
626
646
  function ruleRefForFile(rule) {
627
647
  return { ...ruleRef(rule), title: rule.title };
628
648
  }
649
+ // Benchmark-measured friction: agents looped on attest dialect for ~25 min/cell when docs and SDK
650
+ // disagreed on exact invocation syntax (capability-matrix 2026-06, Row 5). Hand them the exact incantation.
651
+ function attestHint(rule) {
652
+ const minConfidence = rule.enforcement.attestation.host_agent_min_confidence ?? "high";
653
+ const fields = rule.enforcement.attestation.evidence_required ?? [];
654
+ return {
655
+ 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>"`,
656
+ evidence_template: Object.fromEntries(fields.map((f) => [f.field, `<${f.description}>`])),
657
+ };
658
+ }
629
659
  function contractDrift(compliance, rules) {
630
660
  const results = [];
631
661
  const refs = compliance.rules.map((ref) => {
@@ -661,6 +691,7 @@ function contractDrift(compliance, rules) {
661
691
  status: "stale",
662
692
  reason,
663
693
  current_rule: ruleSummary(rule),
694
+ ...(rule.enforcement.attestation.allowed && attestHint(rule)),
664
695
  });
665
696
  }
666
697
  }
@@ -135,7 +135,7 @@ function completenessChecklistFor(outcome) {
135
135
  return undefined;
136
136
  }
137
137
  return {
138
- note: "Build each capability, or opt out with `// vise: scope-omit <id> <reason>`. `vise check` reports present/missing/opted-out (advisorynever fails the check).",
138
+ note: "Build each capability, or opt out with `// vise: scope-omit <id> <reason>`. Missing items that are neither built nor opted-out cause `vise check` to exit `completeness-gap` (exit code 5) treat them as a stop condition.",
139
139
  capabilities: items,
140
140
  };
141
141
  }
@@ -2993,7 +2993,7 @@ function validateFeedTargetTypeExplicit(root, platform, sourceContent) {
2993
2993
  }
2994
2994
  }
2995
2995
  if (flagged) {
2996
- findings.push(finding(ruleId, "warning", `A createPost call appears to hardcode targetType to a literal community or user.`, rel, "Feed targets should be passed dynamically (e.g. from props or intent extras) so the composer component is reusable. If this is intentional, add comment // vise: target-type rationale — <reason>."));
2996
+ findings.push(finding(ruleId, "warning", `A createPost call appears to hardcode targetType to a literal community or user.`, rel, "Feed targets must be bound to the intake-resolved target — the community ID from route params, the user ID from auth context, or a prop passed from the caller. Do not leave targetType as a free variable or default enum value: the intent is to wire it to 'requiredInputs.concrete feed target', not simply to avoid a literal. If this is intentional, add comment // vise: target-type rationale — <reason>."));
2997
2997
  break;
2998
2998
  }
2999
2999
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amityco/social-plus-vise",
3
- "version": "0.14.2",
3
+ "version": "0.14.3",
4
4
  "description": "Skill-guided deterministic CLI for social.plus SDK integration assistance.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",
@@ -67,7 +67,7 @@
67
67
  "test:sdk-facts": "npm run build && node test/run-sdk-facts.mjs",
68
68
  "typecheck": "tsc -p tsconfig.json --noEmit",
69
69
  "test:e2e-package": "npm run build && node test/run-e2e-package.mjs",
70
- "validate": "npm run typecheck && npm test && npm run test:mcp && npm run test:cli && npm run test:docs && npm run test:ast && npm run test:design-extract && npm run test:design-brief && npm run test:capabilities && npm run test:classify && npm run test:compliance && npm run test:rule-coverage && npm run test:readme-coverage && npm run test:happy-path-clean && npm run test:fixture-symmetry && npm run test:nonui-skip && npm run test:sdk-version && npm run test:sdk-surface && npm run test:sdk-facts && npm run test:native-idioms && npm run test:grader-facts && npm run test:ground-truth && npm run test:improvements && npm run test:debug && npm run test:preflight && npm run test:e2e-package && npm run pack:check",
70
+ "validate": "npm run typecheck && npm test && npm run test:mcp && npm run test:cli && npm run test:docs && npm run test:ast && npm run test:design-extract && npm run test:design-brief && npm run test:capabilities && npm run test:classify && npm run test:compliance && npm run test:rule-coverage && npm run test:readme-coverage && npm run test:happy-path-clean && npm run test:fixture-symmetry && npm run test:nonui-skip && npm run test:sdk-version && npm run test:sdk-surface && npm run test:sdk-facts && npm run test:native-idioms && npm run test:grader-facts && npm run test:ground-truth && npm run test:improvements && npm run test:debug && npm run test:preflight && npm run test:e2e-package && npm run bench:symbols-drift && npm run pack:check",
71
71
  "test:ast": "node test/run-ast-helpers.mjs",
72
72
  "test:design-extract": "npm run build && node test/run-design-extract.mjs",
73
73
  "test:design-brief": "npm run build && node test/run-design-brief.mjs",
package/rules/feed.yaml CHANGED
@@ -1110,6 +1110,7 @@
1110
1110
  {
1111
1111
  "id": "typescript.posts.parent-child-rendered",
1112
1112
  "version": 1,
1113
+ "symbol_anchored": true,
1113
1114
  "title": "Post rendering for TypeScript must account for child posts (images/videos)",
1114
1115
  "severity": "warning",
1115
1116
  "rationale": "TypeScript UI components for Posts must recursively render or inspect post.children. If a component only renders the parent post's data.text, it will silently drop attached images and videos.",
@@ -1146,6 +1147,7 @@
1146
1147
  {
1147
1148
  "id": "react-native.posts.parent-child-rendered",
1148
1149
  "version": 1,
1150
+ "symbol_anchored": true,
1149
1151
  "title": "Post rendering for React Native must account for child posts (images/videos)",
1150
1152
  "severity": "warning",
1151
1153
  "rationale": "React Native components for Posts must recursively render or inspect post.children. If a component only renders the parent post's data.text, it will silently drop attached images and videos.",
@@ -1182,6 +1184,7 @@
1182
1184
  {
1183
1185
  "id": "android.posts.parent-child-rendered",
1184
1186
  "version": 1,
1187
+ "symbol_anchored": true,
1185
1188
  "title": "Post rendering for Android must account for child posts (images/videos)",
1186
1189
  "severity": "warning",
1187
1190
  "rationale": "Android Compose/RecyclerView components for Posts must render or inspect the child items (post.children). If a component only renders the parent post's text, it will silently drop attached images and videos.",
@@ -1218,6 +1221,7 @@
1218
1221
  {
1219
1222
  "id": "flutter.posts.parent-child-rendered",
1220
1223
  "version": 1,
1224
+ "symbol_anchored": true,
1221
1225
  "title": "Post rendering for Flutter must account for child posts (images/videos)",
1222
1226
  "severity": "warning",
1223
1227
  "rationale": "Flutter Widgets for Posts must render or inspect the child posts (post.children). If a widget only renders the parent post's data.text, it will silently drop attached images and videos.",
@@ -1254,6 +1258,7 @@
1254
1258
  {
1255
1259
  "id": "ios.posts.parent-child-rendered",
1256
1260
  "version": 1,
1261
+ "symbol_anchored": true,
1257
1262
  "title": "Post rendering for iOS must account for child posts (images/videos)",
1258
1263
  "severity": "warning",
1259
1264
  "rationale": "iOS SwiftUI/UIKit views for Posts must inspect the child posts (post.children). If a view only renders the parent post's text, it will silently drop attached images and videos.",
@@ -1290,6 +1295,7 @@
1290
1295
  {
1291
1296
  "id": "typescript.feed.target-type-explicit",
1292
1297
  "version": 1,
1298
+ "symbol_anchored": true,
1293
1299
  "title": "Explicit targetType for TypeScript createPost calls must not be hardcoded to COMMUNITY",
1294
1300
  "severity": "warning",
1295
1301
  "rationale": "TypeScript feed targets must be passed dynamically (e.g. from props or state), not hardcoded as AmityPostTargetType.COMMUNITY.",
@@ -1326,6 +1332,7 @@
1326
1332
  {
1327
1333
  "id": "react-native.feed.target-type-explicit",
1328
1334
  "version": 1,
1335
+ "symbol_anchored": true,
1329
1336
  "title": "Explicit targetType for React Native createPost calls must not be hardcoded to COMMUNITY",
1330
1337
  "severity": "warning",
1331
1338
  "rationale": "React Native feed targets must be passed dynamically (e.g. from props or state), not hardcoded as AmityPostTargetType.COMMUNITY.",
@@ -1362,6 +1369,7 @@
1362
1369
  {
1363
1370
  "id": "android.feed.target-type-explicit",
1364
1371
  "version": 1,
1372
+ "symbol_anchored": true,
1365
1373
  "title": "Explicit targetType for Android createPost calls must not be hardcoded to COMMUNITY",
1366
1374
  "severity": "warning",
1367
1375
  "rationale": "Android feed targets must be passed dynamically (e.g. from Intent extras or ViewModel state), not hardcoded as AmityPostTargetType.COMMUNITY.",
@@ -1398,6 +1406,7 @@
1398
1406
  {
1399
1407
  "id": "flutter.feed.target-type-explicit",
1400
1408
  "version": 1,
1409
+ "symbol_anchored": true,
1401
1410
  "title": "Explicit targetType for Flutter createPost calls must not be hardcoded to COMMUNITY",
1402
1411
  "severity": "warning",
1403
1412
  "rationale": "Flutter feed targets must be passed dynamically (e.g. from Widget properties), not hardcoded as AmityPostTargetType.COMMUNITY.",
@@ -1434,6 +1443,7 @@
1434
1443
  {
1435
1444
  "id": "ios.feed.target-type-explicit",
1436
1445
  "version": 1,
1446
+ "symbol_anchored": true,
1437
1447
  "title": "Explicit targetType for iOS createPost calls must not be hardcoded to COMMUNITY",
1438
1448
  "severity": "warning",
1439
1449
  "rationale": "iOS feed targets must be passed dynamically (e.g. from View state or initializer), not hardcoded as AmityPostTargetType.community.",
@@ -1470,6 +1480,7 @@
1470
1480
  {
1471
1481
  "id": "typescript.reactions.configured-name-used",
1472
1482
  "version": 2,
1483
+ "symbol_anchored": true,
1473
1484
  "title": "TypeScript reaction name matches console config",
1474
1485
  "severity": "warning",
1475
1486
  "advisory": true,
@@ -1508,6 +1519,7 @@
1508
1519
  {
1509
1520
  "id": "react-native.reactions.configured-name-used",
1510
1521
  "version": 2,
1522
+ "symbol_anchored": true,
1511
1523
  "advisory": true,
1512
1524
  "title": "React Native reaction name matches console config",
1513
1525
  "severity": "warning",
@@ -1546,6 +1558,7 @@
1546
1558
  {
1547
1559
  "id": "android.reactions.configured-name-used",
1548
1560
  "version": 2,
1561
+ "symbol_anchored": true,
1549
1562
  "advisory": true,
1550
1563
  "title": "Android reaction name matches console config",
1551
1564
  "severity": "warning",
@@ -1584,6 +1597,7 @@
1584
1597
  {
1585
1598
  "id": "flutter.reactions.configured-name-used",
1586
1599
  "version": 2,
1600
+ "symbol_anchored": true,
1587
1601
  "advisory": true,
1588
1602
  "title": "Flutter reaction name matches console config",
1589
1603
  "severity": "warning",
@@ -1622,6 +1636,7 @@
1622
1636
  {
1623
1637
  "id": "ios.reactions.configured-name-used",
1624
1638
  "version": 2,
1639
+ "symbol_anchored": true,
1625
1640
  "advisory": true,
1626
1641
  "title": "iOS reaction name matches console config",
1627
1642
  "severity": "warning",
@@ -1660,6 +1675,7 @@
1660
1675
  {
1661
1676
  "id": "typescript.custom-post-type.dataType-declared",
1662
1677
  "version": 1,
1678
+ "symbol_anchored": true,
1663
1679
  "title": "TypeScript custom post must declare dataType",
1664
1680
  "severity": "warning",
1665
1681
  "rationale": "When creating a post with a custom data payload, the 'dataType' tag must be explicitly provided so the SDK can route it correctly. If missing, it defaults to a text post, which stringifies the payload.",
@@ -1696,6 +1712,7 @@
1696
1712
  {
1697
1713
  "id": "react-native.custom-post-type.dataType-declared",
1698
1714
  "version": 1,
1715
+ "symbol_anchored": true,
1699
1716
  "title": "React Native custom post must declare dataType",
1700
1717
  "severity": "warning",
1701
1718
  "rationale": "When creating a post with a custom data payload, the 'dataType' tag must be explicitly provided so the SDK can route it correctly. If missing, it defaults to a text post, which stringifies the payload.",
@@ -1732,6 +1749,7 @@
1732
1749
  {
1733
1750
  "id": "android.custom-post-type.dataType-declared",
1734
1751
  "version": 1,
1752
+ "symbol_anchored": true,
1735
1753
  "title": "Android custom post must declare dataType",
1736
1754
  "severity": "warning",
1737
1755
  "rationale": "When creating a post with a custom data payload, the 'dataType' tag must be explicitly provided so the SDK can route it correctly. If missing, it defaults to a text post, which stringifies the payload.",
@@ -1768,6 +1786,7 @@
1768
1786
  {
1769
1787
  "id": "flutter.custom-post-type.dataType-declared",
1770
1788
  "version": 1,
1789
+ "symbol_anchored": true,
1771
1790
  "title": "Flutter custom post must declare dataType",
1772
1791
  "severity": "warning",
1773
1792
  "rationale": "When creating a post with a custom data payload, the 'dataType' tag must be explicitly provided so the SDK can route it correctly. If missing, it defaults to a text post, which stringifies the payload.",
@@ -1804,6 +1823,7 @@
1804
1823
  {
1805
1824
  "id": "ios.custom-post-type.dataType-declared",
1806
1825
  "version": 1,
1826
+ "symbol_anchored": true,
1807
1827
  "title": "iOS custom post must declare dataType",
1808
1828
  "severity": "warning",
1809
1829
  "rationale": "When creating a post with a custom data payload, the 'dataType' tag must be explicitly provided so the SDK can route it correctly. If missing, it defaults to a text post, which stringifies the payload.",
@@ -37,6 +37,8 @@ vise run-sensors .
37
37
 
38
38
  **`vise check .` is mandatory, not optional. You are not done until it passes or every finding is explicitly attested.** Running `vise plan` and `vise inspect` but skipping `vise check` is the most common failure mode — it means the deterministic catch-net never ran and known-bad patterns ship. When you read a `vise plan`, do not truncate it (`| head` drops the implementation steps); read the full `implementationSteps` array.
39
39
 
40
+ **Completeness is also a stop condition.** When `vise check .` exits with status `completeness-gap` (exit code 5), one or more expected capabilities are neither implemented nor opted-out. For each missing item: either implement it, or place `// vise: scope-omit <id> — <reason>` in the relevant source file and re-run `vise check .` to confirm it exits 0. A missing capability that is neither built nor opted-out is a silent drop — the check will not pass green until every capability is resolved.
41
+
40
42
  Treat Vise runtime smoke sensors as real validation. For TypeScript/React Native projects, `vise run-sensors` may include `TypeScript SDK import smoke`; if it fails, the SDK package does not resolve from the host project runtime and the integration is not done.
41
43
 
42
44
  In CI or pull-request pipelines, use `vise check . --ci`. It is read-only, exits non-zero unless compliance is green, and never writes deterministic-pass records; use `vise sync` only during the implementation loop after a local green check.
@@ -242,13 +244,13 @@ vise plan . --request "<feed or post request>"
242
244
 
243
245
  Require a concrete target from the app: current user feed, selected community, selected channel, or another user-provided domain object. Do not hardcode random target IDs.
244
246
 
245
- **Decide engagement scope explicitly — Vise authors the checklist, you subtract with a reason.** `vise plan` returns a `completenessChecklist` (the canonical capabilities for the outcome, e.g. comments, reactions, pagination, polls, media, moderation) and `vise check` reports each as present / missing / opted-out. This is **advisory it never fails the check**; it exists so completeness doesn't depend on your memory. For each capability: build it, or explicitly opt out with a recorded marker in the code so the omission is reviewable, not accidental:
247
+ **Decide engagement scope explicitly — Vise authors the checklist, you subtract with a reason.** `vise plan` returns a `completenessChecklist` (the canonical capabilities for the outcome, e.g. comments, reactions, pagination, polls, media, moderation) and `vise check` reports each as present / missing / opted-out. The check gate never blocks on completeness (to avoid false positives from legitimately scope-limited integrations), but **missing items that are neither built nor opted-out are silent drops treat them as a stop condition, not a suggestion.** For each capability: build it, or explicitly opt out with a recorded marker in the code so the omission is reviewable, not accidental:
246
248
 
247
249
  ```
248
250
  // vise: scope-omit poll — text + image feed only; polls disabled for this tenant
249
251
  ```
250
252
 
251
- Do not silently drop a capability. `vise check`'s `completeness` section will keep nudging on anything that's neither built nor opted-out.
253
+ Do not report the integration complete while any capability is `missing`. Re-run `vise check .` after placing a scope-omit marker to confirm it moves from `missing` to `opted-out`.
252
254
 
253
255
  **Demo wiring (when the app needs to compile before the real target source exists):** even at the top-level `App` / `MaterialApp` / `_app.tsx` / `AppDelegate` wiring site, do not pass a literal string. Use the host platform's compile-time env channel so the rule sees no string literal at the call site:
254
256