@amityco/social-plus-vise 1.0.1 → 1.1.0

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.
Files changed (56) hide show
  1. package/CHANGELOG.md +62 -25
  2. package/LICENSE +8 -6
  3. package/README.md +51 -18
  4. package/dist/capabilities.js +19 -62
  5. package/dist/intelligence/grounding.js +0 -23
  6. package/dist/intelligence/placement.js +0 -9
  7. package/dist/outcomes.js +44 -22
  8. package/dist/server.js +75 -38
  9. package/dist/tools/ast.js +3 -209
  10. package/dist/tools/blocks.js +6 -20
  11. package/dist/tools/compliance.js +168 -43
  12. package/dist/tools/creative.js +15 -41
  13. package/dist/tools/debug.js +0 -16
  14. package/dist/tools/design.js +18 -364
  15. package/dist/tools/docs.js +53 -24
  16. package/dist/tools/experienceCompiler.js +7 -10
  17. package/dist/tools/experienceSensors.js +1 -1
  18. package/dist/tools/harness.js +2 -27
  19. package/dist/tools/integration.js +6 -38
  20. package/dist/tools/learning.js +1 -1
  21. package/dist/tools/project.js +763 -546
  22. package/dist/tools/sdkFacts.js +2 -15
  23. package/dist/tools/sdkVersion.js +3 -36
  24. package/dist/tools/sensors.js +0 -6
  25. package/dist/tools/uxHarness.js +12 -9
  26. package/package.json +8 -97
  27. package/rules/chat.yaml +225 -0
  28. package/rules/event.yaml +45 -0
  29. package/rules/feed.yaml +24 -24
  30. package/rules/invitation.yaml +58 -0
  31. package/rules/live-data.yaml +104 -2
  32. package/rules/notification-tray.yaml +106 -0
  33. package/rules/poll.yaml +71 -0
  34. package/rules/sdk-lifecycle.yaml +112 -6
  35. package/rules/search.yaml +131 -0
  36. package/rules/story.yaml +221 -0
  37. package/rules/user-blocking.yaml +71 -0
  38. package/sdk-surface/flutter.json +1 -1
  39. package/sdk-surface/ios.json +1 -1
  40. package/sdk-surface/manifest.json +12 -12
  41. package/sdk-surface/models.flutter.json +96 -96
  42. package/sdk-surface/models.ios.json +1 -1
  43. package/sdk-surface/typescript.json +4 -4
  44. package/skills/social-plus-vise/SKILL.md +25 -5
  45. package/scripts/catalog-coverage-html.mjs +0 -325
  46. package/scripts/catalog-relationships-html.mjs +0 -686
  47. package/scripts/catalog-sheets.mjs +0 -286
  48. package/scripts/dart-model-extractor/bin/extract_models.dart +0 -169
  49. package/scripts/dart-model-extractor/pubspec.lock +0 -149
  50. package/scripts/dart-model-extractor/pubspec.yaml +0 -16
  51. package/scripts/extract-sdk-models.mjs +0 -749
  52. package/scripts/import-sdk-surface.mjs +0 -161
  53. package/scripts/pilot-feedback.mjs +0 -107
  54. package/scripts/workshop-board-html.mjs +0 -1018
  55. package/scripts/workshop-kit.mjs +0 -252
  56. package/skills/vise-harness-engineer/SKILL.md +0 -35
@@ -1,33 +1,24 @@
1
- /**
2
- * Feature-completeness assessment.
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
- * unless the customer/agent explicitly removes it from scope. Missing capabilities
7
- * now produce `completeness-gap` in `vise check`; a recorded scope-omit reason is
8
- * the false-positive escape hatch.
9
- *
10
- * Memory-independence comes from inversion: VISE authors the canonical capability
11
- * set per outcome; the agent must OPT OUT of a capability with a recorded reason
12
- * (`// vise: scope-omit <id> <reason>`), which `check` reads and reports. The
13
- * agent subtracts with justification — it doesn't have to remember the set.
14
- *
15
- * Baseline capability gates must stay narrow. For add-feed, pagination is the
16
- * mandatory capability. For add-comments, the comment composer is mandatory.
17
- * For add-chat, sending plus read/unread state are mandatory. For add-follow,
18
- * follower/following relationship data is mandatory. Replies, mentions,
19
- * edit/delete, typing, media-message types, block lists, and deeper moderation
20
- * scope are scoped separately. Richer composer affordances are selected optional sensors.
21
- */
22
1
  import { readdir, readFile, stat } from "node:fs/promises";
23
2
  import path from "node:path";
24
3
  import { canonicalPlatform, getSdkFacts } from "./tools/sdkFacts.js";
25
- // Canonical, Vise-authored capability catalog — the SDK feature surface
26
- // (grounded in rules/*.yaml + SKILL.md, not guessed). Baseline gating is applied
27
- // by baselineCapabilities(); not every catalog item is mandatory for every outcome.
4
+ export function slimCapabilityItem(i) {
5
+ return { id: i.id, status: i.status, validator: i.validator };
6
+ }
7
+ export function slimCapabilityAvailability(ca) {
8
+ if (!ca)
9
+ return undefined;
10
+ return {
11
+ platform: ca.platform,
12
+ ...(ca.canonicalPlatform !== undefined ? { canonicalPlatform: ca.canonicalPlatform } : {}),
13
+ outcome: ca.outcome,
14
+ source: ca.source,
15
+ note: ca.note,
16
+ available: ca.available.map(slimCapabilityItem),
17
+ unavailable: ca.unavailable.map(slimCapabilityItem),
18
+ unknown: ca.unknown.map(slimCapabilityItem),
19
+ };
20
+ }
28
21
  export const CAPABILITIES = [
29
- // ── add-feed: post dataTypes (a feed parent is usually 'text'; rich content
30
- // rides on child posts — render each type the feed can return) ──────────
31
22
  {
32
23
  id: "post-image",
33
24
  label: "Image posts",
@@ -39,10 +30,6 @@ export const CAPABILITIES = [
39
30
  id: "post-video",
40
31
  label: "Video posts",
41
32
  outcomes: ["add-feed"],
42
- // Native idioms differ from TS: Android matches a sealed-class case
43
- // `AmityPost.Data.VIDEO`, Flutter an enum `AmityDataType.VIDEO`, both expose
44
- // getVideo()/videoData. Without these the catalog false-reports video as missing
45
- // on Kotlin/Dart even when the agent handles it (found on Netflix native builds).
46
33
  symbols: [/\bgetVideo\w*/, /\bgetVideoUrl\b/, /AmityVideo/i, /AmityPost\.Data\.VIDEO\b/, /['"]video['"]/, /Data(?:Type)?\.VIDEO\b/, /\bvideoData\b/],
47
34
  hint: "resolve video posts via getVideo / the VIDEO dataType from the parent or a child post",
48
35
  },
@@ -88,8 +75,6 @@ export const CAPABILITIES = [
88
75
  symbols: [/\bmentionees\b/, /\bmentionedUsers\b/, /metadata.*mention/i, /['"]mention['"]/],
89
76
  hint: "wrap @mention spans from metadata offsets and resolve userId→name; pass mentionees on create to notify",
90
77
  },
91
- // ── add-feed: broader social.plus content surfaces (advisory — opt out if a
92
- // plain post feed; these are distinct social.plus features) ─────────────
93
78
  {
94
79
  id: "story",
95
80
  label: "Stories (ephemeral image/video)",
@@ -111,7 +96,6 @@ export const CAPABILITIES = [
111
96
  symbols: [/AmityRoomRepository/i, /\bcreateRoom\b/, /createRoomAndStartStreaming/i, /getBroadcastData/i, /createLiveStreamPost/i, /\bgoLive\b/i, /startPublish/i],
112
97
  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)",
113
98
  },
114
- // ── add-feed: engagement surfaces ─────────────────────────────────────────
115
99
  {
116
100
  id: "comments",
117
101
  label: "Comment thread (list + composer)",
@@ -161,7 +145,6 @@ export const CAPABILITIES = [
161
145
  symbols: [/\bflagPost\b/, /\bflagComment\b/, /\bflagMessage\b/, /\.report\(\)/, /\bflaggedByMe\b/, /\bisFlagged/i],
162
146
  hint: "show report/flag for non-authors; gate moderator-only actions by role",
163
147
  },
164
- // ── add-comments: depth ───────────────────────────────────────────────────
165
148
  {
166
149
  id: "comment-composer",
167
150
  label: "Comment composer (write)",
@@ -190,7 +173,6 @@ export const CAPABILITIES = [
190
173
  symbols: [/\bflagComment\b/, /\.report\(\)/, /\bflaggedByMe\b/],
191
174
  hint: "show flag/report on comments for non-authors",
192
175
  },
193
- // ── add-chat: depth ───────────────────────────────────────────────────────
194
176
  {
195
177
  id: "send-message",
196
178
  label: "Send message",
@@ -223,7 +205,7 @@ export const CAPABILITIES = [
223
205
  id: "read-state",
224
206
  label: "Read state / unread count",
225
207
  outcomes: ["add-chat"],
226
- symbols: [/\bmarkRead\b/, /\bmarkAsRead\b/, /startMessageReceiptSync/i, /\bunreadCount\b/],
208
+ symbols: [/\bmarkRead\b/, /\bmarkAsRead\b/, /\bunreadCount\b/],
227
209
  hint: "mark channel/messages read so the server's unread count decrements",
228
210
  },
229
211
  {
@@ -240,7 +222,6 @@ export const CAPABILITIES = [
240
222
  symbols: [/getMembers/i, /ChannelMember/i, /\bmembership\b/i, /\bgetChannelMembers\b/i],
241
223
  hint: "list/observe channel members; handle join/leave",
242
224
  },
243
- // ── add-community ─────────────────────────────────────────────────────────
244
225
  {
245
226
  id: "community-create",
246
227
  label: "Create community",
@@ -290,7 +271,6 @@ export const CAPABILITIES = [
290
271
  symbols: [/AmityCommunityCategory/i, /getCategories/i, /categoryIds?/i],
291
272
  hint: "organize communities by category (AmityCommunityCategory)",
292
273
  },
293
- // ── add-moderation: depth ─────────────────────────────────────────────────
294
274
  {
295
275
  id: "report-flow",
296
276
  label: "Report / flag flow",
@@ -319,7 +299,6 @@ export const CAPABILITIES = [
319
299
  symbols: [/isDeleted/i, /\bisFlagged/i, /hasFlaggedComment/i, /blockedContent/i, /\bhidden\b/i],
320
300
  hint: "render a placeholder for deleted/flagged content rather than hiding it silently",
321
301
  },
322
- // ── add-follow (social graph) ─────────────────────────────────────────────
323
302
  {
324
303
  id: "follow-unfollow",
325
304
  label: "Follow / unfollow",
@@ -355,7 +334,6 @@ export const CAPABILITIES = [
355
334
  symbols: [/getBlockedUsers/i, /blockedUsers/i],
356
335
  hint: "surface a managed blocked-users list (getBlockedUsers)",
357
336
  },
358
- // ── add-notifications (in-app tray) ───────────────────────────────────────
359
337
  {
360
338
  id: "notification-tray",
361
339
  label: "Notification tray / inbox",
@@ -378,9 +356,6 @@ export const CAPABILITIES = [
378
356
  hint: "respect Amity's server-side notification settings/preferences (getSettings)",
379
357
  },
380
358
  ];
381
- // Optional capabilities are not baseline compliance. `vise plan` offers them as
382
- // explicit feed-forward choices; `vise init --answer feed_optional_capabilities=...`
383
- // records selected choices; `vise check` then evaluates these source sensors.
384
359
  export const OPTIONAL_CAPABILITIES = [
385
360
  {
386
361
  id: "post-image-upload",
@@ -1183,26 +1158,12 @@ function baselineCapabilities(outcome) {
1183
1158
  const baseline = new Set(baselineIds);
1184
1159
  return caps.filter((capability) => baseline.has(capability.id));
1185
1160
  }
1186
- /** The Vise-authored baseline capability checklist for an outcome. */
1187
1161
  export function capabilityChecklist(outcome) {
1188
1162
  return baselineCapabilities(outcome).map((c) => ({ id: c.id, label: c.label, hint: c.hint }));
1189
1163
  }
1190
- /**
1191
- * The Vise-owned completeness id vocabulary — every id a Block Factory contract
1192
- * may reference in `providesCapabilities`. Vise owns this id space; the factory
1193
- * owns the per-block declaration (see block-factory/docs/vise-boundary.md).
1194
- */
1195
1164
  export function completenessCapabilityIds() {
1196
1165
  return new Set(CAPABILITIES.map((capability) => capability.id));
1197
1166
  }
1198
- /**
1199
- * Overlay installed-block evidence onto a source-scan completeness assessment.
1200
- * A still-missing checklist capability becomes present with evidence
1201
- * `source: "block:<blockId>"` when a locally validated installed block declares
1202
- * it in `providesCapabilities`. Callers are responsible for the local validation
1203
- * (package declared + filesTouched intact); on drift they must simply not pass
1204
- * the capability here, so the normal gap returns.
1205
- */
1206
1167
  export function applyBlockProvidedCompleteness(assessment, provided) {
1207
1168
  if (provided.length === 0 || assessment.missing.length === 0) {
1208
1169
  return assessment;
@@ -1231,7 +1192,6 @@ export function applyBlockProvidedCompleteness(assessment, provided) {
1231
1192
  }
1232
1193
  return { ...assessment, present, missing };
1233
1194
  }
1234
- /** Optional feed-forward choices. These are not baseline completeness requirements. */
1235
1195
  export function optionalCapabilityChecklist(outcome, availableIds) {
1236
1196
  const available = availableIds ? new Set(availableIds) : undefined;
1237
1197
  return OPTIONAL_CAPABILITIES
@@ -1296,7 +1256,6 @@ export function assessSelectedOptionalCapabilities(source, outcome, selectedIds
1296
1256
  note: "Selected optional capabilities are enforced only after the user or host agent opts in through `feed_optional_capabilities`. They are source sensors, not baseline compliance rules.",
1297
1257
  };
1298
1258
  }
1299
- /** Pure assessment over already-read source text. */
1300
1259
  export function assessCompleteness(source, outcome) {
1301
1260
  const caps = baselineCapabilities(outcome);
1302
1261
  const optOuts = new Map();
@@ -1339,14 +1298,13 @@ export function assessCompleteness(source, outcome) {
1339
1298
  }
1340
1299
  return { outcome, present, missing, optedOut, invalidOptOuts: invalid, note: ADVISORY_NOTE };
1341
1300
  }
1342
- // ── Bounded source read (advisory; perf-bounded like the design check scan) ──
1343
1301
  const SCAN_EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".dart", ".kt", ".java", ".swift", ".vue"]);
1344
1302
  const SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build", ".next", "out", "coverage", "vendor", ".dart_tool", "pods", "macos", "windows", "linux"]);
1345
1303
  const MAX_FILES = 1500;
1346
1304
  const MAX_FILE_BYTES = 1_000_000;
1347
1305
  export async function assessProjectCompleteness(root, outcome) {
1348
1306
  if (baselineCapabilities(outcome).length === 0) {
1349
- return null; // outcome has no completeness checklist
1307
+ return null;
1350
1308
  }
1351
1309
  return assessCompleteness(await readProjectSource(root), outcome);
1352
1310
  }
@@ -1386,7 +1344,6 @@ async function readProjectSource(root) {
1386
1344
  }
1387
1345
  }
1388
1346
  catch {
1389
- // skip unreadable
1390
1347
  }
1391
1348
  }
1392
1349
  }
@@ -1,27 +1,9 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
- // Deterministic grounding contract for agent-driven variant selection (experience-factory rebuild,
5
- // Option A). The factory does NOT call a model: the driving coding agent performs the semantic
6
- // understanding (customer request -> variant) and hands the factory a selection. This module is the
7
- // factory's deterministic half — it enforces that the agent's selection is GROUNDED in the catalog
8
- // and surfaces honest review signals. It performs no inference and no IO in its core function, so it
9
- // is fully reproducible (the determinism lives here; the non-determinism lives in the visible driver).
10
- //
11
- // Contract, by evidence from the spike + stress test (benchmarks/experience-calibration/
12
- // semantic-understanding-spike.mjs and -stresstest.mjs):
13
- // - variantId MUST be a catalog id, or the literal "none" (the agent's honest no-fit answer).
14
- // Anything else is a hallucination and is REJECTED.
15
- // - rationale is REQUIRED (explainability is a core principle of the factory).
16
- // - confidence is expected (high|medium|low); low confidence and "none" are not failures — they are
17
- // honest signals that the human/agent should reconsider or that the catalog may need a new variant.
18
4
  export const GROUNDING_SCHEMA_VERSION = "2026-06-07.vise-grounding.v1";
19
5
  export const NO_FIT = "none";
20
6
  const CONFIDENCE_VALUES = ["high", "medium", "low"];
21
- /**
22
- * Validate an agent-provided variant selection against the catalog. Pure and deterministic: given the
23
- * same selection and the same catalog id set it always returns the same result.
24
- */
25
7
  export function groundSelection(selection, catalogVariantIds) {
26
8
  const ids = [...catalogVariantIds];
27
9
  const idSet = new Set(ids);
@@ -31,7 +13,6 @@ export function groundSelection(selection, catalogVariantIds) {
31
13
  const rawConfidence = typeof selection?.confidence === "string" ? selection.confidence.trim().toLowerCase() : "";
32
14
  const isNoFit = rawVariant.toLowerCase() === NO_FIT;
33
15
  const isKnownVariant = idSet.has(rawVariant);
34
- // 1. Grounding: variant must be a catalog id or the explicit no-fit answer.
35
16
  if (!isNoFit && !isKnownVariant) {
36
17
  signals.push({
37
18
  code: "hallucinated-variant",
@@ -48,7 +29,6 @@ export function groundSelection(selection, catalogVariantIds) {
48
29
  message: `The agent reported no catalog variant fits this request. Reconsider the request, or treat this as a candidate for a new catalog variant.`,
49
30
  });
50
31
  }
51
- // 2. Rationale is required for explainability (applies to a real variant and to "none").
52
32
  if (!rationale) {
53
33
  signals.push({
54
34
  code: "missing-rationale",
@@ -56,7 +36,6 @@ export function groundSelection(selection, catalogVariantIds) {
56
36
  message: `A rationale is required: the selection must explain, grounded in the request and catalog, why this variant (or "${NO_FIT}") was chosen.`,
57
37
  });
58
38
  }
59
- // 3. Confidence (advisory but expected). Invalid value is a contract violation; absence/low are signals.
60
39
  let confidence = null;
61
40
  if (!rawConfidence) {
62
41
  signals.push({ code: "missing-confidence", severity: "warn", message: `No confidence was provided; expected one of ${CONFIDENCE_VALUES.join(", ")}.` });
@@ -91,12 +70,10 @@ function summarize(status, variantId, signals) {
91
70
  return `Selection rejected: ${signals.filter((s) => s.severity === "error").map((s) => s.code).join(", ")}.`;
92
71
  return `Selection accepted with review signals: ${signals.map((s) => s.code).join(", ")}.`;
93
72
  }
94
- /** Resolve the catalog directory shipped in the package (packages/intelligence/catalog). */
95
73
  export function catalogDir() {
96
74
  const moduleDir = path.dirname(fileURLToPath(import.meta.url));
97
75
  return path.resolve(moduleDir, "..", "..", "packages", "intelligence", "catalog");
98
76
  }
99
- /** Load the set of valid variant ids from the declarative catalog. */
100
77
  export async function loadCatalogVariantIds(dir = catalogDir()) {
101
78
  const raw = await readFile(path.join(dir, "variants.json"), "utf8");
102
79
  const parsed = JSON.parse(raw);
@@ -1,7 +1,6 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
- // Display order — drives CREATIVE_SURFACE_HINTS and, downstream, surfaceSequence.
5
4
  export const SURFACE_REGISTRY = [
6
5
  { id: "feed", outcome: "add-feed", label: "Feed and content surface" },
7
6
  { id: "comments", outcome: "add-comments", label: "Comments and replies" },
@@ -10,9 +9,6 @@ export const SURFACE_REGISTRY = [
10
9
  { id: "community", outcome: "add-community", label: "Community management and identity" },
11
10
  { id: "notifications", outcome: "add-notifications", label: "Notifications and activation" },
12
11
  ];
13
- // Scan order for reducing an object list to its surfaces/outcomes. Kept distinct from the display
14
- // order above to reproduce the pre-derivation if-chain output byte-for-byte (community was scanned
15
- // before profile). Faithful-reproduction only; could be unified if order is proven immaterial.
16
12
  const SURFACE_SCAN_ORDER = ["feed", "comments", "chat", "community", "profile", "notifications"];
17
13
  const outcomeBySurface = new Map(SURFACE_REGISTRY.map((s) => [s.id, s.outcome]));
18
14
  const VALID_SURFACES = new Set(SURFACE_REGISTRY.map((s) => s.id));
@@ -20,8 +16,6 @@ function catalogRoot() {
20
16
  const moduleDir = path.dirname(fileURLToPath(import.meta.url));
21
17
  return path.resolve(moduleDir, "..", "..", "packages", "intelligence", "catalog");
22
18
  }
23
- // object id -> surface / block, in catalog declaration order. Read synchronously at module load so
24
- // the SOCIAL_PLUS_OBJECTS export and the lookup functions stay synchronous (matches prior contract).
25
19
  const objectSurface = new Map();
26
20
  const objectBlock = new Map();
27
21
  {
@@ -41,8 +35,6 @@ const objectBlock = new Map();
41
35
  }
42
36
  }
43
37
  }
44
- // SDK-backed objects are exactly those placed on a surface; app-layer (knowledge/prediction) and
45
- // in-development objects declare no surface and are excluded.
46
38
  export const SOCIAL_PLUS_OBJECTS = new Set(objectSurface.keys());
47
39
  export function surfaceIdsForObjects(objectIds) {
48
40
  const present = new Set(objectIds);
@@ -60,7 +52,6 @@ export function outcomesForObjects(objectIds) {
60
52
  export function blockIdForObject(objectId) {
61
53
  return objectBlock.get(objectId);
62
54
  }
63
- // Object ids bound to a surface, in catalog declaration order (used to build CREATIVE_SURFACE_HINTS).
64
55
  export function surfaceObjectIds(surfaceId) {
65
56
  const ids = [];
66
57
  for (const [id, surface] of objectSurface) {
package/dist/outcomes.js CHANGED
@@ -17,8 +17,16 @@ export const CLASSIFY_ORDER = [
17
17
  "setup-sdk",
18
18
  ];
19
19
  const PUSH_PATTERNS = [/\b(push(?:\s+notification)?|push(?:\s+notifications)?|firebase|fcm|apns)\b/];
20
+ const STORY_REGEX = /\bstor(?:y|ies)\s+(?:tray|ring|bar|reel|carousel|viewer|highlight|feature|collection)\b|\b(?:add|adding|build|building|create|creating|implement|implementing|show|render|rendering|display|displaying)\s+(?:a\s+|an\s+|the\s+)?(?:active\s+|community\s+)?stor(?:y|ies)\b/;
21
+ const SEARCH_REGEX = /\b(?:search|find)\s+(?:for\s+)?(?:users?|people|communit(?:y|ies)|posts?|channels?|messages?|members?|content)\b|\b(?:user|people|communit(?:y|ies)|post|content|global|message)\s+search\b|\bsearch\s+(?:bar|screen|box|field|page|results?|ui|view|feature)\b|\bsemantic search\b|\b(?:add|adding|build|building|implement|implementing|create|creating)\s+(?:a\s+|an\s+|the\s+)?search\b/;
22
+ const EVENT_REGEX = /\bevent\s+(?:calendar|list|feed|page|screen|details?)\b|\battendee(?:s)?\s+list\b|\bRSVP\b|\b(?:add|adding|build|building|create|creating|implement|implementing|show|display)\s+(?:a\s+|an\s+|the\s+)?events?\b/i;
23
+ const LIVESTREAM_REGEX = /\blive\s?stream(?:ing)?\b|\blive\s+video\b|\blive\s+room\b|\b(?:add|adding|build|building|create|creating|implement|implementing|start|starting|watch|watching|play|playing)\s+(?:a\s+|an\s+|the\s+)?(?:live\s+)?(?:livestream|live\s+stream|live\s+room)\b/i;
20
24
  const LIVE_PATTERNS = [
21
25
  /\b(live object|live objects|live collection|live collections|realtime collection|real-time collection|observe|observer|subscribe|subscription|unsubscribe|live update|live updates)\b/,
26
+ STORY_REGEX,
27
+ SEARCH_REGEX,
28
+ EVENT_REGEX,
29
+ LIVESTREAM_REGEX,
22
30
  ];
23
31
  const FEED_PATTERNS = [
24
32
  /\b(social feature|social features|feed|timeline|post list|news feed|create post|create a post|post creation|compose post)\b/,
@@ -32,25 +40,19 @@ const MODERATION_OUTCOME_PATTERNS = [
32
40
  const CHAT_PATTERNS = [
33
41
  /\b(chat|messaging|dm|direct message|conversation|group chat|channel|inbox)\b/,
34
42
  ];
35
- // Community MANAGEMENT/membership — deliberately NOT bare "community" (which
36
- // co-occurs with feed: "community feed" must route to add-feed, handled by
37
- // classify order placing add-community AFTER add-feed).
38
43
  const COMMUNITY_PATTERNS = [
39
44
  /\b(create|creating|build|building|manage|managing|join|joining|leave|leaving|set ?up)\s+(a\s+|the\s+)?communit/,
40
45
  /\bcommunit\w*\s+(member|membership|role|invit|categor|setting|management|directory|discovery)/,
41
46
  /\bcommunity\s+(creation|management|members?|roles?|invitations?|categories|settings|moderation)\b/,
42
47
  ];
43
- // Social graph (follow/followers). "block user" is intentionally left to
44
- // add-moderation; blocking is still a capability under add-follow.
45
48
  const FOLLOW_PATTERNS = [
46
49
  /\b(follow|unfollow|follower|followers|following)\b/,
47
50
  /\b(social graph|user relationship|follow request|followers? list|following list)\b/,
51
+ /\bblocked\s+users?\b/,
52
+ /\bmanage\s+blocked\b/,
53
+ /\bblock\s*list\b/,
54
+ /\bunblock\b/,
48
55
  ];
49
- // In-app notification tray/inbox/settings. Deliberately NOT bare "notification"
50
- // and NOT "push notification" (which routes to setup-push).
51
- // "notification feed"/"notification inbox" deliberately excluded — "feed" and
52
- // "inbox" are claimed by add-feed / add-chat respectively. Tray/center/in-app/
53
- // settings are non-colliding.
54
56
  const NOTIFICATION_PATTERNS = [
55
57
  /\b(notification tray|notification cent(?:er|re)|in-?app notification)/,
56
58
  /\bnotification (?:setting|preference)/,
@@ -58,26 +60,46 @@ const NOTIFICATION_PATTERNS = [
58
60
  const TROUBLESHOOT_PATTERNS = [/\b(error|broken|crash|not working|fail|timeout|401|403)\b/];
59
61
  const VALIDATE_PATTERNS = [/\b(validate|check|correct|setup right|initiali[sz])\b/];
60
62
  const SETUP_PATTERNS = [/\b(setup|set up|install|integrate|wire|configure|init sdk|sdk setup|session lifecycle)\b|initialise?s?\b/];
61
- // Multi-surface social requests must not collapse to the first matching single
62
- // outcome. A request like "feed + chat + profile" needs an explicit surface
63
- // choice or separate contracts, otherwise chat/profile gaps escape an add-feed
64
- // compliance sidecar.
65
63
  export const BROAD_SOCIAL_REGEX = /\b(nice|social features|social feature|engagement|community experience)\b|\b(?:feed|post|posts|comments?)\b[\s\S]{0,160}\b(?:chat|messaging|profile|followers?|following|social graph)\b|\b(?:chat|messaging|profile|followers?|following|social graph)\b[\s\S]{0,160}\b(?:feed|post|posts|comments?)\b/i;
66
64
  export const DESIGN_REGEX = /\bdesign token|design tokens|theme|same design|design system|brand/i;
67
65
  export function classifyOutcome(request) {
68
66
  const normalized = request.toLowerCase();
69
- // Precedence: a feed/timeline build that also mentions comments is a feed
70
- // build, not a standalone "add comments to existing content" task. add-feed
71
- // carries conditional comment guidance, so a multi-feature feed request must
72
- // route there — otherwise add-comments (which precedes add-feed in the
73
- // classify order) wins on the bare word "comment" and silently drops every
74
- // feed/avatar/poll/notification step. Pure comment requests (no feed signal)
75
- // still fall through to add-comments below.
76
67
  const matchesFeed = outcomeRegistry["add-feed"].patterns.some((p) => p.test(normalized));
77
68
  const matchesComments = outcomeRegistry["add-comments"].patterns.some((p) => p.test(normalized));
78
69
  if (matchesFeed && matchesComments) {
79
70
  return "add-feed";
80
71
  }
72
+ if (STORY_REGEX.test(normalized)) {
73
+ if (matchesFeed) {
74
+ return "add-feed";
75
+ }
76
+ if (matchesComments) {
77
+ return "add-comments";
78
+ }
79
+ if (outcomeRegistry["add-chat"].patterns.some((p) => p.test(normalized))) {
80
+ return "add-chat";
81
+ }
82
+ }
83
+ if (SEARCH_REGEX.test(normalized)) {
84
+ if (matchesFeed) {
85
+ return "add-feed";
86
+ }
87
+ if (matchesComments) {
88
+ return "add-comments";
89
+ }
90
+ if (outcomeRegistry["add-chat"].patterns.some((p) => p.test(normalized))) {
91
+ return "add-chat";
92
+ }
93
+ if (outcomeRegistry["add-community"].patterns.some((p) => p.test(normalized))) {
94
+ return "add-community";
95
+ }
96
+ if (outcomeRegistry["add-follow"].patterns.some((p) => p.test(normalized))) {
97
+ return "add-follow";
98
+ }
99
+ }
100
+ if (LIVESTREAM_REGEX.test(normalized) && matchesFeed) {
101
+ return "add-feed";
102
+ }
81
103
  for (const id of CLASSIFY_ORDER) {
82
104
  const def = outcomeRegistry[id];
83
105
  if (def.patterns.some((pattern) => pattern.test(normalized))) {
@@ -1062,7 +1084,7 @@ const addChat = {
1062
1084
  { step: "Add message send with error handling and auth gate. Observe each message's syncState and surface failed sends (error state) with a retry/delete affordance instead of rendering optimistic success unconditionally; clean up unrecoverable failures (e.g. deleteFailedMessages) on init.", evidence: ["implementationRules.file-specific edits"] },
1063
1085
  { step: "Render the chat inbox from SDK channel queries/live collections. Show unread counts/badges from the SDK on channel rows or the chat tab (`channel.getUnreadCount()`, `getSubChannelsUnreadCount()`, or `AmityCoreClient.observeUserUnread()` / `getTotalChannelUnread()`), and declare channel-list sorting explicitly (typically last activity descending) so recency cannot be reversed by UI defaults.", evidence: ["social-plus-sdk/chat/channels", "intake.chat_inbox_scope", "capabilityAvailability.available"] },
1064
1086
  { step: "Declare message order explicitly with `AmityMessageQuerySortOption.FIRST_CREATED` or `LAST_CREATED` so the message thread cannot invert by relying on defaults.", evidence: ["social-plus-sdk/chat/messages"] },
1065
- { step: "Wire read receipts and typing indicators if required — and mark the channel/messages read on open (channel.markAsRead() / message.markRead(), or startMessageReceiptSync paired with stopMessageReceiptSync on the view lifecycle) so the server-side unread count actually decrements. Reading the unread count without ever marking read leaves the badge stuck.", evidence: ["requiredInputs.read receipt and typing indicator requirements"] },
1087
+ { step: "Wire read receipts and typing indicators if required — and mark the channel/messages read on open (channel.markAsRead() / message.markRead()) so the server-side unread count actually decrements. Reading the unread count without ever marking read leaves the badge stuck. Note: the MarkerSyncEngine receipt-sync API (startMessageReceiptSync/stopMessageReceiptSync) is deprecated and being sun-set — track read state with the unread count + markRead() instead.", evidence: ["requiredInputs.read receipt and typing indicator requirements"] },
1066
1088
  { step: "Add moderation affordance on messages.", evidence: ["requiredInputs.moderation flow for chat messages"] },
1067
1089
  { step: "Run validate_setup and detected command sensors after edits.", evidence: ["validate_setup", "run_sensors"] },
1068
1090
  ];