@amityco/social-plus-vise 0.12.4 → 0.13.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 CHANGED
@@ -4,6 +4,72 @@ All notable changes to `@amityco/social-plus-vise` are documented in this file.
4
4
 
5
5
  The format is loosely based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## 0.13.0 — 2026-06-03
8
+
9
+ **Theme:** Deterministic-gate soundness, CLI reliability, and post-rebrand coherence (driven by two repo reviews).
10
+
11
+ ### Fixed
12
+ - **Boolean flags now accept `--flag=true/false`.** `--ci=true`, `--dry-run=true`, `--force=true`, etc. were parsed as known flags but never detected, so `vise check --ci=true` silently ran in non-CI mode without erroring. Boolean flags now parse `=true/1/yes` → on and `=false/0/no` → off, and reject other values loudly.
13
+ - **`vise install-skill` supersedes pre-rebrand installs.** Installing removes a stale sibling `social-plus-foundry/` skill directory (and `social-plus-foundry.mdc` for `cursor-rules`) so hosts no longer serve old `spf`/foundry guidance alongside the current skill. Reported as `supersededLegacy` in the command output.
14
+ - **CLI no longer crashes on a missing tree-sitter native binding** — the parser loads lazily and degrades to regex-only instead of taking down every command (including doc lookup) at startup; affects platforms without prebuilt binaries (linux-arm64, Alpine/musl, win32-arm64).
15
+ - **Gating literal checks are comment-aware** — a commented-out or documented `channelId`/`apiKey` no longer trips a no-escape gate (a false positive that hard-failed CI with no attestation path). ts/tsx/kotlin use the tree-sitter stripper; Swift/Dart use a conservative string-aware scanner that only blanks comment spans.
16
+
17
+ ### Added
18
+ - **`vise_version`** in `sp-vise/compliance.json`, attestations, `engagement.json`, and the design contract — written alongside the retained `foundry_version` (backward-compatible alias; excluded from all digests, so existing contracts do not drift).
19
+
20
+ ### Changed
21
+ - **Docs/copy aligned to the `Vise` name:** README, skill, and the generated sidecar no longer reference a non-existent `sp-check` binary (use `vise check`); `run-sensors` safety wording corrected ("runs detected project scripts/wrappers; inspect with `--dry-run` before running in an untrusted project"); `RULES.md` gating semantics corrected — the exit code is driven by `advisory`/`attestation.allowed`, not `severity`.
22
+ - **README benchmark section recalibrated** to match the Commune paper: N=1 caveat moved up front, the deterministic-grader/circularity disclosed, the speculative "rework sessions" table removed, and the null bug-fix-benchmark result noted.
23
+
24
+ ---
25
+
26
+ ## 0.12.5 — 2026-06-02
27
+
28
+ **Theme:** Design token lifecycle — customers can maintain a dedicated social.plus token file independently from their main app, with no AI agent required for updates.
29
+
30
+ ### Added
31
+ - **`vise design init-tokens [path] [--force]`** — scaffolds `src/styles/social-plus-tokens.css` in the customer's project, the single editable source for social.plus feature styling. **Greenfield:** full `--sp-*` neutral defaults (color, typography, spacing, radius, shadow, motion, sizing, z-index, breakpoints). **Brownfield:** seeded from the project's existing concrete token values, namespaced as `--sp-*` with origin comments. Idempotent — never clobbers an existing file; `--force` to override. The token file lives in `src/` (ships with the app); `sp-vise/` holds only the contract (never ships).
32
+ - **Freshness check in `vise design check`** — hashes `source.inputs` files at extract time, stores as `source.input_digests` in the contract. On `design check`, current file hashes are compared; if any changed, an advisory `staleContract` field surfaces a nudge: *"Run `vise design extract --from-project` to refresh."* Never blocks; purely informational. Works identically across all platforms.
33
+
34
+ ---
35
+
36
+ ## 0.12.4 — 2026-06-02
37
+
38
+ **Theme:** Advisory rule model + honest benchmark methodology.
39
+
40
+ ### Added
41
+ - **Advisory rule flag** (`advisory: true` in rule YAML) — rules marked advisory surface in `vise check` output with `status: "advisory"` but never contribute to `exitCode` or `needs-attestation`. Use for checks where the right answer depends on tenant configuration Vise cannot observe.
42
+
43
+ ### Changed
44
+ - **`reactions.configured-name-used`** (all 5 platforms) downgraded to advisory. Rule fires on ~100% of correct apps (every tenant defaults to `"like"`) and was only clearable by a ritual comment. Version bumped to 2; existing compliance.json files will show contract-drift — run `vise sync` to update.
45
+
46
+ ### Benchmark
47
+ - Brand benchmark (Spotify Encore × social.plus community feed, Sonnet n=3): pure-mcp 0/3 behavioral compliance (avg 2.3 behavioral findings), vise-design 3/3 (0). Ban-state is the standout discriminator — missed by all pure-mcp agents, fixed by all vise-design agents through the iteration loop.
48
+ - Ambiguous-brief design test: vise-design 0 hex literals (all 3 seeds), pure-mcp 0/2/15 (high variance). Design loop is a variance-reduction tool.
49
+ - Grader now partitions findings into behavioral / file-presence / attestation-dialect; headline score is behavioral-only.
50
+
51
+ ---
52
+
53
+ ## 0.12.3 — 2026-06-02
54
+
55
+ **Theme:** Design harness graduation — complete token extraction, Circles-inspired visual reference.
56
+
57
+ ### Added
58
+ - **`vise design reference [path] [--title <name>]`** — generates a self-contained `sp-vise/design-reference.html` design-system spec: fixed sidebar with grouped nav (COLOR / TYPOGRAPHY / LAYOUT / SURFACE / EFFECTS), sticky topbar, section headers with display-font titles, monospace token-row lists for non-visual groups (motion, breakpoints, z-index), and component samples. Reads source CSS for live `var()` resolution; falls back to contract tokens for native projects (Android/Flutter/iOS correctly grouped by category with concrete values).
59
+ - **6 new token categories** — `fontWeight`, `lineHeight`, `letterSpacing`, `borderWidth`, `breakpoint`, `zIndex`. All name-gated to prevent false positives (bare integers, unitless decimals are ambiguous without the name signal).
60
+
61
+ ### Fixed (silent miscategorizations)
62
+ - `--fs-*` (e.g. `--fs-sm: 14px`) was categorized as "space" — now correctly "fontSize".
63
+ - `--border-width-*` (e.g. `--border-width-thin: 1px`) was categorized as "color" (the color-name regex matched `/border/`) — now correctly "borderWidth". Fix requires the borderWidth check to precede the color branch.
64
+ - `--bp-*` and `--ls-*` fell through to the LENGTH→space fallback — now correctly "breakpoint" and "letterSpacing".
65
+
66
+ ### Changed
67
+ - `renderDesignPreview` now includes sections for all 6 new categories so they aren't silently omitted from `vise design preview` output.
68
+ - `categorizeTokenModuleValue` explicitly excludes the new categories (CSS-only extraction — consistent with the existing opacity exclusion).
69
+ - Streamly seed-ui reference: **69 → 80 tokens**, zero "not extracted" tags in the generated HTML.
70
+
71
+ ---
72
+
7
73
  ## 0.12.2 — 2026-06-02
8
74
 
9
75
  **Maintenance / hygiene release.** No functional change from `0.12.1` — identical rules, validators, and CLI. This release exists to scrub an anonymized customer name from the bundled `CHANGELOG`; `0.12.0` and `0.12.1` (which contained it) were unpublished from npm. Use `0.12.2`.
package/README.md CHANGED
@@ -47,6 +47,22 @@ Instead of just providing a CLI or AI skills, Vise implements a technique called
47
47
 
48
48
  Vise acts as the foreman of this factory, wrapping your local coding agents in compliance guardrails when they integrate social.plus SDKs. It inspects your project, grounds the agent in hosted docs, enforces 300 platform-specific compliance rules, checks the generated UI against the customer's design system, surfaces the full SDK feature surface so nothing is silently dropped, and runs your project's own build/lint/typecheck sensors. **Your source code never leaves your machine.**
49
49
 
50
+ At a glance, Vise sits between the user's prompt and the agent's code changes. The agent still edits the app; Vise turns the request into a grounded plan, records the local contract, and keeps checking until the integration is ready to ship.
51
+
52
+ ```mermaid
53
+ flowchart LR
54
+ Prompt["User prompt<br/>Add a social.plus feature"] --> Skill["AI skill<br/>drives the loop"]
55
+ Skill --> Inspect["Inspect project<br/>platform, app surface,<br/>design signals"]
56
+ Inspect --> Plan["Plan<br/>outcome, docs,<br/>intake questions"]
57
+ Plan --> Design["Design + completeness<br/>tokens, feature checklist,<br/>explicit opt-outs"]
58
+ Design --> Build["Agent builds<br/>edits customer code locally"]
59
+ Build --> Check["Vise check<br/>SDK compliance gate"]
60
+ Check -->|findings| Build
61
+ Check --> Sensors["Sensors<br/>typecheck, build,<br/>lint, SDK smoke"]
62
+ Sensors -->|failures| Build
63
+ Sensors --> Done["Done<br/>sp-vise contract<br/>and evidence"]
64
+ ```
65
+
50
66
  | Layer | Purpose |
51
67
  |---|---|
52
68
  | **Skill** (`SKILL.md`) | Tells your AI agent when to inspect, plan, fetch docs, edit, validate, and attest |
@@ -59,7 +75,7 @@ Vise validates on three layers, and the layer is set by the *kind of claim* —
59
75
 
60
76
  | Layer | Claim | How | Enforcement |
61
77
  |---|---|---|---|
62
- | **SDK compliance** | "this is **wrong**" | 300 deterministic rules (session renewal, live-collection vs one-shot, no secret in logs, parent-child rendering, ban-state gating…) | **Hard gate** — `vise check` blocks until green or attested |
78
+ | **SDK compliance** | "this is **wrong**" | 300 deterministic rules (session renewal, live-collection vs one-shot, no secret in logs, parent-child rendering, ban-state gating…) | **Hard gate** — `vise check` blocks until green or attested. A small advisory subset surfaces as informational only and never blocks. |
63
79
  | **Design conformance** | "this **looks off**" | extract the customer's design system into a contract, then check token usage | **Advisory** — `vise design check`/`preview`; never fails a build |
64
80
  | **Feature completeness** | "this is **missing**" | Vise proposes the full SDK feature surface per outcome; the agent opts out of anything out of scope with a recorded reason | **Advisory** — surfaced in `vise plan`/`check`; never fails a build |
65
81
 
@@ -67,7 +83,9 @@ Only correctness is gated (it can be made FP-free); conformance and completeness
67
83
 
68
84
  ### Design-conformant UI
69
85
 
70
- Vise can ingest the customer's aesthetic into a **design contract** and guide generation to match it — from an HTML/CSS prototype (`vise design extract`) or from the host app's own design system across web + Android + Flutter + iOS (`vise design extract --from-project`: CSS vars/Tailwind/token modules, `colors.xml`, Flutter `Color(0x…)`, iOS `.colorset`/Swift). `vise design check` reports token conformance; `vise design preview` writes a visual review. All advisory.
86
+ Vise can ingest the customer's aesthetic into a **design contract** and guide generation to match it — from an HTML/CSS prototype (`vise design extract`) or from the host app's own design system across web + Android + Flutter + iOS (`vise design extract --from-project`: CSS vars/Tailwind/token modules, `colors.xml`, Flutter `Color(0x…)`, iOS `.colorset`/Swift). `vise design check` reports token conformance; `vise design preview` writes a visual review; `vise design reference` generates a full visual design-system spec (swatches, type samples, component demos). All advisory.
87
+
88
+ **For social.plus-specific styling:** `vise design init-tokens` scaffolds `src/styles/social-plus-tokens.css` in your project — a dedicated token file for social.plus features that you can edit independently from your main app's design system. Greenfield projects get sensible `--sp-*` defaults; brownfield projects get their existing token values seeded in. Edit the file, run `vise design extract --from-project` to refresh the contract, and future agent builds inherit the updated palette — no AI agent needed in the update loop.
71
89
 
72
90
  ### Supported integrations (outcomes)
73
91
 
@@ -81,8 +99,8 @@ A bench vise holds the workpiece steady so the craftsman's hands are free to sha
81
99
 
82
100
  ## Benchmark: Phase 1 Results
83
101
 
84
- > **Every feature delivered correctly confirmed independently with two different AI coding tools.**
85
- > With Vise, both agents built all 9 social features with no production gaps. Without Vise, 3 out of 9 features had hidden problems that would only surface after users complained.
102
+ > **The compliance gaps agents ship on their own, they close under Vise's check loop.**
103
+ > Across two capable coding agents (Cursor / Composer 2.5 and Claude Sonnet 4.6), the features with *secondary* compliance requirements Chat, Moderation, Push failed without Vise and passed with it; both agents reached 9/9 with Vise. This is a **strong directional signal at N=1 per cell, not a settled statistical finding.** The [Commune paper](docs/commune-paper-2026-05-30.md) is the full, honest version — methodology, per-cell results, threats to validity, and a complementary bug-fix benchmark where Vise showed *no* advantage.
86
104
 
87
105
  ### What "delivered correctly" means
88
106
 
@@ -93,7 +111,7 @@ A bench vise holds the workpiece steady so the craftsman's hands are free to sha
93
111
  - **Moderation actions** (report, flag, block) are surfaced in the UI so users can act on them, not buried in a hook
94
112
  - **Chat and feed queries** use live, reactive subscriptions — not one-time fetches that go stale
95
113
 
96
- Without Vise, AI agents frequently implement the primary feature correctly but miss these secondary requirements. They know about them in the abstract — but when building a chat screen, "ban state" feels out of scope and gets skipped. `sp-check` turns that vague awareness into a specific, actionable finding.
114
+ Without Vise, AI agents frequently implement the primary feature correctly but miss these secondary requirements. They know about them in the abstract — but when building a chat screen, "ban state" feels out of scope and gets skipped. `vise check` turns that vague awareness into a specific, actionable finding.
97
115
 
98
116
  ### The experiment: three conditions, nine features
99
117
 
@@ -106,7 +124,7 @@ SDK setup · User presence · Social feed · Events · Chat & DMs · Push notifi
106
124
  |---|---|---|
107
125
  | **Pure MCP** | Access to social.plus docs only — no compliance guidance | Baseline: how well does the agent do on its own? |
108
126
  | **Rules-as-Markdown** | The full 1,013-line compliance rulebook pasted directly into the prompt | Is the problem just that the agent doesn't know the rules? |
109
- | **Vise + Skill** | Full Vise CLI — `sp-check` runs automatically, agent reads specific findings, fixes them, repeats until green | Does an active feedback loop change the outcome? |
127
+ | **Vise + Skill** | Full Vise CLI — `vise check` runs automatically, agent reads specific findings, fixes them, repeats until green | Does an active feedback loop change the outcome? |
110
128
 
111
129
  The Rules-as-Markdown condition is the key isolation: if the agent already knows all the rules, does giving it the spec document fix the problem? The answer turned out to be **no** — knowing the rules and being forced to act on specific findings are different things.
112
130
 
@@ -117,38 +135,28 @@ The Rules-as-Markdown condition is the key isolation: if the agent already knows
117
135
  | **Cursor (Composer 2.5)** | 6 out of 9 ✗ | 5 out of 9 ✗ | **9 out of 9 ✅** |
118
136
  | **Claude Code (Sonnet 4.6)** | 6 out of 9 ✗ | 7 out of 9 ✗ | **9 out of 9 ✅** |
119
137
 
120
- The three features that consistently fail without Vise — **Chat**, **Moderation**, and **Push Notifications** — are exactly the ones with secondary compliance requirements (ban-state, report affordances, Amity preference API). Vise's `sp-check` catches these with a specific finding; the rules doc does not.
121
-
122
- Both agents reached a perfect score with Vise. Neither could reach it with the compliance spec pasted into the prompt. All 9 passes were independently verified by code inspection — no scoring shortcuts.
138
+ The three features that consistently fail without Vise — **Chat**, **Moderation**, and **Push Notifications** — are exactly the ones with secondary compliance requirements (ban-state, report affordances, Amity preference API). `vise check` catches these with a specific finding; the rules doc does not.
123
139
 
124
- ### Efficiencyrework sessions needed
140
+ Both agents reached 9/9 with Vise. The Rules-as-Markdown arm did **not** reliably beat the plain-docs control 5/9 on Cursor (*below* control) and 7/9 on Sonnet — and at N=1 per cell neither gap is distinguishable from noise. The robust, reproducible signal is narrower and mechanistic: **Chat and Moderation never pass under either control arm, and always pass under Vise.** Passes were scored by a deterministic grader, not by hand — see [Reproducibility & honest caveats](#reproducibility--honest-caveats) for what that grader does and doesn't establish.
125
141
 
126
- Vise delivers all 9 features correctly in a single session. The other conditions leave failing features that require additional sessions to diagnose (the gap isn't visible without `sp-check`) and fix.
127
-
128
- | Coding agent (model) | Condition | Features correct | Rework sessions needed |
129
- |---|---|---|---|
130
- | **Cursor (Composer 2.5)** | Pure MCP | 6 / 9 ✗ | +3 or more |
131
- | **Cursor (Composer 2.5)** | Rules-as-Markdown | 5 / 9 ✗ | +4 or more |
132
- | **Cursor (Composer 2.5)** | **Vise + Skill** | **9 / 9 ✅** | **0 ✅** |
133
- | **Claude Code (Sonnet 4.6)** | Pure MCP | 6 / 9 ✗ | +3 or more |
134
- | **Claude Code (Sonnet 4.6)** | Rules-as-Markdown | 7 / 9 ✗ | +2 or more |
135
- | **Claude Code (Sonnet 4.6)** | **Vise + Skill** | **9 / 9 ✅** | **0 ✅** |
142
+ ### Why it matters
136
143
 
137
- <sub>Rework sessions are additional developer-initiated prompts needed after the initial session to diagnose and fix the failing features. Each failing feature typically requires at least one session to identify the gap and one to fix it and that's without the benefit of `sp-check` pointing directly at the problem.</sub>
144
+ A failing feature without Vise is *invisible* until a user hits it: the code compiles, the demo works, and the ban-state gap surfaces only when a banned user posts. Vise turns that latent gap into a specific finding the agent fixes before you ship. (We did not separately measure remediation effort, so this makes no rework-cost claim only that the gaps are real and silent without a checker.)
138
145
 
139
- ### Reproducibility
146
+ ### Reproducibility & honest caveats
140
147
 
141
- - **Gate-checked:** Every pass was verified by code inspection — the Vise workspaces contain an actual UI-level ban gate; the pure-MCP workspaces do not. Zero attestation shortcuts.
142
- - **Built from scratch** (greenfield seed)not patching existing code.
143
- - **Three arms run with separate tooling.** The Rules-as-Markdown arm has no `sp-check` tool available — it cannot "cheat" by running the checker.
144
- - **N=1 per cell (Phase 1).** Each agent ran each scenario once. Repeatability seeds on the three most discriminating slices (CM-CHAT, CM-MODERATE, CM-PUSH) are pending. These results should be treated as a strong initial signal, not a statistically settled finding.
145
- - Full per-feature scorecards, agent transcripts, and workspace diffs: [`benchmarks/FINDINGS.html`](benchmarks/FINDINGS.html) · [`benchmarks/RULES_AS_MARKDOWN.html`](benchmarks/RULES_AS_MARKDOWN.html)
148
+ - **Scoring is deterministic — and it overlaps with what Vise enforces.** Each cell is graded on four dimensions: `vise check --ci` (the same compliance ruleset), the project's own sensors (build / typecheck / lint), and hand-authored string-inclusion acceptance patterns. Because the metric overlaps Vise's own rules and only the Vise arm iterates against that checker read the headline as "Vise's checks pass," not as a fully independent oracle. The acceptance patterns are literal string matching (not AST), so they involve authoring judgment.
149
+ - **Vise-arm passes were deterministic-pass**, not attestation exceptions agents fixed the code. (The grader applies a narrow, *symmetric* auto-attestation for absence / type-stub findings across **all** arms including the controls; it cannot satisfy the acceptance patterns, so it does not tilt the result toward Vise.)
150
+ - **Three arms, separate tooling.** The Rules-as-Markdown arm has no Vise checker available — it cannot run `vise check`.
151
+ - **Built from scratch** (greenfield seed), capable models with prior SDK familiarity. A complementary **bug-fix** benchmark showed **no Vise advantage** the loop helps on greenfield integration, not local bug hunts.
152
+ - **N=1 per cell.** A strong directional signal (the Chat/Moderation/Push mechanism reproduces across both models), **not** a statistically settled finding; repeatability seeds are pending.
153
+ - **Full methodology, per-cell analysis, and threats to validity:** [the Commune paper](docs/commune-paper-2026-05-30.md). The [`benchmarks/FINDINGS.html`](benchmarks/FINDINGS.html) and [`benchmarks/RULES_AS_MARKDOWN.html`](benchmarks/RULES_AS_MARKDOWN.html) files are **summary report tables**, not raw transcripts or workspace diffs.
146
154
 
147
155
  ### Which mode should I use?
148
156
 
149
157
  | If you… | Use | Why |
150
158
  |---|---|---|
151
- | Building new social features with an AI agent | **Vise CLI + Skill** | The only mode that reliably delivers all features correctly |
159
+ | Building new social features with an AI agent | **Vise CLI + Skill** | The mode that closed every secondary-compliance gap in our benchmark |
152
160
  | Auditing existing social.plus code | `vise check --ci` | Grades any codebase against the full ruleset |
153
161
  | Enforcing compliance in a CI pipeline | `vise check --ci` | Exits non-zero on failures; structured JSON output for logs |
154
162
 
@@ -162,7 +170,7 @@ Vise delivers all 9 features correctly in a single session. The other conditions
162
170
  | **React Native** | ✅ Full | `tsc`, `npm lint`, SDK import smoke |
163
171
  | **Flutter / Dart** | ✅ Full | `flutter analyze`, `flutter test` |
164
172
  | **Android (Kotlin)** | ✅ Full | Gradle assemble, unit tests |
165
- | **iOS (Swift)** | ✅ Full | (static rule checks; runtime sensors WIP) |
173
+ | **iOS (Swift)** | ✅ Full | Static rule checks fully operational. Build sensor not wired (`xcodebuild` environment requirements make it fragile) — `vise run-sensors` returns no-sensors for iOS; compliance rules run regardless. |
166
174
 
167
175
  Each platform has 52–54 rules across 10 compliance domains (feed, comments, moderation, chat, secrets, session & auth, notifications, live objects, logging hygiene, design tokens).
168
176
 
@@ -251,6 +259,7 @@ The flow above is what the skill teaches your AI agent. You — the human — dr
251
259
  | `vise design check [path]` | Advisory, **non-blocking** report on how closely the UI code matches the contract (token coverage + on/off-contract color literals). Never fails a build and is **not** a `vise check` gate |
252
260
  | `vise design preview [path] [--reference <prototype>]` | Write a self-contained `sp-vise/design-preview.html`: the contract's tokens as visual swatches + the conformance report + the HTML reference embedded for side-by-side review. Vise renders the artifact; a human/VLM judges the visual match. Dependency-free — **not** an automated pixel diff |
253
261
  | `vise design reference [path] [--title <name>]` | Write a self-contained `sp-vise/design-reference.html`: human/VLM-readable design-system spec — token swatches, type samples, component demos, and a growth-layer summary. Pairs with `design-contract.json` (machine-readable). Use `--title` to name the design system (e.g. `--title Streamly`). Advisory — **not** an enforcement gate |
262
+ | `vise design init-tokens [path] [--force]` | Scaffold `src/styles/social-plus-tokens.css` — the dedicated, customer-editable token file for social.plus features. **Greenfield:** neutral defaults (full `--sp-*` token set). **Brownfield:** seeded from your existing concrete tokens. Idempotent — never overwrites an existing file (use `--force` to override). After editing, run `vise design extract --from-project` to refresh the contract |
254
263
 
255
264
  The extracted contract is **advisory input for generation**, not an enforcement gate: a token-poor prototype yields a weaker — never wrong — contract, and absence of a prototype simply means no contract (the existing `*.design.reuse-detected-tokens` rules still cover reuse of a host project's own design system).
256
265
 
@@ -278,7 +287,7 @@ The extracted contract is **advisory input for generation**, not an enforcement
278
287
 
279
288
  | Command | Purpose |
280
289
  |---|---|
281
- | `vise run-sensors [path]` | Run detected project commands (npm scripts, Gradle, Flutter, lint, typecheck, SDK import smokes); never executes arbitrary shell |
290
+ | `vise run-sensors [path]` | Run detected project scripts/wrappers (npm scripts, Gradle, Flutter, lint, typecheck, SDK import smokes); inspect with `--dry-run` before running in an untrusted project |
282
291
  | `vise run-sensors [path] --dry-run` | List what would run without executing |
283
292
 
284
293
  ### Troubleshooting quick loop
@@ -334,7 +343,7 @@ MCP-capable hosts can call Vise as structured tool calls instead of shell comman
334
343
 
335
344
  ### Tool names (snake_case per MCP convention)
336
345
 
337
- `inspect_project`, `plan_harness`, `plan_integration`, `init_compliance`, `check_compliance`, `sync_compliance`, `attest_rule`, `explain_rule`, `init_engagement`, `show_engagement`, `search_docs`, `get_doc_page`, `debug_issue`, `validate_setup`, `run_sensors`.
346
+ `inspect_project`, `plan_harness`, `plan_integration`, `init_compliance`, `check_compliance`, `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`.
338
347
 
339
348
  These are the same operations as the CLI commands above, exposed as MCP tools.
340
349
 
@@ -387,6 +396,8 @@ After `vise init`, your project gets a `sp-vise/` directory. These files become
387
396
  | `sp-vise/compliance.json` | `vise init` | The rules selected for this integration, the Vise version, the ruleset digest, the target app surface, and an optional engagement link. |
388
397
  | `sp-vise/attestations/*.json` | `vise sync` (deterministic) or `vise attest` (host-agent / human) | Per-rule evidence: signer, confidence, rationale, cited files (with source fingerprints for drift detection). |
389
398
  | `sp-vise/inspection.json` | `vise init` | The platform, monorepo surface, and design-token signals detected at init time. |
399
+ | `sp-vise/design-contract.json` | `vise design extract` | The extracted design contract: declared tokens, breakpoints, advisory components, source file digests (for freshness detection), and a stable digest over design facts. |
400
+ | `sp-vise/design-reference.html` | `vise design reference` | Self-contained HTML design-system spec (token swatches, type samples, components). Human/VLM-readable; open in a browser alongside the app. |
390
401
  | `sp-vise/engagement.json` | `vise engagement init` (optional) | Contractual scope: tier, customer ID, contracted outcomes, reviewer assignment. |
391
402
 
392
403
  **Commit `sp-vise/` to your repo.** `vise check` re-validates against the recorded contract on every run, comparing current code against the recorded attestations. If code changes and breaks a rule, the next `check` reports `deterministic-fail`, `attestation-needed`, or `blocked` — never a silent regression.
package/dist/server.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { copyFile, mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
2
+ import { copyFile, mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
@@ -7,7 +7,7 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
7
7
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8
8
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
9
9
  import { attestRule, attestRuleTool, checkCompliance, checkComplianceTool, explainRule, explainRuleTool, initCompliance, initComplianceTool, initEngagement, initEngagementTool, showEngagement, showEngagementTool, statusCompliance, syncCompliance, syncComplianceTool, } from "./tools/compliance.js";
10
- import { designCheckTool, designExtractTool, designPreviewTool, designReferenceTool } from "./tools/design.js";
10
+ import { designCheckTool, designExtractTool, designInitTokensTool, designPreviewTool, designReferenceTool } from "./tools/design.js";
11
11
  import { getDocPageTool, searchDocsTool } from "./tools/docs.js";
12
12
  import { planHarnessTool } from "./tools/harness.js";
13
13
  import { planIntegrationTool } from "./tools/integration.js";
@@ -38,8 +38,13 @@ const tools = new Map([
38
38
  designCheckTool,
39
39
  designPreviewTool,
40
40
  designReferenceTool,
41
+ designInitTokensTool,
41
42
  ].map((tool) => [tool.name, tool]));
42
43
  const bundledSkillName = "social-plus-vise";
44
+ // Pre-rebrand `install-skill` runs created skill dirs/files under this name. We
45
+ // supersede (remove) them on install so a host doesn't keep serving stale
46
+ // foundry/spf guidance alongside the current skill.
47
+ const legacySkillName = "social-plus-foundry";
43
48
  const cliResult = await handleCli(process.argv.slice(2));
44
49
  if (cliResult === "exit") {
45
50
  process.exitCode = process.exitCode ?? 0;
@@ -305,7 +310,15 @@ async function handleCli(args) {
305
310
  });
306
311
  return "exit";
307
312
  }
308
- console.error(`Unknown design subcommand: ${sub ?? "(none)"}. Expected "extract", "check", "preview", or "reference".`);
313
+ if (sub === "init-tokens") {
314
+ assertOnlyKnownFlags(subArgs, ["force"], "design init-tokens");
315
+ await printToolResult(designInitTokensTool, {
316
+ repoPath: positionalRepoPath(subArgs),
317
+ force: hasFlag(subArgs, "force"),
318
+ });
319
+ return "exit";
320
+ }
321
+ console.error(`Unknown design subcommand: ${sub ?? "(none)"}. Expected "extract", "check", "preview", "reference", or "init-tokens".`);
309
322
  process.exitCode = 1;
310
323
  return "exit";
311
324
  }
@@ -515,6 +528,7 @@ Usage:
515
528
  vise design check [repoPath]
516
529
  vise design preview [repoPath] [--reference <prototypePath>]
517
530
  vise design reference [repoPath] [--title "Streamly"] [--no-write]
531
+ vise design init-tokens [repoPath] [--force]
518
532
 
519
533
  extract Build a graded design contract and write it to sp-vise/design-contract.json.
520
534
  Declared CSS custom properties become exact tokens; repeated literal values
@@ -535,7 +549,12 @@ preview Write a self-contained sp-vise/design-preview.html: the contract's to
535
549
  reference Write a self-contained sp-vise/design-reference.html: human/VLM-readable
536
550
  design-system spec (token swatches, type samples, component demos, growth-layer
537
551
  summary). Pairs with the machine-readable design-contract.json. Use --title to
538
- set the design system name. Advisory — not an enforcement gate.`;
552
+ set the design system name. Advisory — not an enforcement gate.
553
+ init-tokens Scaffold src/styles/social-plus-tokens.css in the customer's project —
554
+ the dedicated editable token file for social.plus features. Greenfield:
555
+ neutral defaults. Brownfield (existing tokens found): seeded from their
556
+ concrete values. Idempotent (never overwrites); use --force to overwrite.
557
+ After editing, run vise design extract --from-project to refresh the contract.`;
539
558
  }
540
559
  return `${packageName}
541
560
 
@@ -593,6 +612,7 @@ async function installSkill(args) {
593
612
  }
594
613
  const destination = skillInstallDestination(args);
595
614
  const force = hasFlag(args, "force");
615
+ const supersededLegacy = await removeLegacySkillDir(destination);
596
616
  const installedFiles = await copyDirectory(source, destination, force);
597
617
  return {
598
618
  status: installedFiles.length > 0 ? "installed" : "already-current",
@@ -601,13 +621,36 @@ async function installSkill(args) {
601
621
  destination,
602
622
  force,
603
623
  installedFiles,
624
+ supersededLegacy,
604
625
  nextStep: "Restart or reload the host AI coding tool so it discovers the installed skill.",
605
626
  };
606
627
  }
628
+ // Supersede a pre-rebrand skill install: a prior `install-skill` under the old
629
+ // package name created a sibling `<skillsRoot>/social-plus-foundry/` directory. Left
630
+ // in place, the host keeps offering stale spf/foundry guidance next to the current
631
+ // skill. Remove it on install — but only when it actually looks like a skill dir
632
+ // (contains SKILL.md) and is not the directory we're installing into.
633
+ async function removeLegacySkillDir(destination) {
634
+ const legacyDir = path.join(path.dirname(destination), legacySkillName);
635
+ if (legacyDir === destination) {
636
+ return [];
637
+ }
638
+ if (!(await fileExists(path.join(legacyDir, "SKILL.md")))) {
639
+ return [];
640
+ }
641
+ await rm(legacyDir, { recursive: true, force: true });
642
+ return [legacyDir];
643
+ }
607
644
  async function installInstructionFile(target, args) {
608
645
  const force = hasFlag(args, "force");
609
646
  const source = path.join(skillSourceDir(), "SKILL.md");
610
647
  const content = await readFile(source, "utf8");
648
+ const legacyRule = path.join(path.dirname(target.destination), `${legacySkillName}.mdc`);
649
+ const supersededLegacy = [];
650
+ if (legacyRule !== target.destination && (await fileExists(legacyRule))) {
651
+ await rm(legacyRule, { force: true });
652
+ supersededLegacy.push(legacyRule);
653
+ }
611
654
  if (await fileExists(target.destination)) {
612
655
  const existing = await readFile(target.destination, "utf8");
613
656
  if (existing === content) {
@@ -619,6 +662,7 @@ async function installInstructionFile(target, args) {
619
662
  destination: target.destination,
620
663
  force,
621
664
  installedFiles: [],
665
+ supersededLegacy,
622
666
  nextStep: "Restart or reload the host AI coding tool so it discovers the updated project instructions.",
623
667
  };
624
668
  }
@@ -636,6 +680,7 @@ async function installInstructionFile(target, args) {
636
680
  destination: target.destination,
637
681
  force,
638
682
  installedFiles: [target.destination],
683
+ supersededLegacy,
639
684
  nextStep: "Restart or reload the host AI coding tool so it discovers the updated project instructions.",
640
685
  };
641
686
  }
@@ -876,7 +921,24 @@ function optionalNumberFlag(args, name) {
876
921
  return number;
877
922
  }
878
923
  function hasFlag(args, name) {
879
- return args.includes(`--${name}`);
924
+ const exact = `--${name}`;
925
+ const equalsPrefix = `${exact}=`;
926
+ for (const arg of args) {
927
+ if (arg === exact) {
928
+ return true;
929
+ }
930
+ if (arg.startsWith(equalsPrefix)) {
931
+ const raw = arg.slice(equalsPrefix.length).trim().toLowerCase();
932
+ if (raw === "" || raw === "true" || raw === "1" || raw === "yes") {
933
+ return true;
934
+ }
935
+ if (raw === "false" || raw === "0" || raw === "no") {
936
+ return false;
937
+ }
938
+ throw new Error(`--${name} must be a boolean when provided with "=".`);
939
+ }
940
+ }
941
+ return false;
880
942
  }
881
943
  function keyValueFlag(args, name) {
882
944
  const pairs = flagValues(args, name);
package/dist/tools/ast.js CHANGED
@@ -10,10 +10,46 @@
10
10
  * Scope: Single-file, single-step identifier resolution only.
11
11
  * No cross-file imports, no type inference, no function boundary traversal.
12
12
  */
13
- import Parser from "tree-sitter";
14
- import TypeScriptGrammars from "tree-sitter-typescript";
15
- import KotlinGrammar from "tree-sitter-kotlin";
16
- const { typescript: tsGrammar, tsx: tsxGrammar } = TypeScriptGrammars;
13
+ import { createRequire } from "node:module";
14
+ const nodeRequire = createRequire(import.meta.url);
15
+ // Lazily and defensively load the tree-sitter native bindings. tree-sitter ships
16
+ // prebuilt binaries for common platforms (darwin, linux-x64, win32-x64); on others
17
+ // (linux-arm64, Alpine/musl, win32-arm64) the binding can fail to load when no C++
18
+ // toolchain is present. AST is an ADDITIVE layer over the regex validators, so a
19
+ // load failure must degrade to regex-only — NOT take down the entire CLI (including
20
+ // doc-search/compliance commands that never touch a parser) at import time. Static
21
+ // top-level imports would throw at module load and brick every command; this loader
22
+ // confines the failure to the AST path. `undefined` = not yet attempted; `null` =
23
+ // attempted and unavailable.
24
+ let nativeBindings;
25
+ function loadNativeBindings() {
26
+ if (nativeBindings !== undefined)
27
+ return nativeBindings;
28
+ try {
29
+ const ParserCtor = nodeRequire("tree-sitter");
30
+ const tsGrammars = nodeRequire("tree-sitter-typescript");
31
+ const kotlinGrammar = nodeRequire("tree-sitter-kotlin");
32
+ nativeBindings = {
33
+ Parser: ParserCtor,
34
+ tsGrammar: tsGrammars.typescript,
35
+ tsxGrammar: tsGrammars.tsx,
36
+ kotlinGrammar,
37
+ };
38
+ }
39
+ catch {
40
+ nativeBindings = null;
41
+ }
42
+ return nativeBindings;
43
+ }
44
+ /**
45
+ * Whether tree-sitter native bindings are available in this environment. When
46
+ * false, every AST helper degrades gracefully: parse() throws (so tryParse()
47
+ * returns null and stripComments() returns the source unchanged), and validators
48
+ * fall back to their regex paths.
49
+ */
50
+ export function astAvailable() {
51
+ return loadNativeBindings() !== null;
52
+ }
17
53
  /**
18
54
  * Strip comments from source code using tree-sitter AST.
19
55
  * Replaces comment spans with whitespace (preserving line structure).
@@ -56,13 +92,17 @@ const parsers = new Map();
56
92
  function getParser(language) {
57
93
  let parser = parsers.get(language);
58
94
  if (!parser) {
59
- parser = new Parser();
95
+ const native = loadNativeBindings();
96
+ if (!native) {
97
+ throw new Error("tree-sitter native bindings unavailable; AST analysis disabled (regex fallback in effect)");
98
+ }
99
+ parser = new native.Parser();
60
100
  if (language === "tsx")
61
- parser.setLanguage(tsxGrammar);
101
+ parser.setLanguage(native.tsxGrammar);
62
102
  else if (language === "kotlin")
63
- parser.setLanguage(KotlinGrammar);
103
+ parser.setLanguage(native.kotlinGrammar);
64
104
  else
65
- parser.setLanguage(tsGrammar);
105
+ parser.setLanguage(native.tsGrammar);
66
106
  parsers.set(language, parser);
67
107
  }
68
108
  return parser;
@@ -188,6 +188,7 @@ export async function initEngagement(args) {
188
188
  : undefined;
189
189
  const engagement = {
190
190
  schema_version: schemaVersion,
191
+ vise_version: packageVersion,
191
192
  foundry_version: packageVersion,
192
193
  engagement_id: randomUUID(),
193
194
  customer_id: args.customerId,
@@ -235,6 +236,7 @@ export async function initCompliance(repoPath, request, surfacePath) {
235
236
  const designContract = await readDesignContract(repoRoot);
236
237
  const compliance = {
237
238
  schema_version: schemaVersion,
239
+ vise_version: packageVersion,
238
240
  foundry_version: packageVersion,
239
241
  ruleset_digest: digestJson(refs), // hash of minimal refs (no title)
240
242
  generated_at: new Date().toISOString(),
@@ -688,6 +690,7 @@ function buildAttestation(compliance, rule, signer, confidence, identity, ration
688
690
  rule_version: rule.version,
689
691
  rule_digest: ref.rule_digest,
690
692
  ruleset_digest: compliance.ruleset_digest,
693
+ vise_version: packageVersion,
691
694
  foundry_version: packageVersion,
692
695
  status: signer === "spf-deterministic" ? "deterministic-pass" : "attested",
693
696
  signer_claim: {
@@ -974,7 +977,7 @@ function sidecarReadme(compliance) {
974
977
  "## Quick start",
975
978
  "",
976
979
  "1. Read `findings.json` — it contains a snapshot of rule status taken at init time, including any violations found in the current code.",
977
- "2. Fix the issues listed in `findings.json`, then run `npm run sp-check` (or `vise check .` if vise is on PATH) to verify.",
980
+ "2. Fix the issues listed in `findings.json`, then run `vise check .` to verify.",
978
981
  "3. Run `vise sync .` to persist deterministic-pass evidence once rules are green.",
979
982
  "4. Run `vise attest . --rule <rule-id> ...` to sign off on intentional implementation decisions.",
980
983
  "",
@@ -157,7 +157,16 @@ export async function extractDesignContractFromProject(repoPath) {
157
157
  }
158
158
  }
159
159
  inputs.sort();
160
- return buildDesignContract({ css, html: [], inputs }, { kind: "host-project", inputs, file_count: inputs.length }, moduleTokens);
160
+ // Hash each source file so design check can detect staleness without re-extracting.
161
+ const input_digests = {};
162
+ for (const rel of inputs) {
163
+ try {
164
+ const content = await readFile(path.join(root, rel), "utf8");
165
+ input_digests[rel] = `sha256:${createHash("sha256").update(content).digest("hex")}`;
166
+ }
167
+ catch { /* file read already succeeded above; defensive only */ }
168
+ }
169
+ return buildDesignContract({ css, html: [], inputs }, { kind: "host-project", inputs, input_digests, file_count: inputs.length }, moduleTokens);
161
170
  }
162
171
  export async function writeDesignContract(repoPath, contract) {
163
172
  const sidecarDir = path.join(path.resolve(repoPath), "sp-vise");
@@ -381,6 +390,215 @@ function safeCss(value) {
381
390
  return value.replace(/[<>"]/g, "").slice(0, 200);
382
391
  }
383
392
  // ---------------------------------------------------------------------------
393
+ // Social-plus token scaffold (vise design init-tokens)
394
+ // ---------------------------------------------------------------------------
395
+ //
396
+ // Creates a dedicated `src/styles/social-plus-tokens.css` in the customer's
397
+ // project — the single editable source for social.plus feature styling.
398
+ // The contract always points at this file; customers edit it freely without
399
+ // needing an AI agent. Design check detects changes and prompts re-extract.
400
+ //
401
+ // Two cases:
402
+ // Greenfield (no existing design system): scaffold Option-B neutral defaults.
403
+ // Brownfield (existing tokens found): seed from their concrete values.
404
+ // Already exists: leave it untouched (idempotent — never clobbers edits).
405
+ /** Relative path within the customer's project where sp tokens live. */
406
+ export const SP_TOKENS_PATH = "src/styles/social-plus-tokens.css";
407
+ /** Option-B neutral default — a clean, adaptive light-mode system using system
408
+ * fonts. All token names use `--sp-` prefix to avoid collision with the
409
+ * customer's own design system. Will be replaced with the official social.plus
410
+ * palette (Option A) once that palette is finalised. */
411
+ export const NEUTRAL_SP_TOKENS_DEFAULT = `/* social-plus-tokens.css — social.plus feature design system.
412
+ * This file controls the look of all social.plus features in your app.
413
+ * Edit freely. Run: vise design extract --from-project . to refresh the contract.
414
+ * NOTE: design check scans the whole project; the main-app palette appearing as
415
+ * "off-contract" is expected and advisory only — not a failure. */
416
+ :root {
417
+ /* ── Brand / interactive ───────────────────────────────── */
418
+ --sp-color-brand: #1054DE;
419
+ --sp-color-brand-hover: #0D47C5;
420
+ --sp-color-brand-subtle: #EEF3FF;
421
+ --sp-color-brand-text: #FFFFFF;
422
+
423
+ /* ── Backgrounds ───────────────────────────────────────── */
424
+ --sp-color-bg: #FFFFFF;
425
+ --sp-color-surface: #F8F9FA;
426
+ --sp-color-surface-raised: #FFFFFF;
427
+ --sp-color-surface-hover: #F0F2F4;
428
+ --sp-color-overlay: rgba(0, 0, 0, 0.5);
429
+
430
+ /* ── Text ──────────────────────────────────────────────── */
431
+ --sp-color-text: #0D1017;
432
+ --sp-color-text-muted: #5C6370;
433
+ --sp-color-text-faint: #9AA0AD;
434
+ --sp-color-text-disabled: #C1C7CE;
435
+ --sp-color-text-on-brand: #FFFFFF;
436
+
437
+ /* ── Semantic ──────────────────────────────────────────── */
438
+ --sp-color-success: #1FAF64;
439
+ --sp-color-warning: #F59E0B;
440
+ --sp-color-error: #EF4444;
441
+ --sp-color-info: #3B82F6;
442
+
443
+ /* ── Border ────────────────────────────────────────────── */
444
+ --sp-color-border: #E5E7EB;
445
+ --sp-color-border-strong: #D1D5DB;
446
+
447
+ /* ── Typography ────────────────────────────────────────── */
448
+ --sp-font-body: system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
449
+ --sp-font-display: system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
450
+ --sp-fs-xs: 12px;
451
+ --sp-fs-sm: 14px;
452
+ --sp-fs-md: 16px;
453
+ --sp-fs-lg: 20px;
454
+ --sp-fs-xl: 24px;
455
+ --sp-fs-2xl: 32px;
456
+ --sp-fw-regular: 400;
457
+ --sp-fw-medium: 500;
458
+ --sp-fw-bold: 700;
459
+ --sp-lh-tight: 1.2;
460
+ --sp-lh-normal: 1.5;
461
+ --sp-lh-relaxed: 1.7;
462
+ --sp-ls-tight: -0.01em;
463
+ --sp-ls-wide: 0.04em;
464
+
465
+ /* ── Spacing ───────────────────────────────────────────── */
466
+ --sp-space-1: 4px;
467
+ --sp-space-2: 8px;
468
+ --sp-space-3: 12px;
469
+ --sp-space-4: 16px;
470
+ --sp-space-5: 24px;
471
+ --sp-space-6: 32px;
472
+ --sp-space-8: 48px;
473
+ --sp-space-10: 64px;
474
+
475
+ /* ── Radius ────────────────────────────────────────────── */
476
+ --sp-radius-sm: 6px;
477
+ --sp-radius-md: 10px;
478
+ --sp-radius-lg: 16px;
479
+ --sp-radius-xl: 24px;
480
+ --sp-radius-pill: 999px;
481
+
482
+ /* ── Border width ──────────────────────────────────────── */
483
+ --sp-border-width-thin: 1px;
484
+ --sp-border-width-base: 2px;
485
+ --sp-border-width-thick: 4px;
486
+
487
+ /* ── Elevation ─────────────────────────────────────────── */
488
+ --sp-shadow-1: 0 1px 3px rgba(0, 0, 0, 0.08);
489
+ --sp-shadow-2: 0 4px 16px rgba(0, 0, 0, 0.10);
490
+ --sp-shadow-3: 0 16px 40px rgba(0, 0, 0, 0.14);
491
+
492
+ /* ── Opacity ───────────────────────────────────────────── */
493
+ --sp-opacity-disabled: 0.4;
494
+ --sp-opacity-muted: 0.7;
495
+
496
+ /* ── Motion ────────────────────────────────────────────── */
497
+ --sp-duration-fast: 120ms;
498
+ --sp-duration-base: 200ms;
499
+ --sp-duration-slow: 400ms;
500
+ --sp-ease-standard: cubic-bezier(0.4, 0, 0.2, 1);
501
+ --sp-ease-emphasized:cubic-bezier(0.2, 0, 0, 1);
502
+
503
+ /* ── Sizing ────────────────────────────────────────────── */
504
+ --sp-size-icon-sm: 16px;
505
+ --sp-size-icon-md: 24px;
506
+ --sp-size-icon-lg: 32px;
507
+ --sp-control-height-sm: 32px;
508
+ --sp-control-height-md: 40px;
509
+ --sp-control-height-lg: 48px;
510
+
511
+ /* ── Z-index ───────────────────────────────────────────── */
512
+ --sp-z-nav: 10;
513
+ --sp-z-dropdown: 100;
514
+ --sp-z-modal: 1000;
515
+ --sp-z-toast: 2000;
516
+
517
+ /* ── Breakpoints ───────────────────────────────────────── */
518
+ --sp-bp-sm: 640px;
519
+ --sp-bp-md: 768px;
520
+ --sp-bp-lg: 1024px;
521
+ --sp-bp-xl: 1280px;
522
+ }
523
+ `;
524
+ export const designInitTokensTool = {
525
+ name: "design_init_tokens",
526
+ description: "Scaffold src/styles/social-plus-tokens.css in the customer's project — the dedicated, customer-editable token file for social.plus features. Greenfield: neutral defaults. Brownfield (existing tokens found): seed from their concrete values. Idempotent: never overwrites an existing file.",
527
+ inputSchema: {
528
+ type: "object",
529
+ properties: {
530
+ repoPath: { type: "string", description: "Project root. Defaults to the current directory." },
531
+ force: { type: "boolean", description: "Overwrite existing social-plus-tokens.css (default false — never clobbers)." },
532
+ },
533
+ additionalProperties: false,
534
+ },
535
+ async call(input) {
536
+ const args = objectInput(input);
537
+ const repoPath = optionalStringField(args, "repoPath") ?? ".";
538
+ const force = args.force === true;
539
+ return textResult(await initSpTokens(repoPath, force));
540
+ },
541
+ };
542
+ export async function initSpTokens(repoPath, force = false) {
543
+ const root = path.resolve(repoPath);
544
+ const target = path.join(root, SP_TOKENS_PATH);
545
+ // Idempotent: don't overwrite unless forced.
546
+ try {
547
+ await stat(target);
548
+ if (!force) {
549
+ return {
550
+ status: "exists",
551
+ file: target,
552
+ message: `${SP_TOKENS_PATH} already exists — skipping. Use --force to overwrite.`,
553
+ };
554
+ }
555
+ }
556
+ catch { /* file doesn't exist — proceed to scaffold */ }
557
+ // Try brownfield seeding: extract concrete token values from the existing project.
558
+ const existingContract = await extractDesignContractFromProject(root);
559
+ const hasBrownfieldTokens = existingContract.tokens.length > 0;
560
+ let css;
561
+ let seededFrom;
562
+ if (hasBrownfieldTokens) {
563
+ // Seed from their existing concrete values, namespaced as --sp-*.
564
+ const lines = [
565
+ `/* social-plus-tokens.css — social.plus feature design system.`,
566
+ ` * Seeded from your existing design tokens on ${new Date().toISOString().slice(0, 10)}.`,
567
+ ` * Edit freely to customize social.plus features independently from your main app.`,
568
+ ` * Run: vise design extract --from-project . to refresh the contract. */`,
569
+ `:root {`,
570
+ ];
571
+ const categories = [...new Set(existingContract.tokens.map((t) => t.category))];
572
+ for (const cat of categories) {
573
+ const tokensInCat = existingContract.tokens.filter((t) => t.category === cat && t.name);
574
+ if (tokensInCat.length === 0)
575
+ continue;
576
+ lines.push(``, ` /* ── ${cat} ──────────────────────────────────────────── */`);
577
+ for (const t of tokensInCat) {
578
+ const spName = `--sp-${t.name.replace(/^--/, "")}`;
579
+ lines.push(` ${spName}: ${t.value}; /* seeded from ${t.name} */`);
580
+ }
581
+ }
582
+ lines.push(`}`);
583
+ css = lines.join("\n") + "\n";
584
+ seededFrom = existingContract.source.inputs;
585
+ }
586
+ else {
587
+ // Greenfield: neutral Option-B defaults.
588
+ css = NEUTRAL_SP_TOKENS_DEFAULT;
589
+ }
590
+ await mkdir(path.dirname(target), { recursive: true });
591
+ await writeFile(target, css, "utf8");
592
+ return {
593
+ status: hasBrownfieldTokens ? "seeded" : "scaffolded",
594
+ file: target,
595
+ ...(seededFrom ? { seeded_from: seededFrom } : {}),
596
+ message: hasBrownfieldTokens
597
+ ? `Seeded ${SP_TOKENS_PATH} from your existing design tokens. Edit it to customize social.plus features, then run vise design extract --from-project .`
598
+ : `Scaffolded ${SP_TOKENS_PATH} with neutral defaults. Fill in your colors, then run vise design extract --from-project .`,
599
+ };
600
+ }
601
+ // ---------------------------------------------------------------------------
384
602
  // Design-system reference document (human/VLM-readable, advisory)
385
603
  // ---------------------------------------------------------------------------
386
604
  //
@@ -927,6 +1145,9 @@ export async function runDesignCheck(repoPath) {
927
1145
  note: ADVISORY_NOTE,
928
1146
  };
929
1147
  }
1148
+ // Freshness check: compare source file content hashes to those recorded at extract time.
1149
+ // Advisory only — never blocks, just surfaces a nudge to re-extract.
1150
+ const staleContract = await checkContractFreshness(repoRoot, contract);
930
1151
  const files = (await collectFiles(repoRoot, MAX_SCAN_FILES)).filter((file) => SCAN_EXTS.has(path.extname(file).toLowerCase()));
931
1152
  if (files.length === 0) {
932
1153
  return { status: "no-sources", message: "No UI source files found to check against the contract.", contract: contractSummary(contract), note: ADVISORY_NOTE };
@@ -1018,9 +1239,35 @@ export async function runDesignCheck(repoPath) {
1018
1239
  count: undefinedRefs.length,
1019
1240
  sample: undefinedRefs.slice(0, OFF_CONTRACT_SAMPLE),
1020
1241
  },
1242
+ ...(staleContract ? { staleContract } : {}),
1021
1243
  note: ADVISORY_NOTE,
1022
1244
  };
1023
1245
  }
1246
+ /** Compare source.inputs file content against hashes recorded at extract time.
1247
+ * Returns null if the contract is fresh or has no recorded digests. */
1248
+ async function checkContractFreshness(repoRoot, contract) {
1249
+ const recorded = contract.source?.input_digests;
1250
+ if (!recorded || Object.keys(recorded).length === 0)
1251
+ return undefined;
1252
+ const changed = [];
1253
+ for (const [rel, storedDigest] of Object.entries(recorded)) {
1254
+ try {
1255
+ const content = await readFile(path.join(repoRoot, rel), "utf8");
1256
+ const currentDigest = `sha256:${createHash("sha256").update(content).digest("hex")}`;
1257
+ if (currentDigest !== storedDigest)
1258
+ changed.push(rel);
1259
+ }
1260
+ catch {
1261
+ changed.push(rel); // file deleted or unreadable — also stale
1262
+ }
1263
+ }
1264
+ if (changed.length === 0)
1265
+ return undefined;
1266
+ return {
1267
+ changedFiles: changed,
1268
+ hint: `Run \`vise design extract --from-project\` to refresh the contract against the updated file(s).`,
1269
+ };
1270
+ }
1024
1271
  function dedupeByToken(refs) {
1025
1272
  const seen = new Set();
1026
1273
  const out = [];
@@ -1323,6 +1570,7 @@ export function buildDesignContract(sources, sourceMeta, extraDeclaredTokens = [
1323
1570
  const inferredCount = tokens.filter((token) => token.provenance === "inferred").length;
1324
1571
  const contract = {
1325
1572
  schema_version: DESIGN_CONTRACT_SCHEMA_VERSION,
1573
+ vise_version: packageVersion,
1326
1574
  foundry_version: packageVersion,
1327
1575
  source: sourceMeta,
1328
1576
  digest: "",
@@ -210,7 +210,7 @@ function assessHarnessability(platforms, commandSensors, designSignalCount) {
210
210
  else {
211
211
  gaps.push("No platform signals detected; ask the user for the app framework or repository root.");
212
212
  }
213
- if (platforms.some((platform) => ["typescript", "react-native", "android", "flutter"].includes(platform))) {
213
+ if (platforms.some((platform) => ["typescript", "react-native", "android", "flutter", "ios"].includes(platform))) {
214
214
  affordances.push("Detected a platform with deterministic setup checks available in Vise.");
215
215
  }
216
216
  if (commandSensors.length > 0) {
@@ -223,7 +223,7 @@ function assessHarnessability(platforms, commandSensors, designSignalCount) {
223
223
  affordances.push(`Detected ${designSignalCount} design/theme signal(s) for UI integration grounding.`);
224
224
  }
225
225
  if (platforms.includes("ios")) {
226
- gaps.push("iOS support is guided until deterministic validators are expanded.");
226
+ gaps.push("iOS: static compliance rules are fully operational. No build/compile sensor is wired yet (xcodebuild environment requirements make it fragile); run-sensors will return no-sensors for iOS projects.");
227
227
  }
228
228
  if (platforms.length === 0) {
229
229
  return { level: "weak", affordances, gaps };
@@ -74,7 +74,7 @@ async function inspectRoot(root) {
74
74
  }
75
75
  // When react-native is detected alongside generic typescript signals, prefer react-native
76
76
  // so that platform-specific rules (react-native.*) are used for init/check/run-sensors.
77
- // Same for android: an agent may create package.json (e.g. to enable npm run sp-check) which
77
+ // Same for android: an agent may create package.json (e.g. to enable a local Vise check script) which
78
78
  // would normally trigger typescript detection — suppress it so only android rules apply.
79
79
  const rawPlatforms = Array.from(new Set(signals.map((signal) => signal.platform)));
80
80
  const hasRN = rawPlatforms.includes("react-native");
@@ -1118,9 +1118,11 @@ function validateChat(root, platform, sourceContent) {
1118
1118
  }
1119
1119
  }
1120
1120
  }
1121
- // channel-target-resolved: check for hardcoded channelId/conversationId
1121
+ // channel-target-resolved: check for hardcoded channelId/conversationId.
1122
+ // Comment-stripped so a commented-out or documented channelId can't trip this
1123
+ // no-escape (exit-2) gate.
1122
1124
  for (const filePath of chatFiles) {
1123
- const content = sourceContent.get(filePath) ?? "";
1125
+ const content = commentStripped(filePath, platform, sourceContent.get(filePath) ?? "");
1124
1126
  const hardcodedChannel = /(?:channelId|conversationId|channel_id)\b[^=\n]*=\s*["'`][a-z0-9-]+["'`]/i.exec(content);
1125
1127
  if (hardcodedChannel) {
1126
1128
  findings.push(finding(`${platform}.chat.channel-target-resolved`, "error", "Chat code references a hardcoded channelId or conversationId.", relativeFile(root, filePath), "Resolve the channel from user selection, SDK query, or app routing — never hardcode."));
@@ -1504,7 +1506,7 @@ function validateLiteralGuardrails(root, platform, sourceContent) {
1504
1506
  /\btargetId\b\s*[:=]\s*["'`]([^"'`]+)["'`]/i,
1505
1507
  /\bfeedId\b\s*[:=]\s*["'`]([^"'`]+)["'`]/i,
1506
1508
  /\bchannelId\b\s*[:=]\s*["'`]([^"'`]+)["'`]/i,
1507
- ]);
1509
+ ], platform);
1508
1510
  if (feedTarget && !isAllowedPlaceholder(feedTarget.value)) {
1509
1511
  findings.push(finding(`${platform}.feed.target.literal`, "warning", `A hardcoded feed target literal was found: ${feedTarget.name}.`, relativeFile(root, feedTarget.file), "Do not invent or hardcode communityId, targetId, feedId, or channelId. Ask the user for the target or use an existing app-owned selection/create flow."));
1510
1512
  }
@@ -1531,7 +1533,7 @@ function validateLiteralGuardrails(root, platform, sourceContent) {
1531
1533
  /\bapi[-_]?key\b\s*[:=][\s\S]{0,200}?(?:process\.env\.[A-Z0-9_]+|import\.meta\.env\.[A-Z0-9_]+)\s*(?:\?\?|\|\|)\s*["'`]([^"'`]+)["'`]/i,
1532
1534
  // Ternary fallback: `apiKey = X ? 'literal' : ...` captures the truthy branch.
1533
1535
  /\bapi[-_]?key\b\s*[:=][\s\S]{0,200}?\?\s*["'`]([^"'`]+)["'`]\s*:/i,
1534
- ]);
1536
+ ], platform);
1535
1537
  if (inlineApiKey && !isAllowedPlaceholder(inlineApiKey.value)) {
1536
1538
  findings.push(finding(`${platform}.secret.inline-api-key`, "warning", "A social.plus API key appears to be hardcoded in source.", relativeFile(root, inlineApiKey.file), "Use the host app's environment/config pattern instead of committing API keys directly into source files. The literal is still committed even when wrapped in an env-fallback (e.g. `defaultValue:`, `??`, `||`, ternary)."));
1537
1539
  }
@@ -1543,7 +1545,7 @@ function validateLiteralGuardrails(root, platform, sourceContent) {
1543
1545
  /\buser_id\s*[:=]\s*["'`]([^"'`]+)["'`]/i,
1544
1546
  /\.login\s*\(\s*["'`]([^"'`]+)["'`]/i,
1545
1547
  /\.login\s*\(\s*userId\s*:\s*["'`]([^"'`]+)["'`]/i,
1546
- ]);
1548
+ ], platform);
1547
1549
  if (literalUserId && !isAllowedPlaceholder(literalUserId.value)) {
1548
1550
  findings.push(finding(`${platform}.auth.no-literal-user-id`, "warning", `A hardcoded user identity literal was found: ${literalUserId.name}.`, relativeFile(root, literalUserId.file), "Do not hardcode a userId in source. Read the authenticated user from the host app's auth state (current session, route param, user-store hook, etc.)."));
1549
1551
  }
@@ -1682,8 +1684,9 @@ function validateLiteralGuardrails(root, platform, sourceContent) {
1682
1684
  }
1683
1685
  return findings;
1684
1686
  }
1685
- function firstLiteralAssignment(contents, patterns) {
1686
- for (const [file, content] of contents) {
1687
+ function firstLiteralAssignment(contents, patterns, platform) {
1688
+ for (const [file, rawContent] of contents) {
1689
+ const content = commentStripped(file, platform, rawContent);
1687
1690
  for (const pattern of patterns) {
1688
1691
  pattern.lastIndex = 0;
1689
1692
  const match = pattern.exec(content);
@@ -2162,6 +2165,87 @@ function astLanguageForFile(filePath, platform) {
2162
2165
  }
2163
2166
  return undefined;
2164
2167
  }
2168
+ // Comment-aware view of a source file for the presence-of-a-bad-literal regex
2169
+ // checks that GATE (channel-target-resolved, inline secrets, literal userId/feed
2170
+ // target). A pattern that appears only in a commented-out or documentation line
2171
+ // must not trip a gate — a hard CI failure on a comment is the worst false positive
2172
+ // a compliance gate can produce. ts/tsx/kotlin use the precise tree-sitter stripper;
2173
+ // Swift/Dart (no grammar wired) use the conservative scanner below. Anything else is
2174
+ // returned unchanged.
2175
+ function commentStripped(filePath, platform, content) {
2176
+ const astLang = astLanguageForFile(filePath, platform);
2177
+ if (astLang)
2178
+ return stripComments(astLang, content);
2179
+ const ext = path.extname(filePath).toLowerCase();
2180
+ if (ext === ".swift" || ext === ".dart")
2181
+ return stripLineAndBlockComments(content);
2182
+ return content;
2183
+ }
2184
+ // Conservative comment stripper for languages without a wired tree-sitter grammar
2185
+ // (Swift, Dart). Blanks `//` line comments and `/* */` block comments with spaces,
2186
+ // preserving newlines so offsets/line numbers are unchanged. It tracks single-line
2187
+ // string state ("…" and '…') with escape handling so a `//` inside a string or a URL
2188
+ // ("https://…") is not mistaken for a comment. Critically, it only ever blanks
2189
+ // comment spans — never code or string text — so any mis-classification degrades
2190
+ // toward a residual false-positive (a comment left un-stripped), never a silent
2191
+ // false-negative on a gate. Multi-line/raw strings are not modeled precisely, but the
2192
+ // same fail-toward-firing property holds (worst case: a comment is not stripped).
2193
+ function stripLineAndBlockComments(source) {
2194
+ const out = source.split("");
2195
+ let inString = null;
2196
+ let inBlock = false;
2197
+ let i = 0;
2198
+ while (i < source.length) {
2199
+ const c = source[i];
2200
+ const next = source[i + 1];
2201
+ if (inBlock) {
2202
+ if (c === "*" && next === "/") {
2203
+ out[i] = " ";
2204
+ out[i + 1] = " ";
2205
+ i += 2;
2206
+ inBlock = false;
2207
+ continue;
2208
+ }
2209
+ if (c !== "\n")
2210
+ out[i] = " ";
2211
+ i += 1;
2212
+ continue;
2213
+ }
2214
+ if (inString) {
2215
+ // Escape: skip the next char — but never jump past a newline, so a stray
2216
+ // trailing backslash can't swallow the following line of real code.
2217
+ if (c === "\\" && next !== "\n") {
2218
+ i += 2;
2219
+ continue;
2220
+ }
2221
+ if (c === inString || c === "\n")
2222
+ inString = null;
2223
+ i += 1;
2224
+ continue;
2225
+ }
2226
+ if (c === '"' || c === "'") {
2227
+ inString = c;
2228
+ i += 1;
2229
+ continue;
2230
+ }
2231
+ if (c === "/" && next === "/") {
2232
+ for (let j = i; j < source.length && source[j] !== "\n"; j += 1)
2233
+ out[j] = " ";
2234
+ while (i < source.length && source[i] !== "\n")
2235
+ i += 1;
2236
+ continue;
2237
+ }
2238
+ if (c === "/" && next === "*") {
2239
+ out[i] = " ";
2240
+ out[i + 1] = " ";
2241
+ i += 2;
2242
+ inBlock = true;
2243
+ continue;
2244
+ }
2245
+ i += 1;
2246
+ }
2247
+ return out.join("");
2248
+ }
2165
2249
  function validateCommentReferenceTypeEnum(root, platform, sourceContent) {
2166
2250
  const findings = [];
2167
2251
  // TypeScript/React Native: the SDK types referenceType as the string-literal
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amityco/social-plus-vise",
3
- "version": "0.12.4",
3
+ "version": "0.13.0",
4
4
  "description": "Skill-guided deterministic CLI for social.plus SDK integration assistance.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",