@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 +66 -0
- package/README.md +42 -31
- package/dist/server.js +67 -5
- package/dist/tools/ast.js +48 -8
- package/dist/tools/compliance.js +4 -1
- package/dist/tools/design.js +249 -1
- package/dist/tools/harness.js +2 -2
- package/dist/tools/project.js +92 -8
- package/package.json +1 -1
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
|
-
> **
|
|
85
|
-
>
|
|
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. `
|
|
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 — `
|
|
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).
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
- **
|
|
142
|
-
- **
|
|
143
|
-
- **Three arms
|
|
144
|
-
- **
|
|
145
|
-
-
|
|
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
|
|
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 |
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
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(
|
|
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;
|
package/dist/tools/compliance.js
CHANGED
|
@@ -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 `
|
|
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
|
"",
|
package/dist/tools/design.js
CHANGED
|
@@ -157,7 +157,16 @@ export async function extractDesignContractFromProject(repoPath) {
|
|
|
157
157
|
}
|
|
158
158
|
}
|
|
159
159
|
inputs.sort();
|
|
160
|
-
|
|
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: "",
|
package/dist/tools/harness.js
CHANGED
|
@@ -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
|
|
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 };
|
package/dist/tools/project.js
CHANGED
|
@@ -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
|
|
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,
|
|
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