@amityco/social-plus-vise 0.14.28 → 1.0.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 (43) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +6 -11
  3. package/dist/tools/ast.js +153 -28
  4. package/dist/tools/docs.js +48 -0
  5. package/dist/tools/sdkFacts.js +45 -1
  6. package/docs-cache/README.md +34 -0
  7. package/docs-cache/social-plus-sdk/core-concepts/foundation/logging.mdx +236 -0
  8. package/docs-cache/social-plus-sdk/core-concepts/realtime-communication/live-objects-collections/android.mdx +262 -0
  9. package/docs-cache/social-plus-sdk/core-concepts/realtime-communication/live-objects-collections/flutter.mdx +195 -0
  10. package/docs-cache/social-plus-sdk/core-concepts/realtime-communication/live-objects-collections/ios.mdx +452 -0
  11. package/docs-cache/social-plus-sdk/core-concepts/realtime-communication/live-objects-collections/overview.mdx +133 -0
  12. package/docs-cache/social-plus-sdk/core-concepts/realtime-communication/live-objects-collections/typescript.mdx +264 -0
  13. package/docs-cache/social-plus-sdk/core-concepts/realtime-communication/push-notifications/register-and-unregister-push-notifications-on-a-device.mdx +191 -0
  14. package/docs-cache/social-plus-sdk/core-concepts/realtime-communication/push-notifications/settings/overview.mdx +43 -0
  15. package/docs-cache/social-plus-sdk/core-concepts/realtime-communication/push-notifications/setup/android-setup.mdx +360 -0
  16. package/docs-cache/social-plus-sdk/core-concepts/realtime-communication/push-notifications/setup/flutter-setup.mdx +457 -0
  17. package/docs-cache/social-plus-sdk/core-concepts/realtime-communication/push-notifications/setup/ios-setup.mdx +423 -0
  18. package/docs-cache/social-plus-sdk/core-concepts/realtime-communication/push-notifications/setup/react-native-setup.mdx +384 -0
  19. package/docs-cache/social-plus-sdk/core-concepts/realtime-communication/realtime-events/overview.mdx +94 -0
  20. package/docs-cache/social-plus-sdk/getting-started/authentication.mdx +808 -0
  21. package/docs-cache/social-plus-sdk/getting-started/platform-setup/mobile/android-quick-start.mdx +304 -0
  22. package/docs-cache/social-plus-sdk/getting-started/platform-setup/mobile/flutter-quick-start.mdx +121 -0
  23. package/docs-cache/social-plus-sdk/getting-started/platform-setup/mobile/ios-quick-start.mdx +225 -0
  24. package/docs-cache/social-plus-sdk/getting-started/platform-setup/web/web-quick-start.mdx +99 -0
  25. package/docs-cache/social-plus-sdk/social/communities-spaces/organization/community-invitation.mdx +459 -0
  26. package/docs-cache/social-plus-sdk/social/communities-spaces/organization/join-leave-community.mdx +449 -0
  27. package/docs-cache/social-plus-sdk/social/communities-spaces/organization/query-community-members.mdx +376 -0
  28. package/docs-cache/social-plus-sdk/social/content-management/posts/creation/text-post.mdx +318 -0
  29. package/docs-cache/social-plus-sdk/social/discovery-engagement/notifications/notification-tray-status.mdx +399 -0
  30. package/docs-cache/social-plus-sdk/social/user-relationship/blocking/block-unblock-user.mdx +166 -0
  31. package/docs-cache/social-plus-sdk/social/user-relationship/following/get-follower-following-list.mdx +339 -0
  32. package/package.json +10 -3
  33. package/scripts/dart-model-extractor/bin/extract_models.dart +169 -0
  34. package/scripts/dart-model-extractor/pubspec.lock +149 -0
  35. package/scripts/dart-model-extractor/pubspec.yaml +16 -0
  36. package/scripts/extract-sdk-models.mjs +353 -12
  37. package/scripts/import-sdk-surface.mjs +10 -19
  38. package/sdk-surface/manifest.json +15 -15
  39. package/sdk-surface/models.android.json +1 -1
  40. package/sdk-surface/models.flutter.json +465 -465
  41. package/sdk-surface/models.ios.json +188 -188
  42. package/sdk-surface/models.typescript.json +1 -1
  43. package/skills/social-plus-vise/SKILL.md +14 -0
package/CHANGELOG.md CHANGED
@@ -4,8 +4,48 @@ 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
+ ## 1.0.0 — 2026-06-11
8
+
9
+ **First stable release.** 1.0 is a stability promise about the contract surface, not a new feature: `vise check` exit codes (0–7) and their precedence, the `sp-vise/` sidecar formats (with backward-read + attestation grandfathering now gated by `test:sidecar-compat`), the CLI command and MCP tool surface, the skill's required loop, and the `packages/schemas` wire formats are now under semver — breaking changes require a major bump. See [`docs/V1_READINESS.md`](docs/V1_READINESS.md) for the decision package and evidence basis (5-platform deterministic E2E journeys with all exit codes asserted on every commit; real-agent semantic matrix; three brownfield pilots green with zero Vise findings). This release consolidates everything developed across the 0.14.x-dev line after the published 0.14.28 (block-aware completeness): the items below plus the platform-hardening and model-facts work noted under the 0.14.28 entry, which reached npm here.
10
+
11
+ ### Changed
12
+ - **Skill: prefer installable blocks over hand-building.** When a Block Factory registry is available, agents are taught to check it first and use the governed `vise blocks` install path, hand-building only the remaining gaps — backed by the 2026-06-10 semantic-matrix measurement (installed path ~9× faster to green than hand-building the same outcome). No registry means hand-build as normal; block existence stays the registry's truth.
13
+
14
+ ### Added
15
+ - **False-negative discovery lane — the third compass (`docs/FN_DISCOVERY.md`).** Vise already had a TP-detection compass (`bench/tp-dashboard.mjs`, seeded from the rules so circular) and an FP compass (happy-path-clean + clone cells + pilots) but NO systematic false-NEGATIVE discovery — violations the rule corpus misses only surfaced by accident. This lane closes that, mirroring the semantic lane's two-half split:
16
+ - **Measured lane (periodic, never CI)** — `benchmarks/fn-discovery/run-fn-discovery.mjs`: for a chosen rule, hand a real coding agent the rule's INTENT (title + rationale from `rules/*.yaml`, NOT its detector) and ask for SDK-using samples that violate that intent in novel ways; run `vise validate --json` over each; grade CAUGHT vs MISSED; write append-only dated evidence (samples + validate output + `FINDINGS.md`) under `benchmarks/fn-discovery/runs/<date>-<rule>-<generator>/`. Scoped first to TypeScript × secrets/ban-state/feed-target/session-lifecycle. Generator policy mirrors the semantic lane (default `agy`; `agent`; `claude` pinned to Sonnet). Built and proven with a **deterministic STUB generator** (default) so the grading core is testable without spawning agents or spending tokens; the real-agent path is wired but invoked only with `--generator`, and the live discovery round runs later with explicit human authorization.
17
+ - **Deterministic gate (CI)** — `test/run-fn-regression.mjs` (`test:fn-regression`): a frozen corpus under `test/fixtures/fn-regression/` of previously-discovered false negatives. `fires` entries assert a rule now fires on a committed sample of a class it once missed (so a fixed blind spot can't silently return); seed entries are realistic catches labeled scaffold so the gate is real and grows; quarantined entries are REAL discovered misses asserted to stay missed until a fix lands, then the gate flips loud and the entry is promoted. The gate never patches a rule — fixes are harness-engineering work with their own review.
18
+ - **Self-test** — `test/run-fn-discovery-selftest.mjs` (`test:fn-discovery-selftest`): runs the stub samples through the real validator + real grader and asserts every CAUGHT/MISSED/CLEAN-OK verdict against the stub's oracle, locking down the lane's plumbing. Both new gates run in `validate` right after `test:harness-e2e`.
19
+ - **Real false negatives discovered while building (TypeScript):** (1) `secret.inline-api-key` missed a hardcoded key re-wrapped through a template string before `createClient` (`resolveLiteralValue` resolved direct literals only); (2) `feed.target.literal` missed a feed-target literal re-bound through a second const before `createPost` (the AST resolver followed one identifier hop); (3) `feed.target.literal` missed a hardcoded feed target when the `createPost` payload carried an `as any` cast (`pickObjectProperty` required a bare object node). Controls proved the rules fire at the single-hop / un-cast forms. **All three were then fixed in this release** (see Fixed, below); their corpus entries are promoted from quarantine to `fires`, so each blind spot can never silently reopen.
20
+ - **iOS and Flutter model facts upgraded to `names-and-types`** (previously names-only): `scripts/extract-sdk-models.mjs` now grounds both platforms in typed sources instead of the type-less docs-ops surface exports, completing the per-platform grounding table (typescript/android/ios/flutter all names-and-types). Wire format unchanged (`2026-06-10.sdk-model-facts.v1` — names-and-types was already a legal grounding); the `extraction.sourceKind` enum gains `sdk-swiftmodule-abi` and `sdk-source-dart` in `packages/schemas`.
21
+ - **iOS** — parsed from the prebuilt AmitySDK xcframework's Swift module ABI JSON, canonically the **`ios-arm64` device slice** (the architecture customers ship and the only single-arch slice; the simulator slices were diffed member-for-member identical for the target models). Fields carry the compiler-printed Swift type (`Swift.String?`, `[AmitySDK.AmityCommentAttachment]`) and Optional-wrapper nullability; `declaredIn` stays `null` because a prebuilt binary has no source locations. Discovery follows the import script's pattern: `--ios-abi-json` / `SP_IOS_ABI_JSON`, then the docs-repo-relative `.docs-ops/integration-tests/ios/vendor/AmitySDK.xcframework/...` location beside the surface dir.
22
+ - **Flutter** — parsed from the local Flutter SDK sources by the new `scripts/dart-model-extractor` helper (package:analyzer `parseString`, **syntax-only**: verbatim declared types and literal `?` nullability, no resolution or inference; analyzer version pinned by the committed `pubspec.lock` and recorded in the extraction provenance). Public instance fields and getters only; untyped members are skipped rather than invented. Real file:line anchors into the SDK checkout. Discovery: `--flutter-sdk-root` / `SP_FLUTTER_SDK_ROOT`, then the `Amity-Social-Cloud-SDK-Flutter-Internal` checkout beside `social-plus-docs`.
23
+ - **Degradation, recorded:** when the `.abi.json`, the Flutter checkout, or the `dart` toolchain is unreachable at import time, the platform falls back to the previous names-only surface-export extraction and the manifest records a `degradedReason` — grounding never exceeds what the reachable source proves, and the snapshot gates refuse a degraded re-import of the committed names-and-types artifacts. npm tarball 495.2 kB → 503.9 kB (regenerated typed snapshots + the Dart helper).
24
+
25
+ ### Verified (typed iOS/Flutter model facts)
26
+ - `test:sdk-surface` locks the new grounding with real extracted fields: iOS `AmityComment.commentId` as required `Swift.String` and `parentId` as optional `Swift.String?` from the arm64 abi.json (plus all-fields-typed and no-source-locations invariants), Flutter `AmityComment.commentId` as nullable `String?` property anchored to `lib/src/domain/model/amity_comment.dart` and `AmityUser.isFlaggedByMe` as a non-nullable `bool` getter.
27
+ - `test:sdk-facts` serves the typed schemas end to end (`AmityPost.postId` → `Swift.String` on iOS; source-anchored `AmityComment` on Flutter) with the new extractor ids recorded.
28
+ - The schemas self-test now expects names-and-types on every platform and proves the extended `sourceKind` enum bites (doctored unknown sourceKind fails validation).
29
+ - Block Factory seam unchanged by design: contracts are still TypeScript-provenance only, so `validate:sdk-facts` / `validate:adapters` pass untouched; the typed iOS/Flutter facts are ready for future native-platform `sdkProvenance`.
30
+
31
+ ### Verified
32
+ - **Harness E2E platform matrix completed (`test:harness-e2e`):** the deterministic lane-1 journey now runs for **flutter** and **ios** alongside typescript and android — each with the complete journey, no partial credit (HARNESS_E2E.md). Per-platform discoveries baked into the gate: flutter/ios block on `feed_scope`/`feed_target`/`target_screen_or_route` only; `flutter-happy-path` *is* the add-feed gap state (zero-rule clean, no pagination affordance), so its known-good implementation is a structural Dart pagination helper (naming `AmityCollection` would trip the flutter `feed.ui-states-present` sensor); the ios pre-implementation state removes `AppTheme.swift` together with `FeedViewController.swift` because the feed view is the theme's only consumer (`ios.design.reuse-detected-tokens` would otherwise mask the exit-5 gap); the exit-2 injection reuses the rule-coverage-proven `flutter-rule-missing-guards` / `ios-rule-missing-guards` files, which pair the attestation-disallowed `<platform>.secret.inline-api-key` driver with attestation-level findings in one file.
33
+ - **Exit-4 (`contract-drift`) stage:** every platform journey now tampers the recorded `rule_digest` of its feed-target rule in `sp-vise/compliance.json` and asserts `vise check --ci` exits **4** with status `contract-drift`, naming the drifted rule (`stale`, "Rule digest changed since vise init"); restoring the contract bytes restores green. Lane 1 now asserts all documented exit codes 0–7.
34
+ - **Environment-aware iOS sensors stage:** the guarded xcodebuild sensor is asserted in *both* environments on every machine via a controlled `PATH` (stub toolchain → integrity probe + guarded build detected; empty `PATH` → the single skipped-with-reason precondition), so `test:harness-e2e` passes identically on darwin-with-Xcode and ubuntu CI without it.
35
+
36
+ ### Fixed
37
+ - **Three confirmed TypeScript false negatives closed (resolver/extractor precision; behavior-parity — no rule id/severity/semantics change).** All three were surfaced by the FN-discovery lane above and verified with single-hop/un-cast controls: (1) `secret.inline-api-key` now resolves a pure-passthrough template (`` `${X}` `` ≡ `X`) — a committed production key re-wrapped through a template no longer ships undetected; (2) `feed.target.literal` now does bounded multi-hop identifier resolution (depth cap + cycle guard) so a const aliased through another const still resolves — and the shared resolver means **Kotlin/Swift re-bind chains benefit too**; (3) a new `unwrapExpression` peels `as`/`satisfies`/parenthesized/non-null wrappers before object-property extraction, so a hardcoded target inside `createPost({…} as any)` is caught (high-frequency, since agents emit `as any` constantly). Each broadening is guarded against false positives by a dedicated dynamic-clean canary (member-access / parameter / `process.env`-sourced values must stay clean), and `happy-path-clean` (the 5-platform FP canary) stays at zero findings.
38
+
39
+ ### Added (sidecar compatibility + hermetic docs)
40
+ - **`test:sidecar-compat` — the backward-read gate (the semver promise 1.0 makes).** A frozen corpus of authentic `sp-vise/` sidecars generated by the real published `0.14.26` / `0.14.27` / `0.14.28` (`npx … vise init/sync/attest`, provenance recorded per version — not hand-authored) is replayed by the current build, asserting tolerant reads (no crash/parse-failure, coherent status, older `vise_version`/`foundry_version` tolerated, no unknown old-contract rule) and exercising attestation grandfathering live for the first time against a real v1-era host-agent attestation (`compatible_with` honored; `deprecated_versions` honored and flagged for re-attestation; empty `compatible_with` softens to `attestation-needed`, never a hard fail). Boundary: this covers backward reads (old sidecar, new build — the direction 1.0 promises), not forward compatibility.
41
+ - **Hermetic docs CI mode (`VISE_DOCS_OFFLINE=1`).** Doc lookups can serve a committed `docs-cache/` corpus instead of fetching `learn.social.plus`; CI runs hermetic by default, killing the network-flake class. `test:docs-offline` proves zero network dependency (sinkhole host + 1 ms timeout, with an empty-corpus negative control that fails loudly rather than silently hitting the network); the live-fetch path is preserved off the critical path as `test:docs-live`.
42
+
7
43
  ## 0.14.28 — 2026-06-10
8
44
 
45
+ > **Note (reconciled at 1.0.0):** the published npm `0.14.28` tarball shipped **block-aware completeness** (and the schemas-seam refactor). The platform-hardening (Swift tree-sitter AST, the guarded iOS build sensor) and field-level model facts described below were committed after that publish and first reached npm in **1.0.0** — they are documented here because that is where the work was authored; the version they shipped in is 1.0.0.
46
+
47
+
48
+
9
49
  **Theme:** block-aware completeness — install a block, end green. Plus platform-leg hardening: Swift gets the tree-sitter treatment and iOS gets a guarded build sensor.
10
50
 
11
51
  ### Added
package/README.md CHANGED
@@ -89,13 +89,7 @@ The current milestone is the **Learning Engine sensor bridge with score calibrat
89
89
 
90
90
  ### Relationship to social.plus Block Factory
91
91
 
92
- Vise has two deliberately separate roles:
93
-
94
- - **Customer integration helper:** runs inside customer projects to inspect, plan, validate, and sensor-check social.plus SDK integrations.
95
- - **Block Factory SDK facts provider:** internal mode for social.plus Block Factory to verify SDK capabilities, symbols, and model schemas before reusable blocks are generated or released.
96
- - **Block installer governance:** customer-project workflow that consumes a Block Factory registry, plans safe package/source changes, writes `sp-vise/blocks.json`, and validates installed block state.
97
-
98
- Vise owns SDK truth and customer-project governance. social.plus Block Factory owns block contracts, package adapters, registry metadata, 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
+ Vise keeps two deliberately separate personas — the **customer-integration helper** (runs inside customer projects, may write `sp-vise/*`, including the `vise blocks` installer workflow) and the **Block Factory SDK-facts provider** (projectless, read-only, exports SDK symbol/capability/model facts). Vise owns SDK truth and customer-project governance; Block Factory owns the blocks. The canonical ownership table and the seam contracts live in [`../docs/BOUNDARY.md`](../docs/BOUNDARY.md); the provider-side design is [docs/SDK_FACTS_FOR_BLOCK_FACTORY.md](docs/SDK_FACTS_FOR_BLOCK_FACTORY.md).
99
93
 
100
94
  ### Block Factory user experience
101
95
 
@@ -167,7 +161,7 @@ Aggregate: **98/99 expected feed capabilities** and **27/27 selected optional ca
167
161
 
168
162
  ### Current Release Validation
169
163
 
170
- Version 0.14.26 carries current release proof around the full feed-forward, product-expectation, creative pre-planning, and validation flow:
164
+ Version 1.0.0 carries current release proof around the full feed-forward, product-expectation, creative pre-planning, block-aware completeness, and validation flow:
171
165
 
172
166
  | Surface | What was validated |
173
167
  |---|---|
@@ -180,7 +174,8 @@ Version 0.14.26 carries current release proof around the full feed-forward, prod
180
174
  | **Experience Score calibration guard** | Experience Report, Experience Sensors, and Learning Engine snapshots keep `score: null`, while `learning-summary.json` keeps `recommendationOptimization.status: "not-active"`. The shadow benchmark has 70 measured cells; `shadow-policy-v2-draft` independently ranked expected variants first in 21/21 registered ranking/cross-platform cells, its human gate package has named product FP/FN and privacy sign-off recorded, and runtime exposure is limited to the opt-in local `vise creative --ranking-preview` artifact. |
181
175
  | **Android workplan dogfood** | A brownfield Android music-player app refreshed under `0.14.24` reached `vise check` green with **43/43 deterministic passes** on the focused feed surface and recorded a green-check workplan snapshot. This is dogfood evidence, not a controlled multi-agent benchmark. |
182
176
  | **Shared product expectations** | Public IDs such as `feed.target-resolved`, `feed.post-type-scope-explicit`, `comments.creation-affordance`, `chat.channel-list-order-explicit`, `community.avatar-from-sdk`, `moderation.role-gated-action`, `follow.relationship-live`, `profile.identity-from-sdk`, `profile.social-counts`, and `notifications.tray-live` stay platform-agnostic while check results retain concrete `contractRuleId` and `validator.sensorId` evidence when deterministic sensors exist. |
183
- | **Rule detection** | TP-track dashboard detects **321/321 seeded rule gaps (100.0%)** in the static corpus. |
177
+ | **Rule detection** | TP-track dashboard detects **100% of seeded rule gaps** in the static corpus (run `npm run bench:tp` for the current seeded count; `npm run validate`'s rule-coverage gate prints the full corpus size). |
178
+ | **Block-aware completeness** | An installed Block Factory block satisfies its declared baseline capabilities: `vise check` counts a checklist item as present with evidence `source: "block:<id>"` when the `sp-vise/blocks.json` entry declares it in `providesCapabilities` and the install is still locally valid (dependency declared, touched files present), and the gap returns on drift. Field-level SDK model facts (`sdk-surface/models.<platform>.json`, wire format `2026-06-10.sdk-model-facts.v1`) ship for all five platform surfaces, names-and-types on every grounded platform. |
184
179
  | **Packed-package smoke** | Packed-package and host-agent smokes exercise the release tarball path, surfaced plan questions, selected optional capability sensors, rejected design confirmation handling, and exact contract-rule evidence for shared product expectations. |
185
180
 
186
181
  ### Supporting Proof
@@ -420,9 +415,9 @@ MCP-capable hosts can call Vise as structured tool calls instead of shell comman
420
415
 
421
416
  ### Tool names (snake_case per MCP convention)
422
417
 
423
- `inspect_project`, `creative_brief`, `creative_accept`, `ux_harness`, `compile_experience`, `experience_sensors`, `plan_harness`, `plan_integration`, `init_compliance`, `check_compliance`, `experience_report`, `record_learning`, `show_learning`, `sync_compliance`, `attest_rule`, `explain_rule`, `init_engagement`, `show_engagement`, `resolve_request`, `search_docs`, `get_doc_page`, `debug_issue`, `validate_setup`, `run_sensors`, `suggest_patch`, `design_extract`, `design_check`, `design_preview`, `design_reference`, `design_init_tokens`.
418
+ `inspect_project`, `creative_brief`, `creative_accept`, `ux_harness`, `compile_experience`, `experience_sensors`, `plan_harness`, `plan_integration`, `init_compliance`, `check_compliance`, `experience_report`, `record_learning`, `show_learning`, `sync_compliance`, `attest_rule`, `explain_rule`, `init_engagement`, `show_engagement`, `search_docs`, `get_doc_page`, `debug_issue`, `validate_setup`, `run_sensors`, `design_extract`, `design_check`, `design_preview`, `design_reference`, `design_init_tokens`.
424
419
 
425
- These are the same operations as the CLI commands above, exposed as MCP tools.
420
+ These are the same operations as the CLI commands above, exposed as MCP tools. `get_sdk_facts` is also registered as the internal, read-only Block Factory SDK-facts provider tool (it inspects no customer project — see [docs/SDK_FACTS_FOR_BLOCK_FACTORY.md](docs/SDK_FACTS_FOR_BLOCK_FACTORY.md)). The adapter still answers the legacy `resolve_request` and `suggest_patch` names for backward compatibility, but they are deprecated in favour of `plan_integration` plus host-tool edits.
426
421
 
427
422
  ---
428
423
 
package/dist/tools/ast.js CHANGED
@@ -222,43 +222,148 @@ export function findCallExpressions(tree, calleePattern) {
222
222
  });
223
223
  return results;
224
224
  }
225
+ // Bound on identifier→identifier resolution chains. Five hops covers realistic
226
+ // re-binding (`const RAW = "…"; const T = RAW; const X = T;`) while keeping the
227
+ // walk cheap; a `visited` set guards against cyclic self-reference (which would
228
+ // be a type error anyway, but the parser still produces a tree for it).
229
+ const MAX_RESOLUTION_HOPS = 5;
225
230
  /**
226
231
  * Resolve an AST node to its literal string value within the same file.
227
232
  *
228
233
  * Handles:
229
234
  * - String literals directly → returns the string value
230
- * - Identifiers that reference a const/let/var with a string literal initializer
231
- * (single-step resolution only)
235
+ * - A pure-passthrough template literal `` `${X}` `` with exactly one
236
+ * substitution and no cooked text — which is value-equivalent to `X`; the
237
+ * substitution is resolved through this same resolver. Templates with any
238
+ * literal text (`` `pre${X}` ``) or a dynamic substitution (`` `${a.b}` ``)
239
+ * are NOT pure passthrough and stay unresolved. (Arbitrary string
240
+ * concatenation is a separate, larger blind spot — intentionally not handled.)
241
+ * - Identifiers that reference a const/let/var whose initializer resolves to a
242
+ * literal, following identifier→identifier re-bind chains up to
243
+ * MAX_RESOLUTION_HOPS deep (multi-hop), with a cycle guard. A binding to any
244
+ * dynamic expression (member access, call, parameter, env fallback, …)
245
+ * terminates the chain and stays unresolved — multi-hop never makes a runtime
246
+ * value look literal.
232
247
  *
233
248
  * Returns undefined if the value cannot be statically resolved.
234
249
  */
235
250
  export function resolveLiteralValue(node, tree) {
251
+ return resolveLiteralValueBounded(node, tree, MAX_RESOLUTION_HOPS, new Set());
252
+ }
253
+ function resolveLiteralValueBounded(node, tree, hopsLeft, visited) {
236
254
  // Direct string literal
237
255
  const directValue = extractStringLiteral(node);
238
256
  if (directValue !== undefined)
239
257
  return directValue;
240
- // Identifier try to resolve to declaration in same file
241
- // TypeScript uses "identifier", Kotlin uses "simple_identifier"
258
+ // Pure-passthrough template literal: `${X}` (exactly one substitution, no text).
259
+ // Equivalent to resolving X itself. Bounded to this one shape — not concat.
260
+ if (node.type === "template_string") {
261
+ const passthrough = templatePassthroughSubstitution(node);
262
+ if (passthrough)
263
+ return resolveLiteralValueBounded(passthrough, tree, hopsLeft, visited);
264
+ }
265
+ // Identifier — resolve to its declaration in the same file, then recurse into
266
+ // the initializer so identifier→identifier re-binds and template re-wraps
267
+ // chain through to the underlying literal.
268
+ // TypeScript uses "identifier", Kotlin uses "simple_identifier".
242
269
  if (node.type === "identifier" || node.type === "simple_identifier") {
270
+ if (hopsLeft <= 0)
271
+ return undefined;
243
272
  const name = node.text;
244
- return resolveIdentifierToLiteral(name, tree.rootNode);
273
+ if (visited.has(name))
274
+ return undefined; // cycle guard
275
+ visited.add(name);
276
+ const valueNode = resolveIdentifierToValueNode(name, tree.rootNode);
277
+ if (!valueNode)
278
+ return undefined;
279
+ return resolveLiteralValueBounded(valueNode, tree, hopsLeft - 1, visited);
245
280
  }
246
281
  return undefined;
247
282
  }
283
+ /**
284
+ * If `node` is a template literal that is a pure passthrough of a single
285
+ * substitution — `` `${X}` `` with exactly one `template_substitution` child,
286
+ * no `string_fragment` (cooked text) — return that substitution's inner
287
+ * expression node. Otherwise undefined.
288
+ */
289
+ function templatePassthroughSubstitution(node) {
290
+ if (node.type !== "template_string")
291
+ return undefined;
292
+ let substitution;
293
+ for (let i = 0; i < node.namedChildCount; i++) {
294
+ const child = node.namedChild(i);
295
+ if (!child)
296
+ continue;
297
+ if (child.type === "template_substitution") {
298
+ if (substitution)
299
+ return undefined; // more than one substitution → not passthrough
300
+ substitution = child;
301
+ }
302
+ else {
303
+ // Any other named child (e.g. string_fragment cooked text) → has literal
304
+ // content, so not a pure passthrough.
305
+ return undefined;
306
+ }
307
+ }
308
+ if (!substitution)
309
+ return undefined;
310
+ // The substitution wraps a single expression: `${ expr }`.
311
+ return substitution.namedChild(0) ?? undefined;
312
+ }
313
+ /**
314
+ * Unwrap TypeScript expression wrappers that are transparent for static value
315
+ * extraction — type casts and grouping that don't change the runtime value:
316
+ * - `expr as T` (as_expression)
317
+ * - `expr satisfies T` (satisfies_expression)
318
+ * - `(expr)` (parenthesized_expression)
319
+ * - `expr!` (non_null_expression)
320
+ * - `<T>expr` (type_assertion, angle-bracket cast)
321
+ * Nested wrappers are peeled fully (`({ … } as any)!`). The inner expression is
322
+ * always the wrapper's FIRST named child across these node types. Non-wrapper
323
+ * nodes are returned unchanged, so callers can apply this unconditionally.
324
+ *
325
+ * High-frequency motivation: agents emit `as any` constantly to silence TS
326
+ * errors, which otherwise hides the payload object from property extraction.
327
+ */
328
+ export function unwrapExpression(node) {
329
+ let current = node;
330
+ const wrappers = new Set([
331
+ "as_expression",
332
+ "satisfies_expression",
333
+ "parenthesized_expression",
334
+ "non_null_expression",
335
+ "type_assertion",
336
+ ]);
337
+ // Bounded peel: a handful of stacked casts at most; the guard prevents any
338
+ // pathological loop if a grammar ever yields a self-referential first child.
339
+ for (let i = 0; i < 16 && wrappers.has(current.type); i++) {
340
+ const inner = current.namedChild(0);
341
+ if (!inner)
342
+ break;
343
+ current = inner;
344
+ }
345
+ return current;
346
+ }
248
347
  /**
249
348
  * Pick a specific named property from an object argument node.
250
349
  * E.g., from `{ userId: HARDCODED }`, pick the value node for "userId".
350
+ *
351
+ * The argument is unwrapped first, so a cast/parenthesized/non-null-wrapped
352
+ * payload (`{ … } as any`, `({ … })`, `{ … }!`) is still treated as the inner
353
+ * object — otherwise the wrapper hides every property from extraction.
251
354
  */
252
355
  export function pickObjectProperty(objectNode, propertyName) {
253
- if (objectNode.type !== "object")
356
+ const unwrapped = unwrapExpression(objectNode);
357
+ if (unwrapped.type !== "object")
254
358
  return undefined;
255
- for (let i = 0; i < objectNode.namedChildCount; i++) {
256
- const prop = objectNode.namedChild(i);
359
+ for (let i = 0; i < unwrapped.namedChildCount; i++) {
360
+ const prop = unwrapped.namedChild(i);
257
361
  if (!prop || prop.type !== "pair")
258
362
  continue;
259
363
  const key = prop.childForFieldName("key");
260
364
  if (key && key.text === propertyName) {
261
- return prop.childForFieldName("value") ?? undefined;
365
+ const value = prop.childForFieldName("value");
366
+ return value ? unwrapExpression(value) : undefined;
262
367
  }
263
368
  }
264
369
  return undefined;
@@ -460,7 +565,26 @@ function extractStringLiteral(node) {
460
565
  }
461
566
  return undefined;
462
567
  }
463
- function resolveIdentifierToLiteral(name, root) {
568
+ /**
569
+ * Find the binding for `name` in the same file and return the AST node that is
570
+ * its initializer VALUE — not the resolved string. The caller
571
+ * (resolveLiteralValueBounded) then recurses into that node, so an initializer
572
+ * that is itself an identifier (re-bind) or a pure-passthrough template literal
573
+ * chains through to the underlying literal. Returns undefined when the binding
574
+ * is absent or is a parameter / dynamic expression rather than a value
575
+ * initializer.
576
+ *
577
+ * Per-platform value node:
578
+ * - TypeScript/JS: `variable_declarator` → its `value` field.
579
+ * - Kotlin: `property_declaration` with a `variable_declaration` → the initializer
580
+ * sibling (the node after `=`).
581
+ * - Swift: `property_declaration` with `pattern` → the DIRECT string-literal
582
+ * initializer only. A literal nested inside another expression
583
+ * (`env["KEY"] ?? ""`) is not a static binding and stays unresolved — so for
584
+ * Swift we deliberately surface only a line/multi-line string literal node,
585
+ * preserving the prior conservative behavior.
586
+ */
587
+ function resolveIdentifierToValueNode(name, root) {
464
588
  let result;
465
589
  walkTree(root, (node) => {
466
590
  if (result !== undefined)
@@ -473,41 +597,42 @@ function resolveIdentifierToLiteral(name, root) {
473
597
  const valueNode = node.childForFieldName("value");
474
598
  if (!valueNode)
475
599
  return;
476
- const literal = extractStringLiteral(valueNode);
477
- if (literal !== undefined)
478
- result = literal;
600
+ // Casts/grouping around the initializer are transparent (e.g.
601
+ // `const T = RAW as string;`).
602
+ result = unwrapExpression(valueNode);
479
603
  return;
480
604
  }
481
- // Kotlin: property_declaration with variable_declaration + string_literal
605
+ // Kotlin: property_declaration with variable_declaration + initializer
482
606
  if (node.type === "property_declaration") {
483
607
  const varDecl = node.namedChildren.find((c) => c.type === "variable_declaration");
484
608
  if (varDecl) {
485
609
  const idNode = varDecl.namedChildren.find((c) => c.type === "simple_identifier");
486
610
  if (!idNode || idNode.text !== name)
487
611
  return;
488
- const strLit = node.namedChildren.find((c) => c.type === "string_literal");
489
- if (!strLit)
490
- return;
491
- const literal = extractStringLiteral(strLit);
492
- if (literal !== undefined)
493
- result = literal;
612
+ // The initializer is the named child after the variable_declaration.
613
+ const declIdx = node.namedChildren.indexOf(varDecl);
614
+ const initializer = node.namedChildren[declIdx + 1];
615
+ // Only surface a string literal or a re-bind identifier as the value node;
616
+ // anything else (call, when-expression, etc.) is dynamic and stays
617
+ // unresolved, matching the prior literal-only behavior.
618
+ if (initializer && (initializer.type === "string_literal" || initializer.type === "simple_identifier")) {
619
+ result = initializer;
620
+ }
494
621
  return;
495
622
  }
496
623
  // Swift: property_declaration with pattern → simple_identifier; the string
497
- // literal must be a DIRECT child (the initializer) — a literal nested inside
498
- // e.g. `env["KEY"] ?? ""` is not a static binding and must not resolve.
624
+ // literal (or a re-bind identifier) must be a DIRECT child (the
625
+ // initializer) — a literal nested inside e.g. `env["KEY"] ?? ""` is not a
626
+ // static binding and must not resolve.
499
627
  const pattern = node.namedChildren.find((c) => c.type === "pattern");
500
628
  if (!pattern)
501
629
  return;
502
630
  const swiftId = pattern.namedChildren.find((c) => c.type === "simple_identifier");
503
631
  if (!swiftId || swiftId.text !== name)
504
632
  return;
505
- const swiftLit = node.namedChildren.find((c) => c.type === "line_string_literal" || c.type === "multi_line_string_literal");
506
- if (!swiftLit)
507
- return;
508
- const literal = extractStringLiteral(swiftLit);
509
- if (literal !== undefined)
510
- result = literal;
633
+ const swiftValue = node.namedChildren.find((c) => c.type === "line_string_literal" || c.type === "multi_line_string_literal" || c.type === "simple_identifier");
634
+ if (swiftValue)
635
+ result = swiftValue;
511
636
  }
512
637
  });
513
638
  return result;
@@ -1,5 +1,6 @@
1
1
  import { readdir, readFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
3
4
  import { objectInput, optionalNumberField, stringField, textResult } from "../types.js";
4
5
  import { packageUserAgent } from "../version.js";
5
6
  const DEFAULT_FETCH_TIMEOUT_MS = 15_000;
@@ -10,6 +11,38 @@ let hostedCachePending;
10
11
  function localDocsRoot() {
11
12
  return process.env.SOCIAL_PLUS_DOCS_ROOT ? path.resolve(process.env.SOCIAL_PLUS_DOCS_ROOT) : undefined;
12
13
  }
14
+ // Hermetic ("offline") docs mode. When VISE_DOCS_OFFLINE is set truthy, docs
15
+ // lookups are served from a committed, bundled corpus instead of the hosted
16
+ // learn.social.plus endpoint — so CI (and offline installs) never depend on the
17
+ // docs site being reachable. This kills the learn.social.plus flake class that
18
+ // once hit test:cli. The live-fetch path stays the default when neither
19
+ // VISE_DOCS_OFFLINE nor SOCIAL_PLUS_DOCS_ROOT is set, so real-fetch coverage is
20
+ // preserved (see vise/package.json test:docs-live).
21
+ //
22
+ // Resolution precedence in loadDocPages():
23
+ // 1. SOCIAL_PLUS_DOCS_ROOT (explicit local override) — highest, unchanged.
24
+ // 2. VISE_DOCS_OFFLINE — serve the bundled docs-cache/ corpus.
25
+ // 3. neither — live fetch from the hosted docs (default).
26
+ function docsOfflineMode() {
27
+ const raw = process.env.VISE_DOCS_OFFLINE;
28
+ if (raw === undefined) {
29
+ return false;
30
+ }
31
+ const normalized = raw.trim().toLowerCase();
32
+ return normalized !== "" && normalized !== "0" && normalized !== "false" && normalized !== "off" && normalized !== "no";
33
+ }
34
+ // The bundled hermetic corpus ships in the package `files` allowlist as
35
+ // `docs-cache/`. At runtime this module is dist/tools/docs.js, so the corpus is
36
+ // two directories up. An explicit SOCIAL_PLUS_DOCS_OFFLINE_ROOT override is
37
+ // honoured for tests that want to point offline mode at a fixture corpus.
38
+ function bundledDocsCacheRoot() {
39
+ const override = process.env.SOCIAL_PLUS_DOCS_OFFLINE_ROOT;
40
+ if (override) {
41
+ return path.resolve(override);
42
+ }
43
+ const moduleDir = path.dirname(fileURLToPath(import.meta.url));
44
+ return path.resolve(moduleDir, "..", "..", "docs-cache");
45
+ }
13
46
  function docsBaseUrl() {
14
47
  return (process.env.SOCIAL_PLUS_DOCS_BASE_URL ?? "https://learn.social.plus").replace(/\/+$/, "");
15
48
  }
@@ -110,6 +143,16 @@ async function loadDocPages() {
110
143
  if (root) {
111
144
  return loadLocalDocPages(root);
112
145
  }
146
+ // Hermetic mode: serve the committed bundled corpus, never the network.
147
+ if (docsOfflineMode()) {
148
+ const cacheRoot = bundledDocsCacheRoot();
149
+ const pages = await loadLocalDocPages(cacheRoot);
150
+ if (pages.length === 0) {
151
+ throw new Error(`VISE_DOCS_OFFLINE is set but the bundled docs corpus at ${cacheRoot} is empty or missing. ` +
152
+ `Set SOCIAL_PLUS_DOCS_ROOT to a local docs directory, point SOCIAL_PLUS_DOCS_OFFLINE_ROOT at a corpus, or unset VISE_DOCS_OFFLINE to fetch live.`);
153
+ }
154
+ return pages;
155
+ }
113
156
  const now = Date.now();
114
157
  if (hostedCacheEntry && now - hostedCacheEntry.fetchedAt < cacheTtlMs()) {
115
158
  return hostedCacheEntry.pages;
@@ -271,6 +314,11 @@ async function findDocFiles(root) {
271
314
  if (entry.name === "node_modules" || entry.name === ".git" || entry.name === ".next") {
272
315
  continue;
273
316
  }
317
+ // A README is corpus metadata (e.g. the docs-cache/ provenance note), not
318
+ // an SDK doc page — don't ingest it as a searchable page.
319
+ if (/^readme\.(md|mdx)$/i.test(entry.name)) {
320
+ continue;
321
+ }
274
322
  const entryPath = path.join(directory, entry.name);
275
323
  if (entry.isDirectory()) {
276
324
  await walk(entryPath);
@@ -51,6 +51,47 @@ const CAPABILITIES = {
51
51
  { name: "Reactable", kind: "model", owner: "Amity" },
52
52
  ],
53
53
  },
54
+ // Added with the Block Factory feed block (block #3). The bundled model facts
55
+ // (models.typescript.json) already tagged Amity.Post with capability "posts";
56
+ // this table entry closes the data/vocabulary gap so sdk-facts can answer for
57
+ // the posts surface the same way it does for comments and reactions.
58
+ posts: {
59
+ required: [
60
+ { name: "PostRepository", kind: "type" },
61
+ { name: "getPosts", kind: "member", owner: "PostRepository" },
62
+ { name: "createPost", kind: "member", owner: "PostRepository" },
63
+ ],
64
+ optional: [
65
+ { name: "getPost", kind: "member", owner: "PostRepository" },
66
+ { name: "editPost", kind: "member", owner: "PostRepository" },
67
+ { name: "softDeletePost", kind: "member", owner: "PostRepository" },
68
+ { name: "hardDeletePost", kind: "member", owner: "PostRepository" },
69
+ { name: "flagPost", kind: "member", owner: "PostRepository" },
70
+ { name: "unflagPost", kind: "member", owner: "PostRepository" },
71
+ { name: "isPostFlaggedByMe", kind: "member", owner: "PostRepository" },
72
+ { name: "onPostCreated", kind: "member", owner: "PostRepository" },
73
+ { name: "onPostUpdated", kind: "member", owner: "PostRepository" },
74
+ { name: "onPostDeleted", kind: "member", owner: "PostRepository" },
75
+ ],
76
+ models: [{ name: "Post", kind: "model", owner: "Amity" }],
77
+ },
78
+ users: {
79
+ required: [
80
+ { name: "UserRepository", kind: "type" },
81
+ { name: "getUser", kind: "member", owner: "UserRepository" },
82
+ ],
83
+ optional: [
84
+ { name: "blockUser", kind: "member", owner: "UserRepository" },
85
+ { name: "unBlockUser", kind: "member", owner: "UserRepository" },
86
+ { name: "follow", kind: "member", owner: "UserRepository" },
87
+ { name: "unfollow", kind: "member", owner: "UserRepository" },
88
+ { name: "getFollowers", kind: "member", owner: "UserRepository" },
89
+ { name: "getFollowings", kind: "member", owner: "UserRepository" },
90
+ { name: "getFollowInfo", kind: "member", owner: "UserRepository" },
91
+ { name: "getMyFollowInfo", kind: "member", owner: "UserRepository" },
92
+ ],
93
+ models: [{ name: "User", kind: "model", owner: "Amity" }],
94
+ },
54
95
  };
55
96
  export const getSdkFactsTool = {
56
97
  name: "get_sdk_facts",
@@ -64,7 +105,10 @@ export const getSdkFactsTool = {
64
105
  },
65
106
  capability: {
66
107
  type: "string",
67
- description: "Optional capability filter. Current MVP supports comments and reactions for the TypeScript/React Native surface.",
108
+ // Derived from the capability table so the description can never
109
+ // lag behind a newly added capability (the old hand-written text
110
+ // still said "comments and reactions" after posts landed).
111
+ description: `Optional capability filter. Known capability ids: ${Object.keys(CAPABILITIES).join(", ")}.`,
68
112
  },
69
113
  surfaceDir: {
70
114
  type: "string",
@@ -0,0 +1,34 @@
1
+ # Bundled hermetic docs corpus
2
+
3
+ A committed snapshot of social.plus SDK docs pages, served when **`VISE_DOCS_OFFLINE`** is set so docs lookups (`search_docs` / `get_doc_page`) never depend on `learn.social.plus` being reachable. This kills the docs-site flake class that once hit `test:cli`.
4
+
5
+ It ships with the package (in the `files` allowlist) so installed users and CI can run docs lookups fully offline.
6
+
7
+ ## How docs resolution chooses this corpus
8
+
9
+ In `src/tools/docs.ts` `loadDocPages()`:
10
+
11
+ 1. `SOCIAL_PLUS_DOCS_ROOT` set → that local directory (explicit override; unchanged).
12
+ 2. `VISE_DOCS_OFFLINE` truthy → **this `docs-cache/` corpus** (or `SOCIAL_PLUS_DOCS_OFFLINE_ROOT` if set, used by tests).
13
+ 3. neither → live fetch from the hosted docs (the default; real-fetch coverage lives in `test:docs-live`).
14
+
15
+ CI / the `validate` chain run hermetic via `test:docs-offline`. The live path is the separate `test:docs-live` lane (set `VISE_DOCS_LIVE=1`), kept off the critical path so a docs outage never reds CI.
16
+
17
+ ## Contents & provenance
18
+
19
+ The pages are authentic content extracted from the hosted `llms-full.txt`, scoped to the canonical paths Vise relies on (`knownDocPaths()` in `src/outcomes.ts`) plus the post-creation page used by `test/run-cli.mjs`. Each `.mdx` file is `# <title>` followed by the page body, matching the layout `loadLocalDocPages()` expects.
20
+
21
+ This is a curated subset, not the full site — enough to make docs lookups deterministic and offline, not a mirror.
22
+
23
+ ### Regenerate
24
+
25
+ ```sh
26
+ # 1. Snapshot the hosted llms-full.txt:
27
+ curl -s https://learn.social.plus/llms-full.txt -o /tmp/llms-full.txt
28
+
29
+ # 2. Extract the knownDocPaths() pages into docs-cache/<path>.mdx, each as
30
+ # `# <title>\n\n<body>`. (One-off node script split on the
31
+ # `### [title](url)` page headings, same split parseLlmsFull uses.)
32
+ ```
33
+
34
+ Refresh when `knownDocPaths()` changes or the hosted docs materially move. `test/run-docs-offline.mjs` asserts the corpus answers `flutter quick start` and serves the flutter-quick-start page, so an empty/broken refresh fails CI loudly.