@amityco/social-plus-vise 0.14.5 → 0.14.6

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,24 @@ 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.14.6 — 2026-06-05
8
+
9
+ **Theme:** Intake questions must reach the human before implementation.
10
+
11
+ ### Added
12
+ - **`vise init` now enforces unresolved blocking intake:** normal init returns `status: "needs-clarification"` and exits 7 when required planning questions are still unanswered. This prevents host agents from skipping surfaced questions and writing a compliance sidecar as if scope were resolved.
13
+ - **`sp-vise/intake.json`:** successful init records the request, outcome, answers, remaining blocking count, and whether unresolved intake was explicitly acknowledged.
14
+ - **`--allow-unresolved-intake`:** explicit bypass for retrospective benchmark/harness initialization only. The acknowledgement is recorded in `sp-vise/intake.json`; customer implementations should answer the blocking questions instead.
15
+
16
+ ### Fixed
17
+ - **Host-project `style.css` design extraction:** `vise design extract --from-project` now detects non-theme-named CSS files that declare custom properties, so common roots like `style.css` produce strong host-project design contracts instead of empty weak contracts.
18
+ - **Design-source wording:** `vise plan` now labels host-project design contracts as the host app's design system, not the customer's prototype, and empty contracts tell the agent to ask for the correct design source instead of implying tokens exist.
19
+
20
+ ### Docs
21
+ - **Skill and tool docs now require surfacing blocking intake questions** before implementation, and release smoke guidance uses `--allow-unresolved-intake` only where a disposable retrospective init is intended.
22
+
23
+ ---
24
+
7
25
  ## 0.14.5 — 2026-06-04
8
26
 
9
27
  **Theme:** Optional feed capabilities become explicit opt-in sensors.
package/README.md CHANGED
@@ -86,9 +86,44 @@ Correctness is gated by deterministic rules or attestations. Baseline completene
86
86
  Vise has two deliberately separate roles:
87
87
 
88
88
  - **Customer integration helper:** runs inside customer projects to inspect, plan, validate, and sensor-check social.plus SDK integrations.
89
- - **Block Factory SDK facts provider:** planned internal mode for social.plus Block Factory to verify SDK capabilities, symbols, and model schemas before reusable blocks are generated or released.
89
+ - **Block Factory SDK facts provider:** internal mode for social.plus Block Factory to verify SDK capabilities, symbols, and model schemas before reusable blocks are generated or released.
90
+ - **Block installer governance:** customer-project workflow that consumes a Block Factory registry, plans safe package/source changes, writes `sp-vise/blocks.json`, and validates installed block state.
90
91
 
91
- Vise owns SDK truth and customer-project governance. social.plus Block Factory owns block contracts, package adapters, previews, conformance tests, and release readiness. See [docs/SDK_FACTS_FOR_BLOCK_FACTORY.md](docs/SDK_FACTS_FOR_BLOCK_FACTORY.md) for the internal provider-side plan.
92
+ Vise owns SDK truth and customer-project governance. social.plus Block Factory owns block contracts, package adapters, registry metadata, previews, conformance tests, and release readiness. See [docs/SDK_FACTS_FOR_BLOCK_FACTORY.md](docs/SDK_FACTS_FOR_BLOCK_FACTORY.md) for the internal provider-side plan.
93
+
94
+ ### Block Factory user experience
95
+
96
+ When Vise is used with social.plus Block Factory, the customer experience should feel like asking an AI agent for a social capability rather than manually assembling SDK calls and UI states:
97
+
98
+ > "Add social.plus comments to this app and match my existing design system."
99
+
100
+ The agent uses Vise to turn that request into a governed install workflow:
101
+
102
+ ```sh
103
+ vise blocks list --registry ./registry/blocks.json --format json
104
+ vise blocks plan . --block comments --registry ./registry/blocks.json --format json
105
+ vise blocks add . --block comments --registry ./registry/blocks.json --package-source npm --dry-run
106
+ vise blocks add . --block comments --registry ./registry/blocks.json --package-source npm --apply
107
+ vise blocks validate . --block comments --registry ./registry/blocks.json --format json
108
+ vise run-sensors --dry-run
109
+ ```
110
+
111
+ What Vise does:
112
+
113
+ - Inspects the customer project and detects the target platform.
114
+ - Reads Block Factory registry metadata without owning block product data.
115
+ - Plans package, source-anchor, sidecar, design-contract, and sensor requirements.
116
+ - Applies changes only to package manifests, `sp-vise/blocks.json`, and source files with explicit install anchors.
117
+ - Returns `needs-review` when a brownfield project has no safe install anchor.
118
+ - Validates installed block state and detects drift after future code changes.
119
+
120
+ Required customer or agent actions:
121
+
122
+ - Choose the block id and package source.
123
+ - Review the dry-run plan before using `--apply`.
124
+ - Add explicit install anchors when Vise cannot safely edit the project.
125
+ - Keep `sp-vise/blocks.json` committed so future validation has state to compare.
126
+ - Run the target project's normal build, lint, test, and Vise sensors before shipping.
92
127
 
93
128
  ### Design-conformant UI
94
129
 
@@ -253,7 +288,7 @@ flowchart LR
253
288
  C --> D{Intake<br/>questions?}
254
289
  D -->|Yes| E[Agent asks user<br/>for feed target,<br/>design source, etc.]
255
290
  D -->|No| F
256
- E --> F[Agent runs<br/>vise init]
291
+ E --> F[Agent runs<br/>vise init<br/>with answers]
257
292
  F --> G[Agent runs<br/>vise search-docs<br/>vise get-doc-page]
258
293
  G --> H[Agent edits<br/>your code]
259
294
  H --> I[Agent runs<br/>vise check]
@@ -266,7 +301,7 @@ flowchart LR
266
301
  M --> N[Done.<br/>sp-vise/ contract<br/>committed]
267
302
  ```
268
303
 
269
- The flow above is what the skill teaches your AI agent. You — the human — drive intent and approve edits; the agent runs Vise commands deterministically; Vise grounds the agent in real docs and real compliance rules.
304
+ The flow above is what the skill teaches your AI agent. You — the human — drive intent and approve edits; the agent runs Vise commands deterministically; Vise grounds the agent in real docs and real compliance rules. If blocking intake is still unresolved, `vise init` refuses to initialize, returns `status: "needs-clarification"`, and exits 7 so the agent must surface the questions instead of guessing.
270
305
 
271
306
  ---
272
307
 
@@ -280,7 +315,11 @@ The flow above is what the skill teaches your AI agent. You — the human — dr
280
315
  | `vise inspect [path]` | Detect platform, monorepo surfaces, design signals, available sensors |
281
316
  | `vise plan [path] --request "..."` | Produce a grounded implementation plan with intake questions and docs citations |
282
317
  | `vise plan-harness [path] --request "..."` | (Pre-planning step) Build the harness around the request |
283
- | `vise init [path] --request "..."` | Write the `sp-vise/` compliance contract for this project |
318
+ | `vise init [path] --request "..." [--answer key=value]` | Write the `sp-vise/` compliance contract after blocking intake is answered; returns `needs-clarification` and exits 7 if required answers are missing |
319
+ | `vise blocks list --registry <path>` | Read a social.plus Block Factory registry |
320
+ | `vise blocks plan [path] --block <id> --registry <path>` | Plan safe block package, source-anchor, sidecar, and sensor changes |
321
+ | `vise blocks add [path] --block <id> --registry <path> [--dry-run\|--apply]` | Dry-run or apply safe block installation inside a customer project |
322
+ | `vise blocks validate [path] [--block <id>] --registry <path>` | Validate installed block sidecar state, package presence, and source anchors |
284
323
 
285
324
  ### Design contract (UI generation)
286
325
 
@@ -423,11 +462,12 @@ jobs:
423
462
 
424
463
  ## Compliance Contract
425
464
 
426
- After `vise init`, your project gets a `sp-vise/` directory. These files become part of your repo and travel through code review:
465
+ After a successful `vise init`, your project gets a `sp-vise/` directory. If init returns `needs-clarification`, no compliance sidecar is written; answer the blocking questions and run init again. These files become part of your repo and travel through code review:
427
466
 
428
467
  | File | Created by | What it contains |
429
468
  |---|---|---|
430
469
  | `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. |
470
+ | `sp-vise/intake.json` | `vise init` | The request, outcome, intake answers, remaining blocking count, and any retrospective `--allow-unresolved-intake` acknowledgement. |
431
471
  | `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). |
432
472
  | `sp-vise/inspection.json` | `vise init` | The platform, monorepo surface, and design-token signals detected at init time. |
433
473
  | `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. |
package/dist/server.js CHANGED
@@ -15,6 +15,7 @@ import { inspectProjectTool, validateSetupTool } from "./tools/project.js";
15
15
  import { resolveRequestTool, suggestPatchTool } from "./tools/resolve.js";
16
16
  import { runSensorsTool } from "./tools/sensors.js";
17
17
  import { getSdkFactsTool } from "./tools/sdkFacts.js";
18
+ import { addBlockInstall, listRegistryBlocks, planBlockInstall, validateBlockInstall } from "./tools/blocks.js";
18
19
  import { debugIssueTool, debugIssue } from "./tools/debug.js";
19
20
  import { packageName, packageVersion } from "./version.js";
20
21
  const tools = new Map([
@@ -217,9 +218,62 @@ async function handleCli(args) {
217
218
  });
218
219
  return "exit";
219
220
  }
221
+ if (command === "blocks") {
222
+ const sub = args[1];
223
+ const subArgs = args.slice(2);
224
+ if (sub === "list") {
225
+ assertOnlyKnownFlags(subArgs, ["registry", "format"], "blocks list");
226
+ assertJsonFormat(subArgs, "blocks list");
227
+ console.log(JSON.stringify(await listRegistryBlocks(requiredFlagValue(subArgs, "registry", "blocks list requires --registry.")), null, 2));
228
+ return "exit";
229
+ }
230
+ if (sub === "plan") {
231
+ assertOnlyKnownFlags(subArgs, ["block", "surface", "surface-path", "registry", "format"], "blocks plan");
232
+ assertJsonFormat(subArgs, "blocks plan");
233
+ console.log(JSON.stringify(await planBlockInstall({
234
+ repoPath: positionalRepoPath(subArgs),
235
+ blockId: requiredFlagValue(subArgs, "block", "blocks plan requires --block."),
236
+ registryPath: requiredFlagValue(subArgs, "registry", "blocks plan requires --registry."),
237
+ surfacePath: flagValue(subArgs, "surface") ?? flagValue(subArgs, "surface-path"),
238
+ }), null, 2));
239
+ return "exit";
240
+ }
241
+ if (sub === "add") {
242
+ assertOnlyKnownFlags(subArgs, ["block", "surface", "surface-path", "registry", "package-source", "dry-run", "apply", "format"], "blocks add");
243
+ assertJsonFormat(subArgs, "blocks add");
244
+ console.log(JSON.stringify(await addBlockInstall({
245
+ repoPath: positionalRepoPath(subArgs),
246
+ blockId: requiredFlagValue(subArgs, "block", "blocks add requires --block."),
247
+ registryPath: requiredFlagValue(subArgs, "registry", "blocks add requires --registry."),
248
+ surfacePath: flagValue(subArgs, "surface") ?? flagValue(subArgs, "surface-path"),
249
+ packageSource: flagValue(subArgs, "package-source"),
250
+ dryRun: hasFlag(subArgs, "dry-run"),
251
+ apply: hasFlag(subArgs, "apply"),
252
+ }), null, 2));
253
+ return "exit";
254
+ }
255
+ if (sub === "validate") {
256
+ assertOnlyKnownFlags(subArgs, ["block", "surface", "surface-path", "registry", "format"], "blocks validate");
257
+ assertJsonFormat(subArgs, "blocks validate");
258
+ console.log(JSON.stringify(await validateBlockInstall({
259
+ repoPath: positionalRepoPath(subArgs),
260
+ blockId: flagValue(subArgs, "block"),
261
+ registryPath: requiredFlagValue(subArgs, "registry", "blocks validate requires --registry."),
262
+ surfacePath: flagValue(subArgs, "surface") ?? flagValue(subArgs, "surface-path"),
263
+ }), null, 2));
264
+ return "exit";
265
+ }
266
+ console.error(`Unknown blocks subcommand: ${sub ?? "(none)"}. Expected "list", "plan", "add", or "validate".`);
267
+ process.exitCode = 1;
268
+ return "exit";
269
+ }
220
270
  if (command === "init") {
221
- assertOnlyKnownFlags(args, ["request", "surface", "surface-path", "answer"], "init");
222
- console.log(JSON.stringify(await initCompliance(positionalRepoPath(args.slice(1)), requiredFlagValue(args, "request", "init requires --request."), flagValue(args, "surface") ?? flagValue(args, "surface-path"), keyValueFlag(args, "answer")), null, 2));
271
+ assertOnlyKnownFlags(args, ["request", "surface", "surface-path", "answer", "allow-unresolved-intake"], "init");
272
+ const result = await initCompliance(positionalRepoPath(args.slice(1)), requiredFlagValue(args, "request", "init requires --request."), flagValue(args, "surface") ?? flagValue(args, "surface-path"), keyValueFlag(args, "answer"), { allowUnresolvedIntake: hasFlag(args, "allow-unresolved-intake") });
273
+ if (result.status === "needs-clarification" && typeof result.exitCode === "number") {
274
+ process.exitCode = result.exitCode;
275
+ }
276
+ console.log(JSON.stringify(result, null, 2));
223
277
  return "exit";
224
278
  }
225
279
  if (command === "check") {
@@ -494,6 +548,20 @@ Usage:
494
548
  vise sdk-facts --platform typescript --capability comments --format json
495
549
  vise sdk-facts --platform react-native --capability reactions --include-symbols
496
550
  vise sdk-facts --platform android --surface-dir ./sdk-surface --format json`;
551
+ }
552
+ if (command === "blocks") {
553
+ return `${packageName} blocks
554
+
555
+ Install and validate social.plus Block Factory blocks inside customer projects.
556
+
557
+ Usage:
558
+ vise blocks list --registry <path> --format json
559
+ vise blocks plan <repoPath> --block comments --registry <path> --format json
560
+ vise blocks add <repoPath> --block comments --registry <path> --package-source <npm|path|tarball> [--dry-run|--apply]
561
+ vise blocks validate <repoPath> [--block comments] --registry <path> --format json
562
+
563
+ Safety:
564
+ Dry-run is the default. Apply mode only edits package manifests, explicit source anchors, and sp-vise/blocks.json.`;
497
565
  }
498
566
  if (command === "init") {
499
567
  return `${packageName} init
@@ -502,7 +570,9 @@ Initialize the local sp-vise compliance sidecar for one integration request.
502
570
 
503
571
  Usage:
504
572
  vise init [repoPath] --request "Add a social feed" [--surface apps/web]
505
- vise init [repoPath] --request "Add a social feed" --answer feed_optional_capabilities=post-poll-creation`;
573
+ vise init [repoPath] --request "Add a social feed" --answer feed_target="existing app community picker"
574
+ vise init [repoPath] --request "Add a social feed" --answer feed_optional_capabilities=post-poll-creation
575
+ vise init [repoPath] --request "Add a social feed" --allow-unresolved-intake`;
506
576
  }
507
577
  if (command === "check") {
508
578
  return `${packageName} check
@@ -605,6 +675,7 @@ Usage:
605
675
  vise validate [repoPath] Validate setup and common risks
606
676
  vise run-sensors [repoPath] Run detected project sensors
607
677
  vise sdk-facts --platform ... Internal SDK surface facts for Block Factory
678
+ vise blocks ... Install and validate Block Factory blocks
608
679
  vise design extract <prototype> Extract a design contract from an HTML/CSS prototype
609
680
  vise design check [repoPath] Advisory (non-blocking) UI-vs-contract conformance report
610
681
  vise design preview [repoPath] Write an HTML visual review of the contract + conformance
@@ -858,7 +929,7 @@ function ciCheckResult(result) {
858
929
  };
859
930
  }
860
931
  function positionalRepoPath(args) {
861
- const flagsWithValues = new Set(["request", "surface", "surface-path", "platform", "capability", "surface-dir", "format", "include", "timeout-ms", "query", "path", "limit", "answer", "target", "dest", "destination", "rule", "confidence", "signer", "identity", "evidence-file", "rationale", "repo", "reference"]);
932
+ const flagsWithValues = new Set(["request", "surface", "surface-path", "platform", "capability", "surface-dir", "format", "include", "timeout-ms", "query", "path", "limit", "answer", "target", "dest", "destination", "rule", "confidence", "signer", "identity", "evidence-file", "rationale", "repo", "reference", "registry", "block", "package-source"]);
862
933
  for (let index = 0; index < args.length; index += 1) {
863
934
  const arg = args[index];
864
935
  if (!arg) {
@@ -880,7 +951,7 @@ function positionalRepoPath(args) {
880
951
  }
881
952
  function requiredPositionalText(args, message) {
882
953
  const values = [];
883
- const flagsWithValues = new Set(["request", "surface", "surface-path", "platform", "capability", "surface-dir", "format", "include", "timeout-ms", "query", "path", "limit", "answer", "target", "dest", "destination", "rule", "confidence", "signer", "identity", "evidence-file", "rationale", "repo", "reference"]);
954
+ const flagsWithValues = new Set(["request", "surface", "surface-path", "platform", "capability", "surface-dir", "format", "include", "timeout-ms", "query", "path", "limit", "answer", "target", "dest", "destination", "rule", "confidence", "signer", "identity", "evidence-file", "rationale", "repo", "reference", "registry", "block", "package-source"]);
884
955
  for (let index = 0; index < args.length; index += 1) {
885
956
  const arg = args[index];
886
957
  if (!arg) {
@@ -937,6 +1008,12 @@ function requiredFlagValue(args, name, message) {
937
1008
  }
938
1009
  return value;
939
1010
  }
1011
+ function assertJsonFormat(args, commandName) {
1012
+ const format = flagValue(args, "format") ?? "json";
1013
+ if (format !== "json") {
1014
+ throw new Error(`${commandName} currently supports --format json only.`);
1015
+ }
1016
+ }
940
1017
  function optionalNumberFlag(args, name) {
941
1018
  const value = flagValue(args, name);
942
1019
  if (!value) {
@@ -0,0 +1,385 @@
1
+ import { access, mkdir, readFile, stat, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { packageVersion } from "../version.js";
4
+ import { inspectProject } from "./project.js";
5
+ import { detectCommandSensors } from "./harness.js";
6
+ import { readDesignContract } from "./design.js";
7
+ const registryPlatformByVisePlatform = {
8
+ typescript: "react",
9
+ "react-native": "react-native",
10
+ flutter: "flutter",
11
+ };
12
+ export async function listRegistryBlocks(registryPath) {
13
+ const registry = await loadRegistry(registryPath);
14
+ return {
15
+ source: "social-plus-block-factory",
16
+ mode: "block-registry",
17
+ schemaVersion: registry.schemaVersion,
18
+ blocks: registry.blocks.map((block) => ({
19
+ blockId: block.blockId,
20
+ version: block.version,
21
+ status: block.status,
22
+ surfaces: block.surfaces,
23
+ requiredSdkCapabilities: block.requiredSdkCapabilities,
24
+ events: block.events,
25
+ })),
26
+ };
27
+ }
28
+ export async function planBlockInstall(options) {
29
+ return buildInstallPlan(options, "plan");
30
+ }
31
+ export async function addBlockInstall(options) {
32
+ const mode = options.apply ? "apply" : "dry-run";
33
+ const plan = await buildInstallPlan(options, mode);
34
+ if (!options.apply) {
35
+ return plan;
36
+ }
37
+ if (plan.status !== "ready") {
38
+ return {
39
+ ...plan,
40
+ applied: false,
41
+ reason: "Block install needs review before Vise can safely write files.",
42
+ };
43
+ }
44
+ const repoPath = requiredRepoPath(options);
45
+ const filesTouched = [];
46
+ filesTouched.push(...(await applyPackageChange(repoPath, plan)));
47
+ filesTouched.push(...(await applySourceChanges(repoPath, plan)));
48
+ filesTouched.push(await writeBlocksSidecar(repoPath, plan, options.packageSource ?? "npm", filesTouched));
49
+ return {
50
+ ...plan,
51
+ applied: true,
52
+ filesTouched,
53
+ };
54
+ }
55
+ export async function validateBlockInstall(options) {
56
+ if (!options.blockId) {
57
+ const repoPath = requiredRepoPath(options);
58
+ const registry = await loadRegistry(options.registryPath);
59
+ const sidecar = await readSidecar(repoPath);
60
+ const installed = sidecar?.installed ?? [];
61
+ const knownBlocks = new Set(registry.blocks.map((block) => block.blockId));
62
+ const findings = installed
63
+ .filter((entry) => !knownBlocks.has(entry.blockId))
64
+ .map((entry) => ({
65
+ ruleId: "blocks.registry.known",
66
+ severity: "warning",
67
+ message: `Installed sidecar entry references unknown registry block ${entry.blockId}.`,
68
+ recommendation: "Update the Block Factory registry or remove stale block sidecar state.",
69
+ }));
70
+ if (installed.length === 0) {
71
+ findings.push({
72
+ ruleId: "blocks.sidecar.installed",
73
+ severity: "warning",
74
+ message: "No installed block sidecar entries exist.",
75
+ recommendation: "Run `vise blocks add <repoPath> --block <id> --apply` after reviewing the dry-run plan.",
76
+ });
77
+ }
78
+ return {
79
+ status: findings.some((finding) => finding.severity === "error") ? "needs-review" : "ready",
80
+ mode: "validate",
81
+ registryBlocks: registry.blocks.map((block) => block.blockId),
82
+ sidecarPath: path.join("sp-vise", "blocks.json"),
83
+ sidecarInstalled: installed,
84
+ findings,
85
+ };
86
+ }
87
+ const plan = await buildInstallPlan(options, "validate");
88
+ const repoPath = requiredRepoPath(options);
89
+ const sidecar = await readSidecar(repoPath);
90
+ const installed = (sidecar?.installed ?? []).filter((entry) => !options.blockId || entry.blockId === options.blockId);
91
+ const findings = [];
92
+ if (installed.length === 0) {
93
+ findings.push({
94
+ ruleId: "blocks.sidecar.installed",
95
+ severity: "warning",
96
+ message: options.blockId ? `No sidecar entry exists for block ${options.blockId}.` : "No installed block sidecar entries exist.",
97
+ recommendation: "Run `vise blocks add <repoPath> --block <id> --apply` after reviewing the dry-run plan.",
98
+ });
99
+ }
100
+ if (plan.packageChange.alreadyPresent === false) {
101
+ findings.push({
102
+ ruleId: "blocks.package.present",
103
+ severity: "error",
104
+ message: `Required package dependency is missing: ${plan.packageChange.dependency}.`,
105
+ recommendation: `Install ${plan.packageChange.dependency} in ${plan.packageChange.file}.`,
106
+ });
107
+ }
108
+ for (const targetFile of plan.targetFiles) {
109
+ if (targetFile.operation === "needs-review") {
110
+ findings.push({
111
+ ruleId: "blocks.anchor.present",
112
+ severity: "error",
113
+ message: `Missing safe install anchor ${targetFile.anchor} in ${targetFile.path}.`,
114
+ recommendation: "Add an explicit social.plus block install anchor or install the block manually and re-run validation.",
115
+ });
116
+ }
117
+ }
118
+ const hasError = findings.some((finding) => finding.severity === "error");
119
+ return {
120
+ ...plan,
121
+ status: hasError ? "needs-review" : plan.status,
122
+ validationStatus: hasError ? "failed" : "passed",
123
+ sidecarInstalled: installed,
124
+ findings,
125
+ };
126
+ }
127
+ async function buildInstallPlan(options, mode) {
128
+ const repoPath = requiredRepoPath(options);
129
+ const registry = await loadRegistry(options.registryPath);
130
+ const block = findBlock(registry, options.blockId);
131
+ const inspection = await inspectProject(repoPath, options.surfacePath);
132
+ const platform = inspection.platforms[0] ?? "unknown";
133
+ const registryPlatform = registryPlatformByVisePlatform[platform];
134
+ const sensors = await detectCommandSensors(inspection.effectiveRoot, inspection.platforms);
135
+ const stopConditions = [];
136
+ if (!registryPlatform) {
137
+ stopConditions.push(`Unsupported Vise blocks installer platform: ${platform}. Supported installer platforms: react, react-native, flutter.`);
138
+ }
139
+ const packageInfo = registryPlatform ? block.packages[registryPlatform] : undefined;
140
+ if (!packageInfo || !registryPlatform) {
141
+ stopConditions.push(`Block ${block.blockId} does not provide installer metadata for detected platform ${platform}.`);
142
+ }
143
+ const safePackage = packageInfo ?? {
144
+ packageName: "",
145
+ dependencyName: "",
146
+ publicEntrypoint: "",
147
+ importName: "",
148
+ installAnchor: "",
149
+ };
150
+ const packageChange = await packageChangeFor(inspection.effectiveRoot, registryPlatform, safePackage, options.packageSource);
151
+ const targetFiles = await targetFilesFor(inspection.effectiveRoot, block.blockId, registryPlatform, safePackage);
152
+ for (const targetFile of targetFiles) {
153
+ if (targetFile.operation === "needs-review") {
154
+ stopConditions.push(`Missing safe install anchor ${targetFile.anchor} in ${targetFile.path}.`);
155
+ }
156
+ }
157
+ return {
158
+ status: stopConditions.length > 0 ? "needs-review" : "ready",
159
+ mode,
160
+ block: {
161
+ id: block.blockId,
162
+ version: block.version,
163
+ status: block.status,
164
+ },
165
+ platform,
166
+ registryPlatform: registryPlatform ?? "react",
167
+ package: safePackage,
168
+ packageSource: options.packageSource,
169
+ packageChange,
170
+ targetFiles,
171
+ sensors: sensors.map((sensor) => ({ name: sensor.name, command: sensor.command, source: sensor.source })),
172
+ stopConditions,
173
+ sidecarPath: path.join("sp-vise", "blocks.json"),
174
+ };
175
+ }
176
+ async function loadRegistry(registryPath) {
177
+ if (!registryPath) {
178
+ throw new Error("blocks command requires --registry <path>.");
179
+ }
180
+ const resolved = path.resolve(registryPath);
181
+ if (!(await exists(resolved))) {
182
+ throw new Error(`Block registry not found: ${registryPath}. Local registry paths are supported in this MVP.`);
183
+ }
184
+ const registryFile = (await stat(resolved)).isDirectory() ? path.join(resolved, "blocks.json") : resolved;
185
+ const registry = JSON.parse(await readFile(registryFile, "utf8"));
186
+ validateRegistry(registry, registryFile);
187
+ return registry;
188
+ }
189
+ function validateRegistry(registry, registryFile) {
190
+ if (registry.schemaVersion !== "2026-06-04.block-registry.v1") {
191
+ throw new Error(`${registryFile}: unsupported block registry schemaVersion ${registry.schemaVersion}`);
192
+ }
193
+ if (!Array.isArray(registry.blocks) || registry.blocks.length === 0) {
194
+ throw new Error(`${registryFile}: registry.blocks must be a non-empty array`);
195
+ }
196
+ for (const block of registry.blocks) {
197
+ if (!block.blockId || !block.version || !Array.isArray(block.surfaces)) {
198
+ throw new Error(`${registryFile}: every block requires blockId, version, and surfaces`);
199
+ }
200
+ }
201
+ }
202
+ function findBlock(registry, blockId) {
203
+ if (!blockId) {
204
+ throw new Error("blocks command requires --block <id> for plan/add/validate.");
205
+ }
206
+ const block = registry.blocks.find((entry) => entry.blockId === blockId);
207
+ if (!block) {
208
+ throw new Error(`Unknown block in registry: ${blockId}`);
209
+ }
210
+ return block;
211
+ }
212
+ async function packageChangeFor(root, platform, packageInfo, packageSource) {
213
+ if (platform === "flutter") {
214
+ const file = "pubspec.yaml";
215
+ const source = await readFile(path.join(root, file), "utf8").catch(() => "");
216
+ return {
217
+ file,
218
+ dependency: packageInfo.dependencyName,
219
+ value: dependencyValue(root, packageInfo, packageSource, platform),
220
+ alreadyPresent: new RegExp(`\\b${escapeRegExp(packageInfo.dependencyName)}\\s*:`).test(source),
221
+ };
222
+ }
223
+ const file = "package.json";
224
+ const packageJson = await readPackageJson(path.join(root, file));
225
+ return {
226
+ file,
227
+ dependency: packageInfo.dependencyName,
228
+ value: dependencyValue(root, packageInfo, packageSource, platform),
229
+ alreadyPresent: Boolean(packageJson.dependencies?.[packageInfo.dependencyName]),
230
+ };
231
+ }
232
+ async function targetFilesFor(root, blockId, platform, packageInfo) {
233
+ const sourceFile = sourceFileFor(platform);
234
+ if (!sourceFile) {
235
+ return [];
236
+ }
237
+ const absolutePath = path.join(root, sourceFile);
238
+ const source = await readFile(absolutePath, "utf8").catch(() => "");
239
+ const blockPresent = packageInfo.publicEntrypoint !== "" && source.includes(packageInfo.publicEntrypoint);
240
+ const anchorPresent = packageInfo.installAnchor !== "" && source.includes(packageInfo.installAnchor);
241
+ return [
242
+ {
243
+ path: sourceFile,
244
+ anchor: packageInfo.installAnchor,
245
+ operation: blockPresent ? "already-present" : anchorPresent ? "insert" : "needs-review",
246
+ },
247
+ ];
248
+ }
249
+ async function applyPackageChange(repoPath, plan) {
250
+ const filePath = path.join(repoPath, plan.packageChange.file);
251
+ if (plan.packageChange.alreadyPresent) {
252
+ return [];
253
+ }
254
+ if (plan.registryPlatform === "flutter") {
255
+ const source = await readFile(filePath, "utf8");
256
+ const next = addPubspecDependency(source, plan.packageChange.dependency, plan.packageChange.value);
257
+ await writeFile(filePath, next, "utf8");
258
+ return [plan.packageChange.file];
259
+ }
260
+ const packageJson = await readPackageJson(filePath);
261
+ packageJson.dependencies = {
262
+ ...(packageJson.dependencies ?? {}),
263
+ [plan.packageChange.dependency]: plan.packageChange.value,
264
+ };
265
+ await writeFile(filePath, `${JSON.stringify(packageJson, null, 2)}\n`, "utf8");
266
+ return [plan.packageChange.file];
267
+ }
268
+ async function applySourceChanges(repoPath, plan) {
269
+ const touched = [];
270
+ for (const targetFile of plan.targetFiles) {
271
+ if (targetFile.operation !== "insert") {
272
+ continue;
273
+ }
274
+ const filePath = path.join(repoPath, targetFile.path);
275
+ const source = await readFile(filePath, "utf8");
276
+ const next = insertBlockSnippet(source, plan);
277
+ if (next !== source) {
278
+ await writeFile(filePath, next, "utf8");
279
+ touched.push(targetFile.path);
280
+ }
281
+ }
282
+ return touched;
283
+ }
284
+ async function writeBlocksSidecar(repoPath, plan, packageSource, filesTouched) {
285
+ const designContract = await readDesignContract(repoPath);
286
+ const sidecarPath = path.join(repoPath, "sp-vise", "blocks.json");
287
+ const existing = await readSidecar(repoPath);
288
+ const sidecar = existing ?? {
289
+ schemaVersion: "2026-06-04.vise-blocks-sidecar.v1",
290
+ viseVersion: packageVersion,
291
+ generatedAt: new Date().toISOString(),
292
+ installed: [],
293
+ };
294
+ const entry = {
295
+ blockId: plan.block.id,
296
+ blockVersion: plan.block.version,
297
+ platform: plan.registryPlatform,
298
+ packageSource,
299
+ filesTouched,
300
+ designContractDigest: designContract?.digest,
301
+ sdkFactsVersion: plan.block.version,
302
+ validationStatus: "installed",
303
+ };
304
+ sidecar.viseVersion = packageVersion;
305
+ sidecar.generatedAt = new Date().toISOString();
306
+ sidecar.installed = [...sidecar.installed.filter((item) => item.blockId !== plan.block.id), entry];
307
+ await mkdir(path.dirname(sidecarPath), { recursive: true });
308
+ await writeFile(sidecarPath, `${JSON.stringify(sidecar, null, 2)}\n`, "utf8");
309
+ return path.join("sp-vise", "blocks.json");
310
+ }
311
+ async function readSidecar(repoPath) {
312
+ const sidecarPath = path.join(repoPath, "sp-vise", "blocks.json");
313
+ try {
314
+ return JSON.parse(await readFile(sidecarPath, "utf8"));
315
+ }
316
+ catch {
317
+ return null;
318
+ }
319
+ }
320
+ function dependencyValue(root, packageInfo, packageSource, platform) {
321
+ if (!packageSource || packageSource === "npm") {
322
+ return platform === "flutter" ? "^0.1.0" : "^0.1.0";
323
+ }
324
+ if (packageSource.startsWith("file:")) {
325
+ return packageSource;
326
+ }
327
+ const resolved = path.resolve(root, packageSource);
328
+ return `file:${path.relative(root, resolved)}`;
329
+ }
330
+ function sourceFileFor(platform) {
331
+ if (platform === "react") {
332
+ return path.join("src", "render.mjs");
333
+ }
334
+ if (platform === "react-native") {
335
+ return path.join("src", "App.mjs");
336
+ }
337
+ if (platform === "flutter") {
338
+ return path.join("lib", "main.dart");
339
+ }
340
+ return null;
341
+ }
342
+ function insertBlockSnippet(source, plan) {
343
+ if (source.includes(plan.package.publicEntrypoint)) {
344
+ return source;
345
+ }
346
+ if (plan.registryPlatform === "react" || plan.registryPlatform === "react-native") {
347
+ const importSnippet = `import { ${plan.package.importName} } from "${plan.package.publicEntrypoint}";`;
348
+ return source.replace(plan.package.installAnchor, `${plan.package.installAnchor}\n${importSnippet}\n// social-plus-vise: ${plan.block.id} source mount requires human review in brownfield apps`);
349
+ }
350
+ if (plan.registryPlatform === "flutter") {
351
+ const importSnippet = `import '${plan.package.publicEntrypoint}';`;
352
+ return source.replace(plan.package.installAnchor, `${plan.package.installAnchor}\n${importSnippet}\n// social-plus-vise: ${plan.block.id} widget mount requires human review in brownfield apps`);
353
+ }
354
+ return source;
355
+ }
356
+ function addPubspecDependency(source, dependencyName, value) {
357
+ if (!source.includes("dependencies:")) {
358
+ return `${source.trimEnd()}\n\ndependencies:\n ${dependencyName}:\n path: ${value.replace(/^file:/, "")}\n`;
359
+ }
360
+ const lines = source.split(/\r?\n/);
361
+ const insertIndex = lines.findIndex((line) => line.trim() === "dependencies:") + 1;
362
+ lines.splice(insertIndex, 0, ` ${dependencyName}:`, ` path: ${value.replace(/^file:/, "")}`);
363
+ return `${lines.join("\n").trimEnd()}\n`;
364
+ }
365
+ async function readPackageJson(filePath) {
366
+ return JSON.parse(await readFile(filePath, "utf8"));
367
+ }
368
+ async function exists(filePath) {
369
+ try {
370
+ await access(filePath);
371
+ return true;
372
+ }
373
+ catch {
374
+ return false;
375
+ }
376
+ }
377
+ function requiredRepoPath(options) {
378
+ if (!options.repoPath) {
379
+ throw new Error("blocks command requires repoPath.");
380
+ }
381
+ return path.resolve(options.repoPath);
382
+ }
383
+ function escapeRegExp(value) {
384
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
385
+ }
@@ -2,11 +2,11 @@ import { createHash, randomUUID } from "node:crypto";
2
2
  import { mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- import { assessProjectCompleteness, assessProjectSelectedOptionalCapabilities, selectedOptionalCapabilityIds, } from "../capabilities.js";
6
- import { classifyOutcome } from "../outcomes.js";
7
- import { objectInput, optionalStringField, stringField, textResult } from "../types.js";
5
+ import { assessProjectCompleteness, assessProjectSelectedOptionalCapabilities, optionalCapabilityChecklist, selectedOptionalCapabilityIds, } from "../capabilities.js";
6
+ import { classifyOutcome, getOutcomeDefinition, hasAnswer, planContextFor, } from "../outcomes.js";
7
+ import { objectInput, optionalBooleanField, optionalStringField, stringField, textResult } from "../types.js";
8
8
  import { packageVersion } from "../version.js";
9
- import { readDesignContract } from "./design.js";
9
+ import { buildDesignBrief, readDesignContract } from "./design.js";
10
10
  import { inspectProject, validateSetup } from "./project.js";
11
11
  const complianceDirName = "sp-vise";
12
12
  const attestationsDirName = "attestations";
@@ -26,13 +26,17 @@ export const initComplianceTool = {
26
26
  description: "Optional intake answers from vise plan, including feed_optional_capabilities.",
27
27
  additionalProperties: { type: "string" },
28
28
  },
29
+ allowUnresolvedIntake: {
30
+ type: "boolean",
31
+ description: "Explicit acknowledgement for retrospective/harness initialization when blocking intake questions remain unresolved.",
32
+ },
29
33
  },
30
34
  required: ["repoPath", "request"],
31
35
  additionalProperties: false,
32
36
  },
33
37
  async call(input) {
34
38
  const args = objectInput(input);
35
- return textResult(await initCompliance(stringField(args, "repoPath"), stringField(args, "request"), optionalStringField(args, "surfacePath"), answersFromInput(args.answers)));
39
+ return textResult(await initCompliance(stringField(args, "repoPath"), stringField(args, "request"), optionalStringField(args, "surfacePath"), answersFromInput(args.answers), { allowUnresolvedIntake: optionalBooleanField(args, "allowUnresolvedIntake") }));
36
40
  },
37
41
  };
38
42
  export const checkComplianceTool = {
@@ -242,7 +246,7 @@ async function readEngagement(repoRoot) {
242
246
  function engagementPath(repoRoot) {
243
247
  return path.join(sidecarDir(repoRoot), "engagement.json");
244
248
  }
245
- export async function initCompliance(repoPath, request, surfacePath, answers = {}) {
249
+ export async function initCompliance(repoPath, request, surfacePath, answers = {}, options = {}) {
246
250
  const repoRoot = path.resolve(repoPath);
247
251
  const inspection = await inspectProject(repoRoot, surfacePath);
248
252
  const outcome = classifyOutcome(request);
@@ -252,6 +256,24 @@ export async function initCompliance(repoPath, request, surfacePath, answers = {
252
256
  const fileRefs = rules.map(ruleRefForFile); // adds title for human/agent readers
253
257
  const engagement = await readEngagement(repoRoot);
254
258
  const designContract = await readDesignContract(repoRoot);
259
+ const intake = intakeAuditFor({
260
+ request,
261
+ outcome,
262
+ platforms: inspection.platforms,
263
+ designSignals: inspection.designSignals,
264
+ answers,
265
+ designBrief: designContract ? buildDesignBrief(designContract) : undefined,
266
+ allowUnresolvedIntake: options.allowUnresolvedIntake === true,
267
+ });
268
+ if (intake.remainingBlocking > 0 && !intake.acknowledged_unresolved_blocking) {
269
+ return {
270
+ status: "needs-clarification",
271
+ exitCode: 7,
272
+ outcome,
273
+ intake,
274
+ nextStep: "Run `vise plan` and surface the blocking intake questions to the customer. Re-run `vise init` with --answer for each blocking question, or pass --allow-unresolved-intake only for retrospective/harness initialization.",
275
+ };
276
+ }
255
277
  const compliance = {
256
278
  schema_version: schemaVersion,
257
279
  vise_version: packageVersion,
@@ -276,6 +298,12 @@ export async function initCompliance(repoPath, request, surfacePath, answers = {
276
298
  };
277
299
  await mkdir(attestationsDir(repoRoot), { recursive: true });
278
300
  await writeJson(compliancePath(repoRoot), compliance);
301
+ await writeJson(path.join(sidecarDir(repoRoot), "intake.json"), {
302
+ generated_at: compliance.generated_at,
303
+ request,
304
+ outcome,
305
+ ...intake,
306
+ });
279
307
  await writeJson(path.join(sidecarDir(repoRoot), "inspection.json"), inspection);
280
308
  await writeFile(path.join(sidecarDir(repoRoot), "README.md"), sidecarReadme(compliance), "utf8");
281
309
  // Write a frozen check snapshot so agents can see current rule status immediately
@@ -302,10 +330,82 @@ export async function initCompliance(repoPath, request, surfacePath, answers = {
302
330
  engagement_id: engagement?.engagement_id,
303
331
  ...(compliance.design_contract && { design_contract: compliance.design_contract }),
304
332
  ...(selectedOptionalCapabilities.length > 0 && { selected_optional_capabilities: selectedOptionalCapabilities }),
333
+ intake: {
334
+ status: intake.status,
335
+ remainingBlocking: intake.remainingBlocking,
336
+ acknowledged_unresolved_blocking: intake.acknowledged_unresolved_blocking,
337
+ questions: intake.questions,
338
+ answers: intake.answers,
339
+ },
305
340
  ...(warnings.length > 0 && { warnings }),
306
341
  nextStep: "Run vise check, then implement until rules pass deterministically or are attested.",
307
342
  };
308
343
  }
344
+ function intakeAuditFor(args) {
345
+ const platform = preferredPlatform(args.platforms);
346
+ const ctx = planContextFor({
347
+ request: args.request,
348
+ outcome: args.outcome,
349
+ platform,
350
+ platforms: args.platforms,
351
+ designSignals: args.designSignals,
352
+ answers: args.answers,
353
+ });
354
+ const questions = [...getOutcomeDefinition(args.outcome).intakeQuestions(ctx)];
355
+ if (ctx.mentionsDesign && ctx.designSignals.length === 0 && !hasAnswer(ctx.answers, "design_source")) {
356
+ questions.push({
357
+ id: "design_source",
358
+ question: "Where are the app's design tokens, theme, or reusable UI components defined?",
359
+ why: "The user asked to match the existing design, and no local design source was detected.",
360
+ required: true,
361
+ blocksImplementationWhenMissing: true,
362
+ });
363
+ }
364
+ if (ctx.mentionsDesign && ctx.designSignals.length > 0 && !hasAnswer(ctx.answers, "confirm_design_source")) {
365
+ questions.push({
366
+ id: "confirm_design_source",
367
+ question: `Should the social UI use the detected design source(s): ${ctx.designSignals.map((signal) => signal.file).join(", ")}?`,
368
+ why: "Vise found likely design evidence, but the user or host agent should confirm it is the right source before UI edits.",
369
+ required: true,
370
+ blocksImplementationWhenMissing: false,
371
+ options: ["yes", "use another source"],
372
+ });
373
+ }
374
+ if (args.designBrief &&
375
+ (args.outcome === "add-feed" || args.outcome === "add-chat") &&
376
+ !args.designBrief.roles.some((role) => role.role === "primaryAction") &&
377
+ !hasAnswer(ctx.answers, "primary_action_token")) {
378
+ questions.push({
379
+ id: "primary_action_token",
380
+ question: "Which design token (or color value) should be used as the primary action color? No primary-action token was confidently identified in the design contract.",
381
+ why: "A primary action colour is needed for interactive elements (composer button, own-message bubble). Without a confident token, the agent must guess or omit it.",
382
+ required: false,
383
+ blocksImplementationWhenMissing: false,
384
+ });
385
+ }
386
+ if (args.outcome === "add-feed" && optionalCapabilityChecklist(args.outcome).length > 0 && !hasAnswer(ctx.answers, "feed_optional_capabilities")) {
387
+ questions.push({
388
+ id: "feed_optional_capabilities",
389
+ question: "Which optional feed capabilities should be in scope: post-image-upload, post-poll-creation, post-edit, or none?",
390
+ why: "These capabilities are useful for full feeds, but should become enforceable only after the customer explicitly opts in.",
391
+ required: false,
392
+ blocksImplementationWhenMissing: false,
393
+ options: ["none", ...optionalCapabilityChecklist(args.outcome).map((capability) => capability.id)],
394
+ });
395
+ }
396
+ const remainingBlocking = questions.filter((question) => question.blocksImplementationWhenMissing).length;
397
+ return {
398
+ status: remainingBlocking > 0 ? "needs-clarification" : "ready",
399
+ questions,
400
+ answers: args.answers,
401
+ remainingBlocking,
402
+ acknowledged_unresolved_blocking: args.allowUnresolvedIntake && remainingBlocking > 0,
403
+ };
404
+ }
405
+ function preferredPlatform(platforms) {
406
+ const order = ["flutter", "android", "typescript", "react-native", "ios"];
407
+ return order.find((platform) => platforms.includes(platform)) ?? platforms[0] ?? "unknown";
408
+ }
309
409
  export async function applicableComplianceRuleSummaries(outcome, platforms) {
310
410
  return (await applicableRules(outcome, platforms)).map(ruleRefForFile);
311
411
  }
@@ -1449,14 +1449,14 @@ async function findProjectDesignFiles(root) {
1449
1449
  stack.push(full);
1450
1450
  }
1451
1451
  }
1452
- else if (entry.isFile() && isDesignFile(full)) {
1452
+ else if (entry.isFile() && await isDesignFile(full)) {
1453
1453
  out.push(full);
1454
1454
  }
1455
1455
  }
1456
1456
  }
1457
1457
  return out.sort();
1458
1458
  }
1459
- function isDesignFile(full) {
1459
+ async function isDesignFile(full) {
1460
1460
  const lower = full.replace(/\\/g, "/").toLowerCase();
1461
1461
  const base = path.basename(lower);
1462
1462
  const ext = path.extname(lower);
@@ -1467,7 +1467,10 @@ function isDesignFile(full) {
1467
1467
  return /\/lib\//.test(lower) && /(theme|color|token|palette)/.test(base);
1468
1468
  }
1469
1469
  if (ext === ".css" || ext === ".scss") {
1470
- return base === "globals.css" || /(theme|token|design)/.test(base);
1470
+ if (base === "globals.css" || /(theme|token|design)/.test(base)) {
1471
+ return true;
1472
+ }
1473
+ return cssFileDeclaresDesignTokens(full);
1471
1474
  }
1472
1475
  if (ext === ".json") {
1473
1476
  return /\.colorset\//.test(lower) && base === "contents.json"; // iOS asset-catalog color
@@ -1486,6 +1489,19 @@ function isDesignFile(full) {
1486
1489
  }
1487
1490
  return false;
1488
1491
  }
1492
+ async function cssFileDeclaresDesignTokens(full) {
1493
+ try {
1494
+ const info = await stat(full);
1495
+ if (info.size > MAX_FILE_BYTES) {
1496
+ return false;
1497
+ }
1498
+ const content = await readFile(full, "utf8");
1499
+ return /(^|[{\s;])--[a-zA-Z0-9_-]+\s*:\s*[^;{}]+[;}]/.test(stripCssComments(content));
1500
+ }
1501
+ catch {
1502
+ return false;
1503
+ }
1504
+ }
1489
1505
  // ---------------------------------------------------------------------------
1490
1506
  // Contract construction
1491
1507
  // ---------------------------------------------------------------------------
@@ -147,7 +147,7 @@ function optionalCapabilitiesFor(outcome, answers, request) {
147
147
  }
148
148
  return {
149
149
  answerId: "feed_optional_capabilities",
150
- note: "Optional feed capabilities are feed-forward choices, not baseline compliance. If the user opts in, pass the selected ids through `--answer feed_optional_capabilities=<id[,id]>` on `vise plan` and `vise init`; `vise check` will then run source sensors for those selected capabilities.",
150
+ note: "Optional feed capabilities are feed-forward choices, not baseline compliance. Before `vise init`, make an explicit answer: pass selected ids through `--answer feed_optional_capabilities=<id[,id]>` on `vise plan` and `vise init`, or pass `--answer feed_optional_capabilities=none`. `vise check` runs source sensors only for selected capabilities; explicitly unselected capabilities remain non-mandatory.",
151
151
  choices,
152
152
  selected: selectedOptionalCapabilityIds(outcome, answers, request),
153
153
  };
@@ -163,12 +163,21 @@ function designContractGuidance(contract) {
163
163
  .map((token) => token.provenance === "declared" && token.name
164
164
  ? `${token.name}: ${token.value}`
165
165
  : `${token.value} (inferred, ${token.uses}×)`);
166
+ const tokenCount = contract.stats.declared_tokens + contract.stats.inferred_tokens;
167
+ const sourceLabel = contract.source.kind === "host-project"
168
+ ? "the host project's design system"
169
+ : "the customer's prototype";
170
+ const sourceAction = contract.source.kind === "host-project"
171
+ ? "host app"
172
+ : "prototype";
173
+ const summary = tokenCount === 0
174
+ ? `A design contract was extracted from ${sourceLabel}, but no usable design tokens were found (strength: ${contract.stats.strength}). Ask the customer for the correct prototype, theme file, token source, or screenshots before inventing brand values.`
175
+ : `A design contract was extracted from ${sourceLabel} (${contract.stats.declared_tokens} declared + ${contract.stats.inferred_tokens} inferred tokens, strength: ${contract.stats.strength}). Build the social.plus UI using these tokens so it matches the ${sourceAction}'s aesthetic. Prefer the declared tokens; treat inferred tokens as advisory and confirm brand values with the customer when the contract is weak.`;
166
176
  return {
167
177
  digest: contract.digest,
168
178
  strength: contract.stats.strength,
169
179
  source: contract.source,
170
- summary: `A design contract was extracted from the customer's prototype (${contract.stats.declared_tokens} declared + ${contract.stats.inferred_tokens} inferred tokens, strength: ${contract.stats.strength}). ` +
171
- "Build the social.plus UI using these tokens so it matches the prototype's aesthetic. Prefer the declared tokens; treat inferred tokens as advisory and confirm brand values with the customer when the contract is weak.",
180
+ summary,
172
181
  tokens: {
173
182
  color: byCategory("color"),
174
183
  spacing: byCategory("space"),
@@ -180,7 +189,7 @@ function designContractGuidance(contract) {
180
189
  },
181
190
  components: contract.components.map((component) => ({ name: component.name, selector: component.selector })),
182
191
  breakpoints: contract.breakpoints.map((breakpoint) => breakpoint.raw),
183
- attestation: `When you record a design attestation, cite this contract digest (${contract.digest}) so the generated feed can be claimed conformant to the customer's prototype.`,
192
+ attestation: `When you record a design attestation, cite this contract digest (${contract.digest}) so the generated feed can be claimed conformant to the ${sourceAction}'s design source.`,
184
193
  advisoryOnly: "This contract is advisory generation guidance — it adds no deterministic enforcement and never fails `vise check`.",
185
194
  };
186
195
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amityco/social-plus-vise",
3
- "version": "0.14.5",
3
+ "version": "0.14.6",
4
4
  "description": "Skill-guided deterministic CLI for social.plus SDK integration assistance.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",
@@ -65,9 +65,10 @@
65
65
  "test:sdk-version": "npm run build && node test/run-sdk-version.mjs",
66
66
  "test:sdk-surface": "node test/run-sdk-surface-snapshot.mjs",
67
67
  "test:sdk-facts": "npm run build && node test/run-sdk-facts.mjs",
68
+ "test:blocks-installer": "npm run build && node test/run-blocks-installer.mjs",
68
69
  "typecheck": "tsc -p tsconfig.json --noEmit",
69
70
  "test:e2e-package": "npm run build && node test/run-e2e-package.mjs",
70
- "validate": "npm run typecheck && npm test && npm run test:mcp && npm run test:cli && npm run test:docs && npm run test:ast && npm run test:design-extract && npm run test:design-brief && npm run test:capabilities && npm run test:classify && npm run test:compliance && npm run test:rule-coverage && npm run test:readme-coverage && npm run test:happy-path-clean && npm run test:fixture-symmetry && npm run test:nonui-skip && npm run test:sdk-version && npm run test:sdk-surface && npm run test:sdk-facts && npm run test:native-idioms && npm run test:grader-facts && npm run test:ground-truth && npm run test:improvements && npm run test:debug && npm run test:preflight && npm run test:e2e-package && npm run bench:symbols-drift && npm run pack:check",
71
+ "validate": "npm run typecheck && npm test && npm run test:mcp && npm run test:cli && npm run test:docs && npm run test:ast && npm run test:design-extract && npm run test:design-brief && npm run test:capabilities && npm run test:classify && npm run test:compliance && npm run test:rule-coverage && npm run test:readme-coverage && npm run test:happy-path-clean && npm run test:fixture-symmetry && npm run test:nonui-skip && npm run test:sdk-version && npm run test:sdk-surface && npm run test:sdk-facts && npm run test:blocks-installer && npm run test:native-idioms && npm run test:grader-facts && npm run test:ground-truth && npm run test:improvements && npm run test:debug && npm run test:preflight && npm run test:e2e-package && npm run bench:symbols-drift && npm run pack:check",
71
72
  "test:ast": "node test/run-ast-helpers.mjs",
72
73
  "test:design-extract": "npm run build && node test/run-design-extract.mjs",
73
74
  "test:design-brief": "npm run build && node test/run-design-brief.mjs",
@@ -37,6 +37,10 @@ vise run-sensors .
37
37
 
38
38
  **`vise check .` is mandatory, not optional. You are not done until it passes or every finding is explicitly attested.** Running `vise plan` and `vise inspect` but skipping `vise check` is the most common failure mode — it means the deterministic catch-net never ran and known-bad patterns ship. When you read a `vise plan`, do not truncate it (`| head` drops the implementation steps); read the full `implementationSteps` array.
39
39
 
40
+ **Feed-forward optional capabilities require an explicit answer.** When `vise plan` returns `optionalCapabilities` or the intake question `feed_optional_capabilities`, do not silently ignore it. Either pass the selected exact ids through both `vise plan` and `vise init`, for example `--answer feed_optional_capabilities=post-image-upload,post-poll-creation`, or pass `--answer feed_optional_capabilities=none` and state that the optional capabilities are out of scope. Selected optional capabilities become source sensors in `vise check`; unselected ones remain non-mandatory.
41
+
42
+ **Blocking intake questions must be surfaced to the user.** If `vise plan` returns `intake.status: "needs-clarification"` with blocking questions, stop and ask those questions before implementation. Current Vise also enforces this at `vise init`: unresolved blocking intake returns `status: "needs-clarification"` and the CLI exits non-zero. Only retrospective benchmark/harness setup may pass `--allow-unresolved-intake`, and that acknowledgement is recorded in `sp-vise/intake.json`.
43
+
40
44
  **Baseline completeness is also a stop condition.** When `vise check .` exits with status `completeness-gap` (exit code 5), one or more baseline capabilities are neither implemented nor opted-out. For add-feed, the baseline capability is pagination. For each missing item: either implement it, or place `// vise: scope-omit <id> — <reason>` in the relevant source file and re-run `vise check .` to confirm it exits 0. A missing baseline capability that is neither built nor opted-out is a silent drop — the check will not pass green until every baseline capability is resolved.
41
45
 
42
46
  Treat Vise runtime smoke sensors as real validation. For TypeScript/React Native projects, `vise run-sensors` may include `TypeScript SDK import smoke`; if it fails, the SDK package does not resolve from the host project runtime and the integration is not done.