@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.
- package/CHANGELOG.md +40 -0
- package/README.md +6 -11
- package/dist/tools/ast.js +153 -28
- package/dist/tools/docs.js +48 -0
- package/dist/tools/sdkFacts.js +45 -1
- package/docs-cache/README.md +34 -0
- package/docs-cache/social-plus-sdk/core-concepts/foundation/logging.mdx +236 -0
- package/docs-cache/social-plus-sdk/core-concepts/realtime-communication/live-objects-collections/android.mdx +262 -0
- package/docs-cache/social-plus-sdk/core-concepts/realtime-communication/live-objects-collections/flutter.mdx +195 -0
- package/docs-cache/social-plus-sdk/core-concepts/realtime-communication/live-objects-collections/ios.mdx +452 -0
- package/docs-cache/social-plus-sdk/core-concepts/realtime-communication/live-objects-collections/overview.mdx +133 -0
- package/docs-cache/social-plus-sdk/core-concepts/realtime-communication/live-objects-collections/typescript.mdx +264 -0
- package/docs-cache/social-plus-sdk/core-concepts/realtime-communication/push-notifications/register-and-unregister-push-notifications-on-a-device.mdx +191 -0
- package/docs-cache/social-plus-sdk/core-concepts/realtime-communication/push-notifications/settings/overview.mdx +43 -0
- package/docs-cache/social-plus-sdk/core-concepts/realtime-communication/push-notifications/setup/android-setup.mdx +360 -0
- package/docs-cache/social-plus-sdk/core-concepts/realtime-communication/push-notifications/setup/flutter-setup.mdx +457 -0
- package/docs-cache/social-plus-sdk/core-concepts/realtime-communication/push-notifications/setup/ios-setup.mdx +423 -0
- package/docs-cache/social-plus-sdk/core-concepts/realtime-communication/push-notifications/setup/react-native-setup.mdx +384 -0
- package/docs-cache/social-plus-sdk/core-concepts/realtime-communication/realtime-events/overview.mdx +94 -0
- package/docs-cache/social-plus-sdk/getting-started/authentication.mdx +808 -0
- package/docs-cache/social-plus-sdk/getting-started/platform-setup/mobile/android-quick-start.mdx +304 -0
- package/docs-cache/social-plus-sdk/getting-started/platform-setup/mobile/flutter-quick-start.mdx +121 -0
- package/docs-cache/social-plus-sdk/getting-started/platform-setup/mobile/ios-quick-start.mdx +225 -0
- package/docs-cache/social-plus-sdk/getting-started/platform-setup/web/web-quick-start.mdx +99 -0
- package/docs-cache/social-plus-sdk/social/communities-spaces/organization/community-invitation.mdx +459 -0
- package/docs-cache/social-plus-sdk/social/communities-spaces/organization/join-leave-community.mdx +449 -0
- package/docs-cache/social-plus-sdk/social/communities-spaces/organization/query-community-members.mdx +376 -0
- package/docs-cache/social-plus-sdk/social/content-management/posts/creation/text-post.mdx +318 -0
- package/docs-cache/social-plus-sdk/social/discovery-engagement/notifications/notification-tray-status.mdx +399 -0
- package/docs-cache/social-plus-sdk/social/user-relationship/blocking/block-unblock-user.mdx +166 -0
- package/docs-cache/social-plus-sdk/social/user-relationship/following/get-follower-following-list.mdx +339 -0
- package/package.json +10 -3
- package/scripts/dart-model-extractor/bin/extract_models.dart +169 -0
- package/scripts/dart-model-extractor/pubspec.lock +149 -0
- package/scripts/dart-model-extractor/pubspec.yaml +16 -0
- package/scripts/extract-sdk-models.mjs +353 -12
- package/scripts/import-sdk-surface.mjs +10 -19
- package/sdk-surface/manifest.json +15 -15
- package/sdk-surface/models.android.json +1 -1
- package/sdk-surface/models.flutter.json +465 -465
- package/sdk-surface/models.ios.json +188 -188
- package/sdk-surface/models.typescript.json +1 -1
- 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
|
|
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.
|
|
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 **
|
|
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`, `
|
|
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
|
-
* -
|
|
231
|
-
*
|
|
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
|
-
//
|
|
241
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
356
|
+
const unwrapped = unwrapExpression(objectNode);
|
|
357
|
+
if (unwrapped.type !== "object")
|
|
254
358
|
return undefined;
|
|
255
|
-
for (let i = 0; i <
|
|
256
|
-
const prop =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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 +
|
|
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
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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
|
|
498
|
-
// e.g. `env["KEY"] ?? ""` is not a
|
|
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
|
|
506
|
-
if (
|
|
507
|
-
|
|
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;
|
package/dist/tools/docs.js
CHANGED
|
@@ -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);
|
package/dist/tools/sdkFacts.js
CHANGED
|
@@ -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
|
-
|
|
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.
|