@amityco/social-plus-vise 0.11.0 → 0.12.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,64 @@ 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.12.2 — 2026-06-02
8
+
9
+ **Maintenance / hygiene release.** No functional change from `0.12.1` — identical rules, validators, and CLI. This release exists to scrub an anonymized customer name from the bundled `CHANGELOG`; `0.12.0` and `0.12.1` (which contained it) were unpublished from npm. Use `0.12.2`.
10
+
11
+ ## 0.12.1 — 2026-06-02
12
+
13
+ **Theme:** False-positive-frontier sweep — cross-platform FP hardening driven by the benchmark factory, plus compiler/surface ground-truth for grading. Patch release: all changes are false-positive corrections to existing rules (no new rules, no new features, no breaking changes). TP detection held at **290/290** across all of the below; every fix is locked with a both-direction regression fixture (`test/run-native-idioms.mjs`).
14
+
15
+ ### Fixed
16
+ - **iOS/Android/Flutter push rules** no longer fire on a host app's own generic push (APNs/FCM). `push.unregister.present` and `push.payload-contract-respected` now require a social.plus push **registration nexus** (`registerPushNotification`), not the OS primitive (`didRegisterForRemoteNotifications` / a bare `onMessageReceived`). *(brownfield-iOS cell)*
17
+ - **Ban-state** recognizes a `currentUserIsBanned` prop guard — the ban state fetched in a parent and passed down as a prop (a camelCase boolean form the marker missed). *(brownfield-Flutter cell)*
18
+ - **`custom-post-type.dataType-declared`** no longer flags a plain text post (`createPost({ data: { text } })`) as a custom-data post. *(weak-model-feed cell)*
19
+ - **Android brownfield cluster** — `dependency.sdk` scans feature-module Gradle files (not just `app/`); `setup.lifecycle` no longer flags application-scoped setup in a Hilt `@Module`; `auth.no-anonymous-write` recognizes a write gated on `getCurrentUserId()`. *(brownfield-Android cell)*
20
+ - **React Native** — `client.region` recognizes a positionally-passed region arg (`createClient(KEY, AMITY_REGION)`); `logout-on-user-switch` no longer treats a React `setCurrentUser` useState setter as an SDK user-switch (now keys on the real `setActiveUser`). *(react-native-feed cell)*
21
+
22
+ ### Benchmark & quality infrastructure (`bench/`)
23
+ - **Grading ground truth** (`bench/ground-truth.mjs`) — runs the real toolchain (`flutter analyze`, `tsc --noEmit`) where available; on compiler-less Android, flags `AmityType.member` calls whose member isn't a real surface member. Internal grading aid, **not** a shipped rule (Vise deliberately does not type-check).
24
+ - **Scope-aware finding collection** — `bench/collect.mjs` takes an outcome and excludes out-of-scope rules (the same filter `vise check` uses), so a scoped build isn't graded against the validate-setup everything-sweep.
25
+ - **Marker-boundary audit** (`bench/audit-marker-boundaries.mjs`) — surfaces `\bIDENT\b` markers that would miss a longer camelCase SDK symbol. An occasional eyeball aid, deliberately **not** a CI gate.
26
+
27
+ ### Docs
28
+ - **ARCHITECTURE.md** — new "Sources of Truth" section (facts vs. opinion, across the SDK / docs / Vise repos), with a pointer from the docs repo's `.docs-ops/README`.
29
+
30
+ ---
31
+
32
+ ## 0.12.0 — 2026-06-01
33
+
34
+ **Theme:** Beyond correctness — design conformance, feature completeness, and a self-improving false-positive loop. This release folds in everything since `0.11.0` (which shipped without notes): a full design-contract system, an advisory completeness catalog, three new integration outcomes, SDK-version currency, a large cross-platform false-positive hardening pass (corpus **265 → 300 rules**), and the benchmark factory that now drives FP reduction. All changes are backward-compatible additions or corrections — no breaking CLI changes.
35
+
36
+ ### Added
37
+ - **Design-contract system** — `vise design check` (advisory, non-blocking) verifies a build against a design contract, and `vise design preview` renders a visual conformance report. The contract can be **extracted from an HTML/CSS prototype** (graded extractor) or **derived from the host project's own design system** via `--from-project` (CSS custom properties, Android XML, Flutter named-params, iOS `.colorset` + Swift color extensions). The contract is fed forward into the plan and skill, its digest is recorded in the compliance flow, and undefined CSS-var references are flagged as token-hygiene issues.
38
+ - **Advisory completeness catalog** — feature-completeness nudges with recorded scope opt-outs (the agent can decline a capability with a logged reason; completeness never gates). The capability catalog was deepened to the full social.plus SDK surface, adding stories, events, livestream rooms, pinned posts, post impressions, and message edit/delete.
39
+ - **Three new outcomes** — `add-community` (first-class outcome and the template for future outcomes), `add-follow` (social graph), and `add-notifications` (in-app notification tray).
40
+ - **SDK-version currency** — `vise plan` now surfaces advisory guidance when a project pins an older SDK than the latest published, resolved from the npm registry (TS `@amityco/ts-sdk`, React Native `@amityco/ts-sdk-react-native`; native platforms version-agnostic). Bounded by a tight registry timeout with a graceful offline fallback.
41
+ - **Acme-derived rules** — comment-creation rule + routing fix, mention-rendering guidance, parent/child post-type handling, and several new `add-feed` plan steps, all from real-world gap analysis.
42
+
43
+ ### Changed
44
+ - **Large cross-platform false-positive hardening pass** across iOS (v8), Android, Flutter, and TypeScript/React Native — the markers now recognize the idiomatic native forms agents actually write: ban-state via `isGlobalBanned`/`isGlobalBan` and agent-derived ban booleans; service/repository (non-UI) layers skipped for ban-state/role-gated/flag-count rules; region via idiomatic TS (`API_REGIONS`) and Flutter (`httpEndpoint`/`AmityRegion`) forms; live collections via Android `LiveData`, Jetpack Compose, and Paging3 (`collectAsLazyPagingItems`); Kotlin sealed-class discriminators; one-shot `await queryX()` no longer mistaken for a subscription.
45
+ - **Session-handler guidance corrected** — rules and findings named `AmitySessionHandler`, a type that exists in no SDK. Corrected to the real symbols: `SessionHandler` (Android/iOS) and a `(AccessTokenRenewal renewal)` callback (Flutter). Detection was unaffected; guidance only.
46
+ - **Corpus grew from 265 → 300 rules.**
47
+
48
+ ### Fixed
49
+ - iOS live-collection crash on files larger than 32 KB.
50
+ - Flutter region marker keyed on a nonexistent `socketEndpoint`; replaced with the real `httpEndpoint`/`mqttEndpoint`/`uploadEndpoint` + `AmityRegional{Http,Mqtt}Endpoint` variants.
51
+ - Several moderation false positives (flag is a user-level action; delete/edit are owner-or-mod) and post-datatype scoping (a comment renderer is not a post renderer).
52
+
53
+ ### Benchmark & quality infrastructure (`bench/`)
54
+ - **Benchmark factory** — a synthetic two-track loop. A change counts as an improvement only if **FP↓ AND TP held** (coupled metric). The true-positive detection rate is a static seeded corpus (`bench/tp-dashboard.mjs`, currently 290/290); the false-positive *rate* is measured on fresh blind builds, never a static corpus. An LLM grader classifies findings FP-vs-real and is validated against labeled ground truth before being trusted. A `bench:gate` CI gate enforces the coupled metric on every change.
55
+ - **FP-grader grounded in the authoritative SDK symbol surface** — the grader's symbol-existence judgments are now deterministic lookups against vendored snapshots distilled from the docs repo's machine-extracted surfaces (DocC ABI / Dokka GFM / TypeDoc / dartdoc), not model recall. `bench:symbols-drift` diffs the live surface against the vendored snapshot — the seam for a future SDK-release drift trigger.
56
+
57
+ ### Docs
58
+ - README + ARCHITECTURE/RULES/TESTING refreshed for the design, completeness, and outcomes capabilities.
59
+
60
+ ### Honest scope
61
+ The design, completeness, and SDK-version layers are **advisory** — they inform and attest, they do not gate (only correctness gates; 7 hard gates across 300 rules). The benchmark factory measures and narrows the false-positive frontier for the idioms it has seen; it does not close it in general. Symbol-surface grounding settles whether an API exists — not whether it is used idiomatically, which remains the hand-authored opinion layer that is the product.
62
+
63
+ ---
64
+
7
65
  ## 0.10.0 — 2026-05-29
8
66
 
9
67
  **Theme:** Benchmark-driven sensor expansion. The Commune benchmark (9 new SDK domains: chat, push, social graph, moderation, comments) produced the first measured, defensible advantage for vise+skill over pure MCP: **7/9 working features vs 3/9** with the same agent on the same prompts. This release ships the sensors, rules, and findings.json improvements that produced that result.
package/README.md CHANGED
@@ -45,7 +45,7 @@ See [Usage Flow](#usage-flow) for the full step-by-step diagram.
45
45
 
46
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.
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 262 platform-specific compliance rules, and runs your project's own build/lint/typecheck sensors. **Your source code never leaves your machine.**
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.**
49
49
 
50
50
  | Layer | Purpose |
51
51
  |---|---|
@@ -53,6 +53,26 @@ Vise acts as the foreman of this factory, wrapping your local coding agents in c
53
53
  | **CLI** (`vise`) | Deterministic engine: inspects repos, searches docs, validates setup, runs sensors, manages attestations |
54
54
  | **MCP adapter** | Optional stdio server for MCP-capable tools (Claude Code, Cursor, Codex, VS Code, Copilot) |
55
55
 
56
+ ### What Vise validates: three layers
57
+
58
+ Vise validates on three layers, and the layer is set by the *kind of claim* — which keeps it false-positive-free where it gates:
59
+
60
+ | Layer | Claim | How | Enforcement |
61
+ |---|---|---|---|
62
+ | **SDK compliance** | "this is **wrong**" | 300 deterministic rules (session renewal, live-collection vs one-shot, no secret in logs, parent-child rendering, ban-state gating…) | **Hard gate** — `vise check` blocks until green or attested |
63
+ | **Design conformance** | "this **looks off**" | extract the customer's design system into a contract, then check token usage | **Advisory** — `vise design check`/`preview`; never fails a build |
64
+ | **Feature completeness** | "this is **missing**" | Vise proposes the full SDK feature surface per outcome; the agent opts out of anything out of scope with a recorded reason | **Advisory** — surfaced in `vise plan`/`check`; never fails a build |
65
+
66
+ 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).
67
+
68
+ ### Design-conformant UI
69
+
70
+ 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. All advisory.
71
+
72
+ ### Supported integrations (outcomes)
73
+
74
+ `vise plan`/`init` classify the request into an outcome and tailor the plan, rules, and feature checklist: **feed** · **comments** · **chat** · **moderation** · **community** · **social graph (follow)** · **in-app notifications** · plus setup (SDK, push, live data).
75
+
56
76
  ### Why "Vise"
57
77
 
58
78
  A bench vise holds the workpiece steady so the craftsman's hands are free to shape it. Without one, the workpiece drifts and cuts wander. Vise does the same for AI agents integrating SDKs: it clamps the integration to a known-good position (the real docs, the real project structure, the real compliance rules) so the agent can focus on creative work instead of guessing.
@@ -222,6 +242,18 @@ The flow above is what the skill teaches your AI agent. You — the human — dr
222
242
  | `vise plan-harness [path] --request "..."` | (Pre-planning step) Build the harness around the request |
223
243
  | `vise init [path] --request "..."` | Write the `sp-vise/` compliance contract for this project |
224
244
 
245
+ ### Design contract (UI generation)
246
+
247
+ | Command | Purpose |
248
+ |---|---|
249
+ | `vise design extract <prototypePath> [--repo .] [--no-write]` | Read an HTML/CSS prototype and write a graded `sp-vise/design-contract.json` (declared CSS custom properties become exact tokens; repeated literals become inferred/advisory tokens; single-use literals are dropped) so generated social.plus UI can match the customer's aesthetic |
250
+ | `vise design extract --from-project [path] [--no-write]` | No external prototype? Derive the contract from the host project's **own** design system — CSS custom properties (incl. shadcn `:root` and Tailwind v4 `@theme`), TS/JS token modules, inline tailwind configs, **Android** `colors.xml`/`dimens.xml`, **Flutter** `Color(0x…)`, and **iOS** `.xcassets/*.colorset` + Swift `Color(hex:)`/`Color(red:g:b:)`. Reference values (`var()`/`theme()`/`calc()`) are skipped, so a var-mapped config contributes nothing rather than wrong tokens |
251
+ | `vise design check [path]` | Advisory, **non-blocking** report on how closely the UI code matches the contract (token coverage + on/off-contract color literals). Never fails a build and is **not** a `vise check` gate |
252
+ | `vise design preview [path] [--reference <prototype>]` | Write a self-contained `sp-vise/design-preview.html`: the contract's tokens as visual swatches + the conformance report + the HTML reference embedded for side-by-side review. Vise renders the artifact; a human/VLM judges the visual match. Dependency-free — **not** an automated pixel diff |
253
+ | `vise design reference [path] [--title <name>]` | Write a self-contained `sp-vise/design-reference.html`: human/VLM-readable design-system spec — token swatches, type samples, component demos, and a growth-layer summary. Pairs with `design-contract.json` (machine-readable). Use `--title` to name the design system (e.g. `--title Streamly`). Advisory — **not** an enforcement gate |
254
+
255
+ The extracted contract is **advisory input for generation**, not an enforcement gate: a token-poor prototype yields a weaker — never wrong — contract, and absence of a prototype simply means no contract (the existing `*.design.reuse-detected-tokens` rules still cover reuse of a host project's own design system).
256
+
225
257
  ### Documentation grounding & Troubleshooting
226
258
 
227
259
  | Command | Purpose |
@@ -0,0 +1,447 @@
1
+ /**
2
+ * Feature-completeness assessment — ADVISORY ONLY.
3
+ *
4
+ * Boundary (see the validation-boundaries principle): completeness is a "this is
5
+ * missing" claim — a universal-negative over open-ended correct implementations.
6
+ * It is structurally false-positive-prone and therefore NEVER a hard gate. This
7
+ * module only surfaces nudges; `vise check`'s status/exit code are untouched.
8
+ *
9
+ * Memory-independence comes from inversion: VISE authors the canonical capability
10
+ * set per outcome; the agent must OPT OUT of a capability with a recorded reason
11
+ * (`// vise: scope-omit <id> <reason>`), which `check` reads and reports. The
12
+ * agent subtracts with justification — it doesn't have to remember the set.
13
+ *
14
+ * Gate-eligibility (not used here — these stay advisory): a capability could only
15
+ * ever graduate to a gate if it has a named SDK-call anchor AND a no-fire fixture.
16
+ * Every capability below is anchored on SDK symbols for exactly that reason, but
17
+ * the output is advisory regardless.
18
+ */
19
+ import { readdir, readFile, stat } from "node:fs/promises";
20
+ import path from "node:path";
21
+ // Canonical, Vise-authored capability set — the SDK feature surface (grounded in
22
+ // rules/*.yaml + SKILL.md, not guessed). Anchored on SDK symbol names (consistent
23
+ // enough across TS/Flutter/Android/iOS to match cross-platform). All advisory.
24
+ export const CAPABILITIES = [
25
+ // ── add-feed: post dataTypes (a feed parent is usually 'text'; rich content
26
+ // rides on child posts — render each type the feed can return) ──────────
27
+ {
28
+ id: "post-image",
29
+ label: "Image posts",
30
+ outcomes: ["add-feed"],
31
+ symbols: [/\bgetImageInfo\b/, /AmityImage/i, /['"]image['"]/, /childrenPosts/i, /getChildren/i],
32
+ hint: "resolve image posts via getImageInfo from the parent or a child post (parent is usually dataType 'text')",
33
+ },
34
+ {
35
+ id: "post-video",
36
+ label: "Video posts",
37
+ outcomes: ["add-feed"],
38
+ // Native idioms differ from TS: Android matches a sealed-class case
39
+ // `AmityPost.Data.VIDEO`, Flutter an enum `AmityDataType.VIDEO`, both expose
40
+ // getVideo()/videoData. Without these the catalog false-reports video as missing
41
+ // on Kotlin/Dart even when the agent handles it (found on Netflix native builds).
42
+ symbols: [/\bgetVideo\w*/, /AmityVideo/i, /['"]video['"]/, /Data(?:Type)?\.VIDEO\b/, /\bvideoData\b/],
43
+ hint: "resolve video posts via getVideo / the VIDEO dataType from the parent or a child post",
44
+ },
45
+ {
46
+ id: "post-file",
47
+ label: "File posts",
48
+ outcomes: ["add-feed"],
49
+ symbols: [/\bgetFileInfo\b/, /AmityFile(?:Info|Data)?/i, /['"]file['"]/],
50
+ hint: "render file posts (getFileInfo); upload via AmityFileRepository to get a fileId, never an external URL",
51
+ },
52
+ {
53
+ id: "post-poll",
54
+ label: "Poll posts",
55
+ outcomes: ["add-feed"],
56
+ symbols: [/\bgetPollInfo\b/, /\bvotePoll\b/, /\bunvotePoll\b/, /AmityPoll/i, /PollRepository/i],
57
+ hint: "resolve polls via getPollInfo; read answer.text + answer.image, support votePoll/unvotePoll, show closedAt",
58
+ },
59
+ {
60
+ id: "post-livestream",
61
+ label: "Livestream / room posts",
62
+ outcomes: ["add-feed"],
63
+ symbols: [/livestream/i, /liveStream/, /getLiveStream/i, /AmityStream/i, /['"](?:liveStream|room)['"]/, /getRoomInfo/i],
64
+ hint: "render livestream/room posts by title/status — never the raw roomId",
65
+ },
66
+ {
67
+ id: "post-custom",
68
+ label: "Custom post types",
69
+ outcomes: ["add-feed"],
70
+ symbols: [/dataType\s*[:=]\s*['"]custom/i, /['"]custom\.[\w-]+['"]/, /customDataType/i],
71
+ hint: "render custom-dataType posts (declare dataType on create so the SDK routes them, not a JSON blob)",
72
+ },
73
+ {
74
+ id: "mentions",
75
+ label: "@mention rendering",
76
+ outcomes: ["add-feed", "add-comments", "add-chat"],
77
+ symbols: [/\bmentionees\b/, /\bmentionedUsers\b/, /metadata.*mention/i, /['"]mention['"]/],
78
+ hint: "wrap @mention spans from metadata offsets and resolve userId→name; pass mentionees on create to notify",
79
+ },
80
+ // ── add-feed: broader social.plus content surfaces (advisory — opt out if a
81
+ // plain post feed; these are distinct social.plus features) ─────────────
82
+ {
83
+ id: "story",
84
+ label: "Stories (ephemeral image/video)",
85
+ outcomes: ["add-feed"],
86
+ symbols: [/AmityStoryRepository/i, /createImageStory/i, /createVideoStory/i, /getActiveStories/i, /AmityStoryTarget/i, /\bAmityStory\b/],
87
+ hint: "stories via AmityStoryRepository (createImageStory/createVideoStory) — render the story tray, support reactions/comments, and mark stories seen for impression analytics",
88
+ },
89
+ {
90
+ id: "event",
91
+ label: "Community events",
92
+ outcomes: ["add-feed"],
93
+ symbols: [/AmityEventRepository/i, /\bgetEvents\b/, /\bcreateEvent\b/, /AmityEvent(?:Type|Status|QueryBuilder)?\b/],
94
+ hint: "community events via AmityEventRepository (getEvents/createEvent) — render an events list/detail",
95
+ },
96
+ {
97
+ id: "livestream-broadcast",
98
+ label: "Livestream rooms / broadcasting",
99
+ outcomes: ["add-feed"],
100
+ symbols: [/AmityRoomRepository/i, /\bcreateRoom\b/, /createRoomAndStartStreaming/i, /getBroadcastData/i, /createLiveStreamPost/i, /\bgoLive\b/i, /startPublish/i],
101
+ hint: "live rooms/broadcasting via AmityRoomRepository (createRoom, go-live, live-viewing); post the stream with createLiveStreamPost (distinct from rendering a livestream post in the feed)",
102
+ },
103
+ // ── add-feed: engagement surfaces ─────────────────────────────────────────
104
+ {
105
+ id: "comments",
106
+ label: "Comment thread (list + composer)",
107
+ outcomes: ["add-feed"],
108
+ symbols: [/\bgetComments\b/, /\bcreateComment\b/, /CommentRepository/i],
109
+ hint: "pair getComments (list) with createComment (composer), gated by the user's ban state",
110
+ },
111
+ {
112
+ id: "reactions",
113
+ label: "Reactions",
114
+ outcomes: ["add-feed", "add-comments"],
115
+ symbols: [/\baddReaction\b/, /\bremoveReaction\b/, /ReactionRepository/i, /\bmyReactions\b/],
116
+ hint: "addReaction/removeReaction with a tenant-configured reaction name (not a hardcoded literal)",
117
+ },
118
+ {
119
+ id: "pagination",
120
+ label: "Pagination / load-more",
121
+ outcomes: ["add-feed", "add-comments", "add-chat"],
122
+ symbols: [/\bloadMore\b/, /\bonNextPage\b/, /\bnextPage\b/, /\bhasNextPage\b/, /\bloadNext\b/],
123
+ hint: "drive the collection's loadMore/nextPage (opaque cursors); never numeric page math",
124
+ },
125
+ {
126
+ id: "composer",
127
+ label: "Post composer (create)",
128
+ outcomes: ["add-feed"],
129
+ symbols: [/\bcreatePost\b/, /PostRepository[\s\S]{0,20}create/i],
130
+ hint: "createPost with a dynamic targetType (not a hardcoded literal)",
131
+ },
132
+ {
133
+ id: "pinned-posts",
134
+ label: "Pinned / announcement posts",
135
+ outcomes: ["add-feed"],
136
+ symbols: [/getPinnedPosts/i, /AmityPinnedPost/i, /\bpinnedPost/i, /\bpinPost\b/i, /\bisPinned\b/i, /AmityPinPlacement/i],
137
+ hint: "surface pinned/announcement posts (getPinnedPosts) above the feed",
138
+ },
139
+ {
140
+ id: "post-impressions",
141
+ label: "Post impressions / reach analytics",
142
+ outcomes: ["add-feed"],
143
+ symbols: [/markAsViewed/i, /markPostAsViewed/i, /\bimpression/i, /\breach\b/i, /AmityViewedType/i],
144
+ hint: "mark posts viewed (markAsViewed) so reach/impression analytics are recorded",
145
+ },
146
+ {
147
+ id: "moderation",
148
+ label: "Moderation affordance (report/flag)",
149
+ outcomes: ["add-feed", "add-comments", "add-chat"],
150
+ symbols: [/\bflagPost\b/, /\bflagComment\b/, /\bflagMessage\b/, /\.report\(\)/, /\bflaggedByMe\b/, /\bisFlagged/i],
151
+ hint: "show report/flag for non-authors; gate moderator-only actions by role",
152
+ },
153
+ // ── add-comments: depth ───────────────────────────────────────────────────
154
+ {
155
+ id: "comment-composer",
156
+ label: "Comment composer (write)",
157
+ outcomes: ["add-comments"],
158
+ symbols: [/\bcreateComment\b/],
159
+ hint: "a comment surface needs a composer (createComment), not just a read-only list",
160
+ },
161
+ {
162
+ id: "replies",
163
+ label: "Comment replies (threads)",
164
+ outcomes: ["add-comments"],
165
+ symbols: [/parentId/i, /\bchildrenNumber\b/],
166
+ hint: "render replies via getComments({ parentId }) when childrenNumber > 0",
167
+ },
168
+ {
169
+ id: "comment-edit-delete",
170
+ label: "Comment edit / delete (author)",
171
+ outcomes: ["add-comments"],
172
+ symbols: [/\bupdateComment\b/, /\beditComment\b/, /\bdeleteComment\b/],
173
+ hint: "show edit (updateComment) / delete (deleteComment) for the author of a comment",
174
+ },
175
+ {
176
+ id: "comment-moderation",
177
+ label: "Comment moderation (flag)",
178
+ outcomes: ["add-comments"],
179
+ symbols: [/\bflagComment\b/, /\.report\(\)/, /\bflaggedByMe\b/],
180
+ hint: "show flag/report on comments for non-authors",
181
+ },
182
+ // ── add-chat: depth ───────────────────────────────────────────────────────
183
+ {
184
+ id: "send-message",
185
+ label: "Send message",
186
+ outcomes: ["add-chat"],
187
+ symbols: [/\bcreateMessage\b/, /\bsendMessage\b/, /MessageRepository/i],
188
+ hint: "createMessage; observe each message's syncState for failed sends",
189
+ },
190
+ {
191
+ id: "message-types",
192
+ label: "Message types (image / video / file / custom)",
193
+ outcomes: ["add-chat"],
194
+ symbols: [/createImageMessage/i, /createVideoMessage/i, /createFileMessage/i, /createCustomMessage/i, /messageType/i, /AmityMessageType/i],
195
+ hint: "support non-text message types (image/video/file/custom), not just text",
196
+ },
197
+ {
198
+ id: "message-sync-state",
199
+ label: "Message delivery state",
200
+ outcomes: ["add-chat"],
201
+ symbols: [/\bsyncState\b/, /\bdeleteFailedMessages\b/],
202
+ hint: "surface failed (error) syncState with a retry/delete affordance",
203
+ },
204
+ {
205
+ id: "message-edit-delete",
206
+ label: "Message edit / delete (author)",
207
+ outcomes: ["add-chat"],
208
+ symbols: [/\bupdateMessage\b/, /\beditMessage\b/, /\bdeleteMessage\b/, /\bsoftDeleteMessage\b/],
209
+ hint: "let the author edit (updateMessage) and delete (softDeleteMessage) their messages",
210
+ },
211
+ {
212
+ id: "read-state",
213
+ label: "Read state / unread count",
214
+ outcomes: ["add-chat"],
215
+ symbols: [/\bmarkRead\b/, /\bmarkAsRead\b/, /startMessageReceiptSync/i, /\bunreadCount\b/],
216
+ hint: "mark channel/messages read so the server's unread count decrements",
217
+ },
218
+ {
219
+ id: "typing-indicator",
220
+ label: "Typing indicator",
221
+ outcomes: ["add-chat"],
222
+ symbols: [/startTyping/i, /stopTyping/i, /\bisTyping\b/, /typingPreview/i],
223
+ hint: "start/stop typing and render others' typing state",
224
+ },
225
+ {
226
+ id: "channel-members",
227
+ label: "Channel membership",
228
+ outcomes: ["add-chat"],
229
+ symbols: [/getMembers/i, /ChannelMember/i, /\bmembership\b/i, /\bgetChannelMembers\b/i],
230
+ hint: "list/observe channel members; handle join/leave",
231
+ },
232
+ // ── add-community ─────────────────────────────────────────────────────────
233
+ {
234
+ id: "community-create",
235
+ label: "Create community",
236
+ outcomes: ["add-community"],
237
+ symbols: [/\bcreateCommunity\b/, /AmityCommunityRepository[\s\S]{0,20}create/i, /AmityCommunityCreateOptions/i],
238
+ hint: "create communities via AmityCommunityRepository (createCommunity) with a chosen privacy model",
239
+ },
240
+ {
241
+ id: "community-join-leave",
242
+ label: "Join / leave community",
243
+ outcomes: ["add-community"],
244
+ symbols: [/\bjoinCommunity\b/, /\bleaveCommunity\b/, /AmityCommunityRepository/i],
245
+ hint: "joinCommunity/leaveCommunity for public communities",
246
+ },
247
+ {
248
+ id: "community-join-requests",
249
+ label: "Join requests (private communities)",
250
+ outcomes: ["add-community"],
251
+ symbols: [/\bjoinRequest/i, /AmityJoinRequest/i, /joinRequestStatus/i],
252
+ hint: "private communities use the join-request approval flow (AmityJoinRequest) — handle pending/approve/decline",
253
+ },
254
+ {
255
+ id: "community-members",
256
+ label: "Member list",
257
+ outcomes: ["add-community"],
258
+ symbols: [/\bgetMembers\b/, /AmityCommunityMembership/i, /AmityCommunityMembershipFilter/i],
259
+ hint: "list/observe members via getMembers (a Live Collection), with membership filters",
260
+ },
261
+ {
262
+ id: "community-roles",
263
+ label: "Member roles / moderation",
264
+ outcomes: ["add-community"],
265
+ symbols: [/\baddRole/i, /\bremoveRole/i, /\bbanMember\b/i, /\bremoveMember\b/i, /\.roles?\b/],
266
+ hint: "manage moderator roles / ban-remove members, gated by a role check",
267
+ },
268
+ {
269
+ id: "community-invitation",
270
+ label: "Invitations",
271
+ outcomes: ["add-community"],
272
+ symbols: [/createInvitation/i, /AmityInvitation/i, /AmityMembershipAcceptanceType/i],
273
+ hint: "invite members via createInvitation; handle accept/decline (AmityInvitationStatus)",
274
+ },
275
+ {
276
+ id: "community-categories",
277
+ label: "Categories",
278
+ outcomes: ["add-community"],
279
+ symbols: [/AmityCommunityCategory/i, /getCategories/i, /categoryIds?/i],
280
+ hint: "organize communities by category (AmityCommunityCategory)",
281
+ },
282
+ // ── add-moderation: depth ─────────────────────────────────────────────────
283
+ {
284
+ id: "report-flow",
285
+ label: "Report / flag flow",
286
+ outcomes: ["add-moderation"],
287
+ symbols: [/\bflagPost\b/, /\bflagComment\b/, /\bflagMessage\b/, /\.report\(\)/, /\bunflag\b/],
288
+ hint: "flag/unflag with a confirmation affordance",
289
+ },
290
+ {
291
+ id: "ban-mute",
292
+ label: "Ban / mute members",
293
+ outcomes: ["add-moderation"],
294
+ symbols: [/\bbanUser\b/, /\bunbanUser\b/, /\bmuteChannel\b/, /\bmuteMember\b/, /\bglobalBan\b/i],
295
+ hint: "ban/unban or mute members (gated by moderator role)",
296
+ },
297
+ {
298
+ id: "review-queue",
299
+ label: "Post review queue (ADMIN_REVIEW)",
300
+ outcomes: ["add-moderation"],
301
+ symbols: [/\bapprovePost\b/, /\bdeclinePost\b/, /reviewing/i, /REVIEW_COMMUNITY_POST/i],
302
+ hint: "for a moderator surface with review enabled, query feedType 'reviewing' and wire approve/decline (gated on REVIEW_COMMUNITY_POST)",
303
+ },
304
+ {
305
+ id: "hidden-content",
306
+ label: "Hidden / flagged content rendering",
307
+ outcomes: ["add-moderation"],
308
+ symbols: [/isDeleted/i, /\bisFlagged/i, /hasFlaggedComment/i, /blockedContent/i, /\bhidden\b/i],
309
+ hint: "render a placeholder for deleted/flagged content rather than hiding it silently",
310
+ },
311
+ // ── add-follow (social graph) ─────────────────────────────────────────────
312
+ {
313
+ id: "follow-unfollow",
314
+ label: "Follow / unfollow",
315
+ outcomes: ["add-follow"],
316
+ symbols: [/\bfollow\b/i, /\bunfollow\b/i, /AmityUserRepository/i],
317
+ hint: "follow/unfollow via AmityUserRepository; reflect the live follow state on the button",
318
+ },
319
+ {
320
+ id: "followers-following",
321
+ label: "Follower / following lists",
322
+ outcomes: ["add-follow"],
323
+ symbols: [/getFollowers/i, /getFollowings/i, /getFollowerList/i, /followRelationship/i],
324
+ hint: "list/observe followers and following as a Live Collection (getFollowers/getFollowings)",
325
+ },
326
+ {
327
+ id: "follow-status",
328
+ label: "Follow-request status",
329
+ outcomes: ["add-follow"],
330
+ symbols: [/AmityFollowStatusFilter/i, /followStatus/i, /\bpending\b/i, /acceptFollow/i, /declineFollow/i],
331
+ hint: "handle the follow-request pending/accept/decline flow when following is not automatic",
332
+ },
333
+ {
334
+ id: "block-unblock",
335
+ label: "Block / unblock user",
336
+ outcomes: ["add-follow"],
337
+ symbols: [/\bblockUser\b/i, /\bunblockUser\b/i, /\bunBlockUser\b/i],
338
+ hint: "block/unblock a user (blockUser/unblockUser)",
339
+ },
340
+ {
341
+ id: "blocked-users",
342
+ label: "Blocked-users list",
343
+ outcomes: ["add-follow"],
344
+ symbols: [/getBlockedUsers/i, /blockedUsers/i],
345
+ hint: "surface a managed blocked-users list (getBlockedUsers)",
346
+ },
347
+ // ── add-notifications (in-app tray) ───────────────────────────────────────
348
+ {
349
+ id: "notification-tray",
350
+ label: "Notification tray / inbox",
351
+ outcomes: ["add-notifications"],
352
+ symbols: [/notificationTray/i, /getNotificationTraySeen/i, /NotificationTrayManager/i, /NotificationTray/],
353
+ hint: "observe the notification tray (getNotificationTraySeen / tray Live Object) and render the inbox",
354
+ },
355
+ {
356
+ id: "notification-seen",
357
+ label: "Mark tray seen (unseen badge)",
358
+ outcomes: ["add-notifications"],
359
+ symbols: [/markAsSeen/i, /\bmarkSeen\b/i, /\bisSeen\b/i, /unseenCount/i],
360
+ hint: "mark the tray/items seen (markAsSeen) so the unseen badge clears server-side",
361
+ },
362
+ {
363
+ id: "notification-settings",
364
+ label: "Notification settings / preferences",
365
+ outcomes: ["add-notifications"],
366
+ symbols: [/getSettings/i, /notificationSettings?/i, /notificationPreference/i, /notifications\(\)/],
367
+ hint: "respect Amity's server-side notification settings/preferences (getSettings)",
368
+ },
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>`.";
371
+ /** The Vise-authored capability checklist for an outcome (for `vise plan` feed-forward). */
372
+ export function capabilityChecklist(outcome) {
373
+ return CAPABILITIES.filter((c) => c.outcomes.includes(outcome)).map((c) => ({ id: c.id, label: c.label, hint: c.hint }));
374
+ }
375
+ /** Pure assessment over already-read source text. */
376
+ export function assessCompleteness(source, outcome) {
377
+ const caps = CAPABILITIES.filter((c) => c.outcomes.includes(outcome));
378
+ const optOuts = new Map();
379
+ const omitPattern = /vise:\s*scope-omit\s+([a-z][\w-]*)\s*(?:[—:|-]+\s*(.*))?/gi;
380
+ let match;
381
+ while ((match = omitPattern.exec(source)) !== null) {
382
+ optOuts.set(match[1].toLowerCase(), (match[2] ?? "").trim() || "no reason given");
383
+ }
384
+ const present = [];
385
+ const missing = [];
386
+ const optedOut = [];
387
+ for (const cap of caps) {
388
+ if (optOuts.has(cap.id)) {
389
+ optedOut.push({ id: cap.id, reason: optOuts.get(cap.id) });
390
+ }
391
+ else if (cap.symbols.some((symbol) => symbol.test(source))) {
392
+ present.push({ id: cap.id, label: cap.label });
393
+ }
394
+ else {
395
+ missing.push({ id: cap.id, label: cap.label, hint: cap.hint });
396
+ }
397
+ }
398
+ return { outcome, present, missing, optedOut, note: ADVISORY_NOTE };
399
+ }
400
+ // ── Bounded source read (advisory; perf-bounded like the design check scan) ──
401
+ const SCAN_EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".dart", ".kt", ".java", ".swift", ".vue"]);
402
+ const SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build", ".next", "out", "coverage", "vendor", ".dart_tool", "pods", "macos", "windows", "linux"]);
403
+ const MAX_FILES = 1500;
404
+ const MAX_FILE_BYTES = 1_000_000;
405
+ export async function assessProjectCompleteness(root, outcome) {
406
+ if (CAPABILITIES.every((c) => !c.outcomes.includes(outcome))) {
407
+ return null; // outcome has no completeness checklist
408
+ }
409
+ const resolved = path.resolve(root);
410
+ const parts = [];
411
+ const stack = [resolved];
412
+ let count = 0;
413
+ while (stack.length > 0 && count < MAX_FILES) {
414
+ const dir = stack.pop();
415
+ let entries;
416
+ try {
417
+ entries = await readdir(dir, { withFileTypes: true });
418
+ }
419
+ catch {
420
+ continue;
421
+ }
422
+ for (const entry of entries) {
423
+ if (count >= MAX_FILES) {
424
+ break;
425
+ }
426
+ const full = path.join(dir, entry.name);
427
+ if (entry.isDirectory()) {
428
+ if (!SKIP_DIRS.has(entry.name) && !entry.name.startsWith(".") && entry.name !== "sp-vise") {
429
+ stack.push(full);
430
+ }
431
+ }
432
+ else if (entry.isFile() && SCAN_EXTS.has(path.extname(entry.name).toLowerCase())) {
433
+ count += 1;
434
+ try {
435
+ const info = await stat(full);
436
+ if (info.size <= MAX_FILE_BYTES) {
437
+ parts.push(await readFile(full, "utf8"));
438
+ }
439
+ }
440
+ catch {
441
+ // skip unreadable
442
+ }
443
+ }
444
+ }
445
+ }
446
+ return assessCompleteness(parts.join("\n"), outcome);
447
+ }