@amityco/social-plus-vise 0.14.1 → 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,37 @@ 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
+
27
+ ## 0.14.2 — 2026-06-03
28
+
29
+ **Theme:** SDK facts bridge for social.plus Block Factory.
30
+
31
+ ### Added
32
+ - **Bundled SDK surface snapshot** in `sdk-surface/` for offline/npm use across TypeScript, Android, iOS, and Flutter.
33
+ - **Internal `vise sdk-facts` CLI command and `get_sdk_facts` MCP tool** for Block Factory. The tool is projectless and read-only: it proves SDK symbol/capability/model-symbol facts from the normalized SDK surface without inspecting customer code.
34
+ - **TypeScript Comments/Reactions capability facts** for the first Block Factory validation slice, including React Native aliasing to the TypeScript SDK surface.
35
+
36
+ ---
37
+
7
38
  ## 0.14.1 — 2026-06-03
8
39
 
9
40
  **Theme:** Retract the enumerative DesignBuildBrief from plan output — our own benchmark said so.
package/README.md CHANGED
@@ -43,9 +43,9 @@ See [Usage Flow](#usage-flow) for the full step-by-step diagram.
43
43
 
44
44
  ## What Vise Does: Agentic Workflow Governance
45
45
 
46
- Instead of just providing a CLI or AI skills, Vise implements a technique called **Agentic Workflow Governance**. Think of it as building a software factory directly on top of the customer's project.
46
+ Instead of just providing a CLI or AI skills, Vise implements a technique called **Agentic Workflow Governance**. Think of it as a customer-project integration harness: the governed build loop runs inside the target repo, grounded in the real project, real docs, and real validation signals.
47
47
 
48
- Vise acts as the foreman of this factory, wrapping your local coding agents in compliance guardrails when they integrate social.plus SDKs. It inspects your project, grounds the agent in hosted docs, enforces 300 platform-specific compliance rules, checks the generated UI against the customer's design system, surfaces the full SDK feature surface so nothing is silently dropped, and runs your project's own build/lint/typecheck sensors. **Your source code never leaves your machine.**
48
+ Vise wraps your local coding agents in compliance guardrails when they integrate social.plus SDKs. It inspects your project, grounds the agent in hosted docs, enforces 300 platform-specific compliance rules, checks the generated UI against the customer's design system, surfaces the full SDK feature surface so nothing is silently dropped, and runs your project's own build/lint/typecheck sensors. **Your source code never leaves your machine.**
49
49
 
50
50
  At a glance, Vise sits between the user's prompt and the agent's code changes. The agent still edits the app; Vise turns the request into a grounded plan, records the local contract, and keeps checking until the integration is ready to ship.
51
51
 
@@ -81,6 +81,15 @@ Vise validates on three layers, and the layer is set by the *kind of claim* —
81
81
 
82
82
  Only correctness is gated (it can be made FP-free); conformance and completeness are surfaced, because "all post types" and "matches the brand" are legitimately scope-dependent. See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
83
83
 
84
+ ### Relationship to social.plus Block Factory
85
+
86
+ Vise has two deliberately separate roles:
87
+
88
+ - **Customer integration helper:** runs inside customer projects to inspect, plan, validate, and sensor-check social.plus SDK integrations.
89
+ - **Block Factory SDK facts provider:** planned internal mode for social.plus Block Factory to verify SDK capabilities, symbols, and model schemas before reusable blocks are generated or released.
90
+
91
+ Vise owns SDK truth and customer-project governance. social.plus Block Factory owns block contracts, package adapters, previews, conformance tests, and release readiness. See [docs/SDK_FACTS_FOR_BLOCK_FACTORY.md](docs/SDK_FACTS_FOR_BLOCK_FACTORY.md) for the internal provider-side plan.
92
+
84
93
  ### Design-conformant UI
85
94
 
86
95
  Vise can ingest the customer's aesthetic into a **design contract** and guide generation to match it — from an HTML/CSS prototype (`vise design extract`) or from the host app's own design system across web + Android + Flutter + iOS (`vise design extract --from-project`: CSS vars/Tailwind/token modules, `colors.xml`, Flutter `Color(0x…)`, iOS `.colorset`/Swift). `vise design check` reports token conformance; `vise design preview` writes a visual review; `vise design reference` generates a full visual design-system spec (swatches, type samples, component demos). All advisory.
@@ -149,7 +158,8 @@ A failing feature without Vise is *invisible* until a user hits it: the code com
149
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.)
150
159
  - **Three arms, separate tooling.** The Rules-as-Markdown arm has no Vise checker available — it cannot run `vise check`.
151
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.
152
- - **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.
153
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.
154
164
 
155
165
  ### Which mode should I use?
@@ -162,6 +172,28 @@ A failing feature without Vise is *invisible* until a user hits it: the code com
162
172
 
163
173
  ---
164
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
+
165
197
  ## Supported Platforms
166
198
 
167
199
  | Platform | Coverage | Sensors |
@@ -377,11 +409,12 @@ jobs:
377
409
 
378
410
  | Code | Meaning |
379
411
  |---|---|
380
- | `0` | All rules pass (deterministic or attested) |
412
+ | `0` | All rules pass and all expected capabilities are either present or opted-out |
381
413
  | `1` | One or more rules need attestation |
382
414
  | `2` | One or more rules have deterministic failures |
383
415
  | `3` | One or more blockers fired (missing prerequisite, e.g. `google-services.json`) |
384
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>` |
385
418
 
386
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.
387
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"],
package/dist/server.js CHANGED
@@ -14,6 +14,7 @@ import { planIntegrationTool } from "./tools/integration.js";
14
14
  import { inspectProjectTool, validateSetupTool } from "./tools/project.js";
15
15
  import { resolveRequestTool, suggestPatchTool } from "./tools/resolve.js";
16
16
  import { runSensorsTool } from "./tools/sensors.js";
17
+ import { getSdkFactsTool } from "./tools/sdkFacts.js";
17
18
  import { debugIssueTool, debugIssue } from "./tools/debug.js";
18
19
  import { packageName, packageVersion } from "./version.js";
19
20
  const tools = new Map([
@@ -39,6 +40,7 @@ const tools = new Map([
39
40
  designPreviewTool,
40
41
  designReferenceTool,
41
42
  designInitTokensTool,
43
+ getSdkFactsTool,
42
44
  ].map((tool) => [tool.name, tool]));
43
45
  const bundledSkillName = "social-plus-vise";
44
46
  // Pre-rebrand `install-skill` runs created skill dirs/files under this name. We
@@ -201,6 +203,20 @@ async function handleCli(args) {
201
203
  });
202
204
  return "exit";
203
205
  }
206
+ if (command === "sdk-facts" || command === "sdk_facts") {
207
+ assertOnlyKnownFlags(args, ["platform", "capability", "surface-dir", "format", "include-symbols"], "sdk-facts");
208
+ const format = flagValue(args, "format") ?? "json";
209
+ if (format !== "json") {
210
+ throw new Error("sdk-facts currently supports --format json only.");
211
+ }
212
+ await printToolResult(getSdkFactsTool, {
213
+ platform: requiredFlagValue(args, "platform", "sdk-facts requires --platform."),
214
+ capability: flagValue(args, "capability"),
215
+ surfaceDir: flagValue(args, "surface-dir"),
216
+ includeSymbols: hasFlag(args, "include-symbols"),
217
+ });
218
+ return "exit";
219
+ }
204
220
  if (command === "init") {
205
221
  assertOnlyKnownFlags(args, ["request", "surface", "surface-path"], "init");
206
222
  console.log(JSON.stringify(await initCompliance(positionalRepoPath(args.slice(1)), requiredFlagValue(args, "request", "init requires --request."), flagValue(args, "surface") ?? flagValue(args, "surface-path")), null, 2));
@@ -468,6 +484,16 @@ Resolve a natural-language request into the closest supported Vise outcome.
468
484
 
469
485
  Usage:
470
486
  vise resolve [repoPath] --request "Add a social feed"`;
487
+ }
488
+ if (command === "sdk-facts" || command === "sdk_facts") {
489
+ return `${packageName} sdk-facts
490
+
491
+ Read bundled SDK surface facts for social.plus Block Factory planning. Internal, projectless, and read-only.
492
+
493
+ Usage:
494
+ vise sdk-facts --platform typescript --capability comments --format json
495
+ vise sdk-facts --platform react-native --capability reactions --include-symbols
496
+ vise sdk-facts --platform android --surface-dir ./sdk-surface --format json`;
471
497
  }
472
498
  if (command === "init") {
473
499
  return `${packageName} init
@@ -577,6 +603,7 @@ Usage:
577
603
  vise status [repoPath] Print compliance summary
578
604
  vise validate [repoPath] Validate setup and common risks
579
605
  vise run-sensors [repoPath] Run detected project sensors
606
+ vise sdk-facts --platform ... Internal SDK surface facts for Block Factory
580
607
  vise design extract <prototype> Extract a design contract from an HTML/CSS prototype
581
608
  vise design check [repoPath] Advisory (non-blocking) UI-vs-contract conformance report
582
609
  vise design preview [repoPath] Write an HTML visual review of the contract + conformance
@@ -830,7 +857,7 @@ function ciCheckResult(result) {
830
857
  };
831
858
  }
832
859
  function positionalRepoPath(args) {
833
- const flagsWithValues = new Set(["request", "surface", "surface-path", "platform", "include", "timeout-ms", "query", "path", "limit", "answer", "target", "dest", "destination", "rule", "confidence", "signer", "identity", "evidence-file", "rationale", "repo", "reference"]);
860
+ const flagsWithValues = new Set(["request", "surface", "surface-path", "platform", "capability", "surface-dir", "format", "include", "timeout-ms", "query", "path", "limit", "answer", "target", "dest", "destination", "rule", "confidence", "signer", "identity", "evidence-file", "rationale", "repo", "reference"]);
834
861
  for (let index = 0; index < args.length; index += 1) {
835
862
  const arg = args[index];
836
863
  if (!arg) {
@@ -852,7 +879,7 @@ function positionalRepoPath(args) {
852
879
  }
853
880
  function requiredPositionalText(args, message) {
854
881
  const values = [];
855
- const flagsWithValues = new Set(["request", "surface", "surface-path", "platform", "include", "timeout-ms", "query", "path", "limit", "answer", "target", "dest", "destination", "rule", "confidence", "signer", "identity", "evidence-file", "rationale", "repo", "reference"]);
882
+ const flagsWithValues = new Set(["request", "surface", "surface-path", "platform", "capability", "surface-dir", "format", "include", "timeout-ms", "query", "path", "limit", "answer", "target", "dest", "destination", "rule", "confidence", "signer", "identity", "evidence-file", "rationale", "repo", "reference"]);
856
883
  for (let index = 0; index < args.length; index += 1) {
857
884
  const arg = args[index];
858
885
  if (!arg) {
@@ -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
  }