@amityco/social-plus-vise 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CHANGELOG.md +62 -25
  2. package/LICENSE +8 -6
  3. package/README.md +51 -18
  4. package/dist/capabilities.js +19 -62
  5. package/dist/intelligence/grounding.js +0 -23
  6. package/dist/intelligence/placement.js +0 -9
  7. package/dist/outcomes.js +44 -22
  8. package/dist/server.js +75 -38
  9. package/dist/tools/ast.js +3 -209
  10. package/dist/tools/blocks.js +6 -20
  11. package/dist/tools/compliance.js +168 -43
  12. package/dist/tools/creative.js +15 -41
  13. package/dist/tools/debug.js +0 -16
  14. package/dist/tools/design.js +18 -364
  15. package/dist/tools/docs.js +53 -24
  16. package/dist/tools/experienceCompiler.js +7 -10
  17. package/dist/tools/experienceSensors.js +1 -1
  18. package/dist/tools/harness.js +2 -27
  19. package/dist/tools/integration.js +6 -38
  20. package/dist/tools/learning.js +1 -1
  21. package/dist/tools/project.js +763 -546
  22. package/dist/tools/sdkFacts.js +2 -15
  23. package/dist/tools/sdkVersion.js +3 -36
  24. package/dist/tools/sensors.js +0 -6
  25. package/dist/tools/uxHarness.js +12 -9
  26. package/package.json +8 -97
  27. package/rules/chat.yaml +225 -0
  28. package/rules/event.yaml +45 -0
  29. package/rules/feed.yaml +24 -24
  30. package/rules/invitation.yaml +58 -0
  31. package/rules/live-data.yaml +104 -2
  32. package/rules/notification-tray.yaml +106 -0
  33. package/rules/poll.yaml +71 -0
  34. package/rules/sdk-lifecycle.yaml +112 -6
  35. package/rules/search.yaml +131 -0
  36. package/rules/story.yaml +221 -0
  37. package/rules/user-blocking.yaml +71 -0
  38. package/sdk-surface/flutter.json +1 -1
  39. package/sdk-surface/ios.json +1 -1
  40. package/sdk-surface/manifest.json +12 -12
  41. package/sdk-surface/models.flutter.json +96 -96
  42. package/sdk-surface/models.ios.json +1 -1
  43. package/sdk-surface/typescript.json +4 -4
  44. package/skills/social-plus-vise/SKILL.md +25 -5
  45. package/scripts/catalog-coverage-html.mjs +0 -325
  46. package/scripts/catalog-relationships-html.mjs +0 -686
  47. package/scripts/catalog-sheets.mjs +0 -286
  48. package/scripts/dart-model-extractor/bin/extract_models.dart +0 -169
  49. package/scripts/dart-model-extractor/pubspec.lock +0 -149
  50. package/scripts/dart-model-extractor/pubspec.yaml +0 -16
  51. package/scripts/extract-sdk-models.mjs +0 -749
  52. package/scripts/import-sdk-surface.mjs +0 -161
  53. package/scripts/pilot-feedback.mjs +0 -107
  54. package/scripts/workshop-board-html.mjs +0 -1018
  55. package/scripts/workshop-kit.mjs +0 -252
  56. package/skills/vise-harness-engineer/SKILL.md +0 -35
@@ -1,4 +1,6 @@
1
- import { readdir, readFile } from "node:fs/promises";
1
+ import { createHash } from "node:crypto";
2
+ import { mkdir, readdir, readFile, rename, stat, writeFile } from "node:fs/promises";
3
+ import os from "node:os";
2
4
  import path from "node:path";
3
5
  import { fileURLToPath } from "node:url";
4
6
  import { objectInput, optionalNumberField, stringField, textResult } from "../types.js";
@@ -11,18 +13,6 @@ let hostedCachePending;
11
13
  function localDocsRoot() {
12
14
  return process.env.SOCIAL_PLUS_DOCS_ROOT ? path.resolve(process.env.SOCIAL_PLUS_DOCS_ROOT) : undefined;
13
15
  }
14
- // Hermetic ("offline") docs mode. When VISE_DOCS_OFFLINE is set truthy, docs
15
- // lookups are served from a committed, bundled corpus instead of the hosted
16
- // learn.social.plus endpoint — so CI (and offline installs) never depend on the
17
- // docs site being reachable. This kills the learn.social.plus flake class that
18
- // once hit test:cli. The live-fetch path stays the default when neither
19
- // VISE_DOCS_OFFLINE nor SOCIAL_PLUS_DOCS_ROOT is set, so real-fetch coverage is
20
- // preserved (see vise/package.json test:docs-live).
21
- //
22
- // Resolution precedence in loadDocPages():
23
- // 1. SOCIAL_PLUS_DOCS_ROOT (explicit local override) — highest, unchanged.
24
- // 2. VISE_DOCS_OFFLINE — serve the bundled docs-cache/ corpus.
25
- // 3. neither — live fetch from the hosted docs (default).
26
16
  function docsOfflineMode() {
27
17
  const raw = process.env.VISE_DOCS_OFFLINE;
28
18
  if (raw === undefined) {
@@ -31,10 +21,6 @@ function docsOfflineMode() {
31
21
  const normalized = raw.trim().toLowerCase();
32
22
  return normalized !== "" && normalized !== "0" && normalized !== "false" && normalized !== "off" && normalized !== "no";
33
23
  }
34
- // The bundled hermetic corpus ships in the package `files` allowlist as
35
- // `docs-cache/`. At runtime this module is dist/tools/docs.js, so the corpus is
36
- // two directories up. An explicit SOCIAL_PLUS_DOCS_OFFLINE_ROOT override is
37
- // honoured for tests that want to point offline mode at a fixture corpus.
38
24
  function bundledDocsCacheRoot() {
39
25
  const override = process.env.SOCIAL_PLUS_DOCS_OFFLINE_ROOT;
40
26
  if (override) {
@@ -62,6 +48,40 @@ function cacheTtlMs() {
62
48
  const value = Number(raw);
63
49
  return Number.isFinite(value) && value >= 0 ? value : DEFAULT_CACHE_TTL_MS;
64
50
  }
51
+ function docsDiskCacheFile() {
52
+ const dir = process.env.VISE_DOCS_CACHE_DIR ?? path.join(os.tmpdir(), "vise-docs-cache");
53
+ const key = createHash("sha256").update(docsBaseUrl()).digest("hex").slice(0, 16);
54
+ return path.join(dir, `llms-full.${key}.txt`);
55
+ }
56
+ async function readDiskCorpus() {
57
+ const ttl = cacheTtlMs();
58
+ if (ttl === 0)
59
+ return undefined;
60
+ const file = docsDiskCacheFile();
61
+ try {
62
+ const info = await stat(file);
63
+ if (Date.now() - info.mtimeMs >= ttl)
64
+ return undefined;
65
+ const pages = parseLlmsFull(await readFile(file, "utf8"), docsBaseUrl());
66
+ return pages.length >= MIN_EXPECTED_PAGES ? pages : undefined;
67
+ }
68
+ catch {
69
+ return undefined;
70
+ }
71
+ }
72
+ async function writeDiskCorpus(content) {
73
+ if (cacheTtlMs() === 0)
74
+ return;
75
+ const file = docsDiskCacheFile();
76
+ try {
77
+ await mkdir(path.dirname(file), { recursive: true });
78
+ const tmp = `${file}.tmp.${process.pid}`;
79
+ await writeFile(tmp, content, "utf8");
80
+ await rename(tmp, file);
81
+ }
82
+ catch {
83
+ }
84
+ }
65
85
  export const searchDocsTool = {
66
86
  name: "search_docs",
67
87
  description: "Search social.plus documentation. For implementation requests, call plan_integration first so required inputs and stop conditions are known before docs lookup.",
@@ -143,7 +163,6 @@ async function loadDocPages() {
143
163
  if (root) {
144
164
  return loadLocalDocPages(root);
145
165
  }
146
- // Hermetic mode: serve the committed bundled corpus, never the network.
147
166
  if (docsOfflineMode()) {
148
167
  const cacheRoot = bundledDocsCacheRoot();
149
168
  const pages = await loadLocalDocPages(cacheRoot);
@@ -157,7 +176,7 @@ async function loadDocPages() {
157
176
  if (hostedCacheEntry && now - hostedCacheEntry.fetchedAt < cacheTtlMs()) {
158
177
  return hostedCacheEntry.pages;
159
178
  }
160
- hostedCachePending ??= loadHostedDocPages()
179
+ hostedCachePending ??= resolveHostedDocPages()
161
180
  .then((pages) => {
162
181
  hostedCacheEntry = { pages, fetchedAt: Date.now() };
163
182
  return pages;
@@ -167,6 +186,20 @@ async function loadDocPages() {
167
186
  });
168
187
  return hostedCachePending;
169
188
  }
189
+ async function resolveHostedDocPages() {
190
+ const cached = await readDiskCorpus();
191
+ if (cached)
192
+ return cached;
193
+ try {
194
+ return await loadHostedDocPages();
195
+ }
196
+ catch (error) {
197
+ const bundled = await loadLocalDocPages(bundledDocsCacheRoot()).catch(() => []);
198
+ if (bundled.length > 0)
199
+ return bundled;
200
+ throw error;
201
+ }
202
+ }
170
203
  async function loadHostedDocPages() {
171
204
  const baseUrl = docsBaseUrl();
172
205
  const fullUrl = `${baseUrl}/llms-full.txt`;
@@ -174,6 +207,7 @@ async function loadHostedDocPages() {
174
207
  const failures = [];
175
208
  try {
176
209
  const content = await fetchText(fullUrl);
210
+ await writeDiskCorpus(content);
177
211
  return parseLlmsFull(content, baseUrl);
178
212
  }
179
213
  catch (error) {
@@ -204,9 +238,6 @@ async function loadLocalDocPages(root) {
204
238
  async function fetchText(url) {
205
239
  return fetchTextWithTimeout(url, "text/plain, text/markdown, */*");
206
240
  }
207
- // Shared network primitive — same AbortController + env-configurable timeout used for
208
- // docs fetches. Exported so other plan-layer lookups (e.g. the npm registry SDK-version
209
- // check) reuse one network path rather than adding a parallel one.
210
241
  export async function fetchTextWithTimeout(url, accept = "*/*", timeoutMs = fetchTimeoutMs()) {
211
242
  const controller = new AbortController();
212
243
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
@@ -314,8 +345,6 @@ async function findDocFiles(root) {
314
345
  if (entry.name === "node_modules" || entry.name === ".git" || entry.name === ".next") {
315
346
  continue;
316
347
  }
317
- // A README is corpus metadata (e.g. the docs-cache/ provenance note), not
318
- // an SDK doc page — don't ingest it as a searchable page.
319
348
  if (/^readme\.(md|mdx)$/i.test(entry.name)) {
320
349
  continue;
321
350
  }
@@ -29,7 +29,7 @@ export const compileExperienceTool = {
29
29
  },
30
30
  registryPath: {
31
31
  type: "string",
32
- description: "Optional local Block Factory registry path used to bridge selected objects to block install plans.",
32
+ description: "Optional local block registry path used to bridge selected objects to block install plans.",
33
33
  },
34
34
  write: {
35
35
  type: "boolean",
@@ -65,7 +65,7 @@ export async function compileExperience(options) {
65
65
  ]);
66
66
  const surfaceHints = creativeSurfaceHints(selection);
67
67
  const surfacePlans = await Promise.all(surfaceHints.map((hint) => compileSurfacePlan(repoRoot, request, options.surfacePath, hint, uxHarness)));
68
- const blockFactoryBridge = await blockFactoryBridgeFor({
68
+ const blocksBridge = await blocksBridgeFor({
69
69
  repoRoot,
70
70
  registryPath: options.registryPath,
71
71
  surfacePath: options.surfacePath,
@@ -77,7 +77,7 @@ export async function compileExperience(options) {
77
77
  : surface.intake.remainingBlocking > 0
78
78
  ? [`Surface ${surface.surfaceId} has ${surface.intake.remainingBlocking} blocking intake question(s).`]
79
79
  : []),
80
- ...(blockFactoryBridge.status === "needs-review" ? ["One or more Block Factory bridge candidates need review before block installation."] : []),
80
+ ...(blocksBridge.status === "needs-review" ? ["One or more block bridge candidates need review before block installation."] : []),
81
81
  ];
82
82
  const compiler = {
83
83
  schema_version: EXPERIENCE_COMPILER_SCHEMA_VERSION,
@@ -109,7 +109,7 @@ export async function compileExperience(options) {
109
109
  surfaceSequence: surfaceSequenceFor(surfaceHints, selection, uxHarness),
110
110
  surfacePlans,
111
111
  designAdaptation: designAdaptationFor(designContract),
112
- blockFactoryBridge,
112
+ blocksBridge,
113
113
  validationPlan: validationPlanFor(request, surfacePlans),
114
114
  sensors: sensors.map((sensor) => ({ name: sensor.name, command: sensor.command, source: sensor.source })),
115
115
  stopConditions,
@@ -204,10 +204,7 @@ function questionSummary(raw) {
204
204
  blocksImplementationWhenMissing: question.blocksImplementationWhenMissing === true,
205
205
  };
206
206
  }
207
- async function blockFactoryBridgeFor(args) {
208
- // Gate on the creative feasibility's computed social.plus-vs-custom classification rather than
209
- // guessing Block Factory's catalog: skip custom objects (no social.plus block exists), keep every
210
- // social.plus-backed object as a candidate, and let the registry confirm or deny each one.
207
+ async function blocksBridgeFor(args) {
211
208
  const customObjectIds = new Set(args.selection.selectedVariant.feasibility?.customObjectIds ?? []);
212
209
  const candidates = blockCandidatesFor(args.selection.selectedVariant.experienceObjectIds, customObjectIds);
213
210
  const customAppLayerObjects = customAppLayerObjectsFor(args.selection.selectedVariant.experienceObjectIds, customObjectIds);
@@ -219,7 +216,7 @@ async function blockFactoryBridgeFor(args) {
219
216
  ...candidate,
220
217
  status: "no-registry",
221
218
  command: `vise blocks plan . --block ${candidate.blockId} --registry <path>${surfaceOption} --format json`,
222
- reason: "No Block Factory registry was provided; this is a suggested bridge candidate only.",
219
+ reason: "No block registry was provided; this is a suggested bridge candidate only.",
223
220
  })),
224
221
  customAppLayerObjects,
225
222
  };
@@ -296,7 +293,7 @@ function installPlanFor(platforms, surfacePlans) {
296
293
  guidance: [
297
294
  `Use the ${platform} social.plus SDK installation path surfaced by focused plans.`,
298
295
  "Pin SDK package versions; do not rely on floating latest ranges.",
299
- "If Block Factory registry candidates are available, review dry-run block plans before applying any package/source changes.",
296
+ "If block registry candidates are available, review dry-run block plans before applying any package/source changes.",
300
297
  ],
301
298
  sdkVersion,
302
299
  packageManagers: packageManagersFor(platforms),
@@ -93,7 +93,7 @@ export async function buildExperienceSensors(options) {
93
93
  stopConditions,
94
94
  nextStep: stopConditions.length > 0
95
95
  ? "Resolve needs-setup and needs-review sensor items, then regenerate `vise experience sensors .` and `vise experience-report .`."
96
- : "Use this sensor framework as the review checklist after implementation. Keep `score` null until benchmark and dogfood evidence calibrate scoring.",
96
+ : "Use this sensor framework as the review checklist after implementation. Keep `score` null until benchmark evidence calibrates scoring.",
97
97
  };
98
98
  if (options.write !== false) {
99
99
  framework.artifacts = await writeExperienceSensors(repoRoot, framework);
@@ -164,19 +164,12 @@ export async function detectCommandSensors(repoPath, platforms) {
164
164
  }
165
165
  return sensors;
166
166
  }
167
- // xcodebuild failure text that means "this ENVIRONMENT cannot build", not "this project is
168
- // broken". Matched case-insensitively against combined stdout+stderr only when the exit code is
169
- // non-zero. Deliberately narrow: a real compile/link error ("error: cannot find 'X' in scope",
170
- // BUILD FAILED with diagnostics) matches none of these and stays a hard failure.
171
167
  const XCODEBUILD_ENVIRONMENT_SKIPS = [
172
168
  {
173
- // xcode-select points at the Command Line Tools: xcodebuild exists on PATH but cannot build.
174
169
  pattern: "requires Xcode|command line tools instance",
175
170
  reason: "xcodebuild is present but the active developer directory is the Command Line Tools, not a full Xcode install. Run `sudo xcode-select -s /Applications/Xcode.app` (or install Xcode) and re-run; this is an environment precondition, not a project failure.",
176
171
  },
177
172
  {
178
- // "Agreeing to the Xcode/iOS license requires admin privileges, please run 'sudo xcodebuild -license'…"
179
- // Anchored to the agreement message — a compile error citing a LicenseView.swift must stay a failure.
180
173
  pattern: "agreeing to the .{0,20}license|xcodebuild -license|license agreement",
181
174
  reason: "The Xcode license has not been accepted in this environment. Run `sudo xcodebuild -license accept` and re-run; this is an environment precondition, not a project failure.",
182
175
  },
@@ -185,8 +178,6 @@ const XCODEBUILD_ENVIRONMENT_SKIPS = [
185
178
  reason: "The build stopped on code-signing requirements (certificates/provisioning not available in this environment). Vise already disables signing for this probe; a signing demand that persists is an environment/config precondition, not a project failure.",
186
179
  },
187
180
  {
188
- // 'SDK "iphonesimulator" cannot be located' / destination errors. Anchored so a generic
189
- // "cannot be located" inside project diagnostics does not classify as environmental.
190
181
  pattern: "unable to find a destination|no destinations|sdk \\S{0,30}\\s*cannot be located|unable to locate a simulator|CoreSimulator",
191
182
  reason: "No usable build destination (simulator runtime/SDK) is installed in this environment. Install an iOS Simulator runtime via Xcode and re-run; this is an environment precondition, not a project failure.",
192
183
  },
@@ -196,11 +187,8 @@ const XCODEBUILD_ENVIRONMENT_SKIPS = [
196
187
  },
197
188
  ];
198
189
  async function iosBuildSensors(root) {
199
- // .xcodeproj/.xcworkspace are bundle DIRECTORIES at the app root.
200
190
  const entries = await readdir(root, { withFileTypes: true }).catch(() => []);
201
191
  const dirNames = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
202
- // Prefer the workspace (CocoaPods projects must build via the workspace); ignore the
203
- // .xcodeproj-embedded project.xcworkspace which readdir at root never returns anyway.
204
192
  const workspace = dirNames.find((name) => /\.xcworkspace$/i.test(name));
205
193
  const project = dirNames.find((name) => /\.xcodeproj$/i.test(name));
206
194
  if (!workspace && !project) {
@@ -208,9 +196,6 @@ async function iosBuildSensors(root) {
208
196
  }
209
197
  const container = workspace ?? project;
210
198
  const source = container;
211
- // Visible precondition (pf-003): the Xcode project enables the sensor, but the sensor only
212
- // RUNS when xcodebuild exists on PATH. Absent toolchain → skipped-with-reason, never a
213
- // silent non-detection and never a spawn-ENOENT failure.
214
199
  if (!(await commandOnPath("xcodebuild"))) {
215
200
  return [
216
201
  {
@@ -225,10 +210,9 @@ async function iosBuildSensors(root) {
225
210
  }
226
211
  const containerFlag = workspace ? "-workspace" : "-project";
227
212
  const buildCommand = workspace
228
- ? // A workspace build REQUIRES a scheme; the conventional shared scheme matches the
229
- // workspace name. A wrong guess is classified as an environment/config skip below.
213
+ ?
230
214
  ["xcodebuild", containerFlag, container, "-scheme", container.replace(/\.xcworkspace$/i, ""), "build"]
231
- : // A bare project build (no -scheme) builds the first target with the default config.
215
+ :
232
216
  ["xcodebuild", containerFlag, container, "build"];
233
217
  return [
234
218
  {
@@ -243,7 +227,6 @@ async function iosBuildSensors(root) {
243
227
  name: "iOS build (xcodebuild)",
244
228
  command: [
245
229
  ...buildCommand,
246
- // Guards: never demand signing assets for a compile probe, and keep the build local.
247
230
  "CODE_SIGNING_ALLOWED=NO",
248
231
  "CODE_SIGNING_REQUIRED=NO",
249
232
  "CODE_SIGN_IDENTITY=",
@@ -256,8 +239,6 @@ async function iosBuildSensors(root) {
256
239
  },
257
240
  ];
258
241
  }
259
- // True when an executable with this name exists on the current PATH. Used for sensor
260
- // preconditions, so the answer must be computed WITHOUT spawning anything.
261
242
  async function commandOnPath(command) {
262
243
  const pathValue = process.env.PATH ?? "";
263
244
  for (const dir of pathValue.split(path.delimiter)) {
@@ -268,7 +249,6 @@ async function commandOnPath(command) {
268
249
  return true;
269
250
  }
270
251
  catch {
271
- // keep scanning
272
252
  }
273
253
  }
274
254
  return false;
@@ -318,11 +298,6 @@ function hasPackageDependency(packageJson, dependencyName) {
318
298
  packageJson.peerDependencies?.[dependencyName] ??
319
299
  packageJson.optionalDependencies?.[dependencyName]);
320
300
  }
321
- // A package.json script that runs a watcher/dev server never exits; running it as a sensor just
322
- // burns the timeout and reports a misleading "timed-out". Detect the unambiguous cases and skip with
323
- // guidance. Deliberately conservative so a real one-shot sensor is never skipped: an explicitly
324
- // disabled flag (--watchAll=false) is NOT a watcher, and ambiguous short flags (e.g. `prettier -w`
325
- // = --write) are not treated as watch.
326
301
  function watchScriptSkipReason(scriptName, body) {
327
302
  const command = String(body);
328
303
  const watchFlag = command.match(/(?:^|\s)--watch(?:all)?(=\S+)?/i);
@@ -2,7 +2,7 @@ import { access, readFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { BROAD_SOCIAL_REGEX, DESIGN_REGEX, getOutcomeDefinition, hasAnswer, planContextFor, resolveOutcome, } from "../outcomes.js";
4
4
  import { objectInput, optionalStringField, stringField, textResult } from "../types.js";
5
- import { availableOptionalCapabilityIds, capabilityChecklist, optionalCapabilityChecklist, platformCapabilityAvailability, selectedOptionalCapabilityIds, } from "../capabilities.js";
5
+ import { availableOptionalCapabilityIds, capabilityChecklist, optionalCapabilityChecklist, platformCapabilityAvailability, selectedOptionalCapabilityIds, slimCapabilityAvailability, slimCapabilityItem, } from "../capabilities.js";
6
6
  import { applicableCompliancePlanRuleSummaries } from "./compliance.js";
7
7
  import { DESIGN_CONTRACT_CONFIRMATION_ANSWER_ID, buildDesignBrief, designContractConfirmationFromAnswers, designPreviewPath, readDesignContract, } from "./design.js";
8
8
  import { sdkVersionGuidance } from "./sdkVersion.js";
@@ -84,12 +84,6 @@ async function buildIntegrationPlan(repoPath, request, surfacePath, answers = {}
84
84
  });
85
85
  const definition = getOutcomeDefinition(outcome);
86
86
  const capabilityAvailability = await platformCapabilityAvailability(outcome, platform);
87
- // The design brief is computed INTERNALLY only, to drive the missing-primary-action
88
- // intake fallback. It is deliberately NOT emitted in plan output: a pre-registered
89
- // ablation (benchmarks/brief-ablation/RESULTS.md, two runs, n=6/arm) showed that
90
- // enumerative plan-time design guidance NARROWS what capable agents build and style —
91
- // the contract + design-check loop alone scored higher on by-name token conformance.
92
- // Retracted in 0.14.1; the generator stays in design.ts for future redesign.
93
87
  const designContract = await readDesignContract(repoRoot);
94
88
  const designReview = designReviewGuidance(repoRoot, designContract, answers);
95
89
  const acceptedDesignContract = designReview.status === "accepted" ? designContract : null;
@@ -111,18 +105,8 @@ async function buildIntegrationPlan(repoPath, request, surfacePath, answers = {}
111
105
  ? definition.intakeQuestions(ctx).filter((question) => question.id !== "feature_surface")
112
106
  : definition.intakeQuestions(ctx);
113
107
  const intake = intakeFor(ctx, outcomeQuestions, outcome, designBrief, capabilityAvailability, designReview);
114
- // Advisory SDK-version currency guidance (npm registry for TS/RN; version-agnostic
115
- // for native). Best-effort — degrades to greenfield "install latest + pin" if the
116
- // registry is unreachable. Never gates.
117
108
  const packageJsonText = await readFile(path.join(root, "package.json"), "utf8").catch(() => undefined);
118
109
  const sdkVersion = await sdkVersionGuidance(platform, packageJsonText);
119
- // Scope decisions, hoisted to the top of the output so they survive a `head`
120
- // truncation. Blocking questions must be resolved before building; clarifying
121
- // questions (e.g. which engagement surfaces are in scope) must be decided and
122
- // the decision stated — so an omitted feature like comments is an explicit,
123
- // reviewable choice rather than a silent drop. Mirrors the detailed `intake`
124
- // block below; placed here because the intake block lands past common `head`
125
- // cut points.
126
110
  const decisionsRequired = [
127
111
  ...(creativeContext
128
112
  ? [
@@ -138,11 +122,6 @@ async function buildIntegrationPlan(repoPath, request, surfacePath, answers = {}
138
122
  ? `[resolve before building] ${q.question}`
139
123
  : `[decide & state in your summary] ${q.question}`),
140
124
  ];
141
- // Steps-first ordering: implementationSteps + validation lead the object so
142
- // that an agent piping `vise plan` through `head` still captures the
143
- // actionable guidance. The verbose scaffolding (intake, docs, surfaces, rule
144
- // dumps) follows. Do not move implementationSteps back down — feature steps
145
- // landing past a `head -120` cut is a documented real-world failure mode.
146
125
  return {
147
126
  outcome,
148
127
  platform,
@@ -151,7 +130,7 @@ async function buildIntegrationPlan(repoPath, request, surfacePath, answers = {}
151
130
  creativeContext,
152
131
  uxHarness: uxHarnessContext,
153
132
  socialWorkplan,
154
- capabilityAvailability,
133
+ capabilityAvailability: slimCapabilityAvailability(capabilityAvailability),
155
134
  designReview,
156
135
  decisionsRequired,
157
136
  implementationSteps: [
@@ -182,10 +161,6 @@ async function buildIntegrationPlan(repoPath, request, surfacePath, answers = {}
182
161
  sdkVersion,
183
162
  };
184
163
  }
185
- // Vise-authored capability checklist for the outcome (feed-forward). The agent
186
- // builds each or opts out with a recorded reason (`// vise: scope-omit <id>
187
- // <reason>`), which `vise check` reads and reports. Advisory — Vise proposes the
188
- // set so completeness doesn't depend on the agent remembering it.
189
164
  function completenessChecklistFor(outcome) {
190
165
  const items = capabilityChecklist(outcome);
191
166
  if (items.length === 0) {
@@ -202,8 +177,8 @@ function optionalCapabilitiesFor(outcome, answers, request, availability) {
202
177
  if (choices.length === 0) {
203
178
  return undefined;
204
179
  }
205
- const unavailable = availability?.unavailable.filter((item) => item.kind === "optional-scope") ?? [];
206
- const unknown = availability?.unknown.filter((item) => item.kind === "optional-scope") ?? [];
180
+ const unavailable = (availability?.unavailable.filter((item) => item.kind === "optional-scope") ?? []).map(slimCapabilityItem);
181
+ const unknown = (availability?.unknown.filter((item) => item.kind === "optional-scope") ?? []).map(slimCapabilityItem);
207
182
  return {
208
183
  answerId: "feed_optional_capabilities",
209
184
  note: "Optional feed capabilities are feed-forward choices, not baseline compliance. Vise filters choices through the platform SDK surface before showing them. 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 supported capabilities; explicitly unselected or unavailable capabilities remain non-mandatory.",
@@ -405,7 +380,7 @@ async function socialWorkplanFor(args) {
405
380
  questions: intake.questions,
406
381
  remainingBlocking: intake.remainingBlocking,
407
382
  },
408
- capabilityAvailability,
383
+ capabilityAvailability: slimCapabilityAvailability(capabilityAvailability),
409
384
  validation: ["validate_setup", "run_sensors", ...definition.validation(args.platform)],
410
385
  docs: definition.docs(args.platform).filter((doc) => doc.path !== "unknown"),
411
386
  optionalCapabilities: optionalCapabilitiesFor(surface.outcome, surfaceAnswers, args.request, capabilityAvailability),
@@ -450,6 +425,7 @@ function designReviewGuidance(repoRoot, contract, answers) {
450
425
  digest: contract.digest,
451
426
  previewPath,
452
427
  previewCommand: "vise design preview .",
428
+ referenceCommand: "vise design reference .",
453
429
  requiredForDesignConformance: true,
454
430
  };
455
431
  if (confirmation === "accepted") {
@@ -472,11 +448,6 @@ function designReviewGuidance(repoRoot, contract, answers) {
472
448
  nextStep: `Open ${previewPath} and show it to the user. Re-run plan/init with --answer ${DESIGN_CONTRACT_CONFIRMATION_ANSWER_ID}=yes only if the user confirms it matches the desired design; use =no to reject and request a new design source.`,
473
449
  };
474
450
  }
475
- // Build advisory UI-generation guidance from an extracted design contract.
476
- // Declared tokens are surfaced with their custom-property name (so the agent
477
- // references `var(--x)` / maps it per platform); inferred tokens carry their
478
- // raw value plus a usage count and an explicit "inferred" marker so they are
479
- // never mistaken for authoritative brand values.
480
451
  function designContractGuidance(contract) {
481
452
  const byCategory = (category) => contract.tokens
482
453
  .filter((token) => token.category === category)
@@ -562,9 +533,6 @@ function intakeFor(ctx, outcomeQuestions, outcome, brief, availability, designRe
562
533
  options: ["yes", "use another source"],
563
534
  });
564
535
  }
565
- // Graceful-degradation fallback: when a design contract exists for a feed or chat
566
- // outcome but no primary-action token was confidently inferred, ask the developer
567
- // to name the correct token. Non-blocking so it doesn't stall implementation.
568
536
  if (brief &&
569
537
  designReview?.status === "accepted" &&
570
538
  (outcome === "add-feed" || outcome === "add-chat") &&
@@ -414,7 +414,7 @@ function summarizeLearning(events) {
414
414
  recommendationOptimization: {
415
415
  status: "not-active",
416
416
  reason: "Local learning events are evidence inputs only. Vise does not alter variant ranking from them yet.",
417
- nextStep: "Use repeated dogfood/benchmark review to define a calibrated ranking policy before enabling recommendation optimization.",
417
+ nextStep: "Use repeated benchmark review to define a calibrated ranking policy before enabling recommendation optimization.",
418
418
  },
419
419
  privacy: privacyBlock(),
420
420
  nextStep: "Review these local events and recurring sensor gaps with Product/SE. Aggregate learning or automatic ranking changes require explicit opt-in and privacy review.",