@amityco/social-plus-vise 0.14.3 → 0.14.5

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,7 +4,17 @@ 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.3 — 2026-06-04
7
+ ## 0.14.5 — 2026-06-04
8
+
9
+ **Theme:** Optional feed capabilities become explicit opt-in sensors.
10
+
11
+ ### Changed
12
+ - **Add-feed completeness baseline narrowed to pagination:** image upload, poll creation, and edit post are no longer baseline completeness requirements. The add-feed `completenessChecklist` now gates pagination / load-more only.
13
+ - **Optional feed capabilities require explicit opt-in:** `vise plan` now offers image upload, poll creation, and edit post as `optionalCapabilities`. When the user selects one and the agent carries it into `vise init --answer feed_optional_capabilities=...`, `vise check` runs selected source sensors and exits `selected-capability-failures` (exit code 6) until the selected capability is implemented.
14
+
15
+ ---
16
+
17
+ ## 0.14.4 — 2026-06-04
8
18
 
9
19
  **Theme:** Completeness-gap enforcement and add-feed guidance hardening (image upload + poll creation + pagination build step).
10
20
 
@@ -17,6 +27,7 @@ The format is loosely based on [Keep a Changelog](https://keepachangelog.com/en/
17
27
  ### Fixed
18
28
  - **`targetType` rule recommendation tightened:** the `validateFeedTargetTypeExplicit` validator recommendation now explicitly calls for binding `targetType` to the intake-resolved feed target (e.g. `communityId` from route params, `userId` from auth context) rather than simply avoiding a literal. Prevents agents from introducing an unbound variable that satisfies the literal check while leaving the same intent gap.
19
29
  - **Completeness assessment note language corrected:** `vise plan` completeness note and `assessCompleteness` output no longer say "advisory — never fails the check". They now accurately state that missing items cause `vise check` to exit `completeness-gap` (exit code 5).
30
+ - **Scope-omit markers now require a reason:** `// vise: scope-omit <id>` no longer clears a completeness gap by itself. The marker must include a reason (for example `// vise: scope-omit post-poll — polls disabled for this tenant`) or the capability remains `missing`.
20
31
 
21
32
  ### Changed
22
33
  - **`SKILL.md` scope-omit enforcement:** the Required Loop section now treats missing completeness items as a stop condition, not a suggestion. Agents must either implement or `// vise: scope-omit <id> — <reason>` each missing capability before reporting done.
package/README.md CHANGED
@@ -45,7 +45,7 @@ See [Usage Flow](#usage-flow) for the full step-by-step diagram.
45
45
 
46
46
  Instead of just providing a CLI or AI skills, Vise implements a technique called **Agentic Workflow Governance**. Think of it as a customer-project integration harness: the governed build loop runs inside the target repo, grounded in the real project, real docs, and real validation signals.
47
47
 
48
- Vise wraps 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.**
48
+ Vise wraps 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, gates narrow baseline capabilities so nothing mandatory is silently dropped, offers richer feed capabilities as explicit opt-ins, and runs your project's own build/lint/typecheck sensors. **Your source code never leaves your machine.**
49
49
 
50
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
51
 
@@ -77,9 +77,9 @@ Vise validates on three layers, and the layer is set by the *kind of claim* —
77
77
  |---|---|---|---|
78
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. |
79
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 |
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 |
80
+ | **Feature completeness** | "this is **missing**" | Vise proposes a narrow baseline per outcome; for add-feed, pagination is mandatory, while richer feed capabilities are opt-in choices from `vise plan` | **Decision gate** — `vise check` exits `completeness-gap` until each baseline capability is built or validly opted out; selected optional capabilities run separate sensors |
81
81
 
82
- Only correctness is gated (it can be made FP-free); conformance and completeness are surfaced, because "all post types" and "matches the brand" are legitimately scope-dependent. See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
82
+ Correctness is gated by deterministic rules or attestations. Baseline completeness is gated by explicit scope decisions: if a baseline capability is legitimately out of scope, record `// vise: scope-omit <id> — <reason>` and it no longer blocks. Optional feed capabilities such as image upload, poll creation, and edit post are offered during planning and become checked only after the user opts in. Conformance remains advisory because "matches the brand" is legitimately subjective. See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
83
83
 
84
84
  ### Relationship to social.plus Block Factory
85
85
 
@@ -409,12 +409,13 @@ jobs:
409
409
 
410
410
  | Code | Meaning |
411
411
  |---|---|
412
- | `0` | All rules pass and all expected capabilities are either present or opted-out |
412
+ | `0` | All rules pass, all baseline capabilities are present or opted-out, and any selected optional capabilities pass |
413
413
  | `1` | One or more rules need attestation |
414
414
  | `2` | One or more rules have deterministic failures |
415
415
  | `3` | One or more blockers fired (missing prerequisite, e.g. `google-services.json`) |
416
416
  | `4` | Contract drift — rules in `sp-vise/compliance.json` no longer match the current ruleset |
417
- | `5` | One or more expected capabilities are neither implemented nor opted-out — add the capability or place `// vise: scope-omit <id> — <reason>` |
417
+ | `5` | One or more baseline capabilities are neither implemented nor validly opted-out — add the capability or place `// vise: scope-omit <id> — <reason>` |
418
+ | `6` | One or more explicitly selected optional capabilities failed source sensors |
418
419
 
419
420
  `vise check --ci` is read-only. It never updates `sp-vise/`. The JSON output includes a `ci` block with structured details for pipeline logs.
420
421
 
@@ -1,26 +1,25 @@
1
1
  /**
2
- * Feature-completeness assessment — ADVISORY ONLY.
2
+ * Feature-completeness assessment.
3
3
  *
4
4
  * Boundary (see the validation-boundaries principle): completeness is a "this is
5
- * missing" claim — a universal-negative over open-ended correct implementations.
6
- * It is structurally false-positive-prone and therefore NEVER a hard gate. This
7
- * module only surfaces nudges; `vise check`'s status/exit code are untouched.
5
+ * missing" claim — a universal-negative over open-ended correct implementations
6
+ * unless the customer/agent explicitly removes it from scope. Missing capabilities
7
+ * now produce `completeness-gap` in `vise check`; a recorded scope-omit reason is
8
+ * the false-positive escape hatch.
8
9
  *
9
10
  * Memory-independence comes from inversion: VISE authors the canonical capability
10
11
  * set per outcome; the agent must OPT OUT of a capability with a recorded reason
11
12
  * (`// vise: scope-omit <id> <reason>`), which `check` reads and reports. The
12
13
  * agent subtracts with justification — it doesn't have to remember the set.
13
14
  *
14
- * Gate-eligibility (not used here these stay advisory): a capability could only
15
- * ever graduate to a gate if it has a named SDK-call anchor AND a no-fire fixture.
16
- * Every capability below is anchored on SDK symbols for exactly that reason, but
17
- * the output is advisory regardless.
15
+ * Baseline capability gates must stay narrow. For add-feed, pagination is the
16
+ * mandatory capability; richer composer affordances are selected optional sensors.
18
17
  */
19
18
  import { readdir, readFile, stat } from "node:fs/promises";
20
19
  import path from "node:path";
21
- // Canonical, Vise-authored capability set — the SDK feature surface (grounded in
22
- // rules/*.yaml + SKILL.md, not guessed). Anchored on SDK symbol names (consistent
23
- // enough across TS/Flutter/Android/iOS to match cross-platform). All advisory.
20
+ // Canonical, Vise-authored capability catalog — the SDK feature surface
21
+ // (grounded in rules/*.yaml + SKILL.md, not guessed). Baseline gating is applied
22
+ // by baselineCapabilities(); not every catalog item is mandatory for every outcome.
24
23
  export const CAPABILITIES = [
25
24
  // ── add-feed: post dataTypes (a feed parent is usually 'text'; rich content
26
25
  // rides on child posts — render each type the feed can return) ──────────
@@ -367,23 +366,143 @@ export const CAPABILITIES = [
367
366
  hint: "respect Amity's server-side notification settings/preferences (getSettings)",
368
367
  },
369
368
  ];
370
- const ADVISORY_NOTE = "Build each missing capability, or opt out with a recorded reason: `// vise: scope-omit <id> <reason>`. Missing capabilities that are neither built nor opted-out cause `vise check` to exit with status `completeness-gap` (exit code 5).";
371
- /** The Vise-authored capability checklist for an outcome (for `vise plan` feed-forward). */
369
+ // Optional capabilities are not baseline compliance. `vise plan` offers them as
370
+ // explicit feed-forward choices; `vise init --answer feed_optional_capabilities=...`
371
+ // records selected choices; `vise check` then evaluates these source sensors.
372
+ export const OPTIONAL_CAPABILITIES = [
373
+ {
374
+ id: "post-image-upload",
375
+ label: "Image/file upload composer",
376
+ outcomes: ["add-feed"],
377
+ aliases: [/image(?:\/file)? upload/i, /image post/i, /file post/i, /attachments?/i, /media upload/i],
378
+ sensors: [
379
+ { label: "FileRepository.uploadImage or uploadImage", regex: /FileRepository\.uploadImage\s*\(|(?<![.\w])uploadImage\s*\(/ },
380
+ { label: "createPost attachments array", regex: /createPost\s*\(\s*\{[\s\S]*?attachments\s*:/ },
381
+ ],
382
+ hint: "If the user opts into media posting, implement file picker -> FileRepository.uploadImage(file) -> createPost({ ..., attachments: [{ fileId, fileType: 'image' }] }).",
383
+ },
384
+ {
385
+ id: "post-poll-creation",
386
+ label: "Poll composer and voting",
387
+ outcomes: ["add-feed"],
388
+ aliases: [/poll creation/i, /poll composer/i, /create poll/i, /\bpolls?\b/i],
389
+ sensors: [
390
+ { label: "PollRepository.createPoll", regex: /PollRepository\.createPoll\s*\(|(?<![.\w])createPoll\s*\(/ },
391
+ {
392
+ label: "createPost links pollId",
393
+ regex: /createPost\s*\(\s*\{[\s\S]*?data\s*:\s*\{[\s\S]*?\bpollId\b\s*(?::|[,}])/,
394
+ },
395
+ { label: "PollRepository.votePoll", regex: /PollRepository\.votePoll\s*\(|(?<![.\w])votePoll\s*\(/ },
396
+ ],
397
+ hint: "If the user opts into polls, implement the createPoll -> createPost({ data: { pollId } }) chain and the votePoll read-side interaction.",
398
+ },
399
+ {
400
+ id: "post-edit",
401
+ label: "Edit own post",
402
+ outcomes: ["add-feed"],
403
+ aliases: [/edit post/i, /edit own post/i, /author edit/i, /\beditPost\b/i],
404
+ sensors: [
405
+ { label: "PostRepository.editPost", regex: /PostRepository\.editPost\s*\(|(?<![.\w])editPost\s*\(/ },
406
+ ],
407
+ hint: "If the user opts into author management, show edit only for post.postedUserId === currentUserId and call PostRepository.editPost with updated text data.",
408
+ },
409
+ ];
410
+ const ADVISORY_NOTE = "Build each missing capability, or opt out with a recorded reason: `// vise: scope-omit <id> — <reason>`. A scope-omit marker without a reason is invalid and still counts as missing. Missing capabilities that are neither built nor validly opted-out cause `vise check` to exit with status `completeness-gap` (exit code 5).";
411
+ const BASELINE_CAPABILITY_IDS_BY_OUTCOME = {
412
+ "add-feed": ["pagination"],
413
+ };
414
+ function baselineCapabilities(outcome) {
415
+ const caps = CAPABILITIES.filter((c) => c.outcomes.includes(outcome));
416
+ const baselineIds = BASELINE_CAPABILITY_IDS_BY_OUTCOME[outcome];
417
+ if (!baselineIds) {
418
+ return caps;
419
+ }
420
+ const baseline = new Set(baselineIds);
421
+ return caps.filter((capability) => baseline.has(capability.id));
422
+ }
423
+ /** The Vise-authored baseline capability checklist for an outcome. */
372
424
  export function capabilityChecklist(outcome) {
373
- return CAPABILITIES.filter((c) => c.outcomes.includes(outcome)).map((c) => ({ id: c.id, label: c.label, hint: c.hint }));
425
+ return baselineCapabilities(outcome).map((c) => ({ id: c.id, label: c.label, hint: c.hint }));
426
+ }
427
+ /** Optional feed-forward choices. These are not baseline completeness requirements. */
428
+ export function optionalCapabilityChecklist(outcome) {
429
+ return OPTIONAL_CAPABILITIES.filter((c) => c.outcomes.includes(outcome)).map((c) => ({ id: c.id, label: c.label, hint: c.hint }));
430
+ }
431
+ export function selectedOptionalCapabilityIds(outcome, answers = {}, _request = "") {
432
+ const candidates = OPTIONAL_CAPABILITIES.filter((capability) => capability.outcomes.includes(outcome));
433
+ if (candidates.length === 0) {
434
+ return [];
435
+ }
436
+ const answerText = [answers.feed_optional_capabilities, answers.optional_capabilities, answers.selected_capabilities]
437
+ .filter(Boolean)
438
+ .join("\n");
439
+ const normalized = answerText.toLowerCase();
440
+ if (!normalized.trim() || /\b(none|no optional|default only)\b/.test(normalized)) {
441
+ return [];
442
+ }
443
+ const selected = new Set();
444
+ for (const capability of candidates) {
445
+ if (normalized.includes(capability.id.toLowerCase()) || capability.aliases.some((alias) => alias.test(answerText))) {
446
+ selected.add(capability.id);
447
+ }
448
+ }
449
+ return [...selected].sort();
450
+ }
451
+ export function assessSelectedOptionalCapabilities(source, outcome, selectedIds = []) {
452
+ const selected = [...new Set(selectedIds.map((id) => id.trim()).filter(Boolean))].sort();
453
+ if (selected.length === 0) {
454
+ return null;
455
+ }
456
+ const applicable = new Map(OPTIONAL_CAPABILITIES.filter((capability) => capability.outcomes.includes(outcome)).map((capability) => [capability.id, capability]));
457
+ const passed = [];
458
+ const failed = [];
459
+ const unknown = [];
460
+ for (const id of selected) {
461
+ const capability = applicable.get(id);
462
+ if (!capability) {
463
+ unknown.push(id);
464
+ continue;
465
+ }
466
+ const present = capability.sensors.filter((sensor) => sensor.regex.test(source)).map((sensor) => sensor.label);
467
+ const missing = capability.sensors.filter((sensor) => !sensor.regex.test(source)).map((sensor) => sensor.label);
468
+ if (missing.length === 0) {
469
+ passed.push({ id, label: capability.label, present });
470
+ }
471
+ else {
472
+ failed.push({ id, label: capability.label, missing, hint: capability.hint });
473
+ }
474
+ }
475
+ return {
476
+ outcome,
477
+ selected,
478
+ passed,
479
+ failed,
480
+ unknown,
481
+ note: "Selected optional capabilities are enforced only after the user or host agent opts in through `feed_optional_capabilities`. They are source sensors, not baseline compliance rules.",
482
+ };
374
483
  }
375
484
  /** Pure assessment over already-read source text. */
376
485
  export function assessCompleteness(source, outcome) {
377
- const caps = CAPABILITIES.filter((c) => c.outcomes.includes(outcome));
486
+ const caps = baselineCapabilities(outcome);
378
487
  const optOuts = new Map();
488
+ const invalidOptOuts = new Map();
379
489
  const omitPattern = /vise:\s*scope-omit\s+([a-z][\w-]*)\s*(?:[—:|-]+\s*(.*))?/gi;
380
490
  let match;
381
491
  while ((match = omitPattern.exec(source)) !== null) {
382
- optOuts.set(match[1].toLowerCase(), (match[2] ?? "").trim() || "no reason given");
492
+ const id = match[1].toLowerCase();
493
+ const reason = (match[2] ?? "").trim();
494
+ if (reason) {
495
+ optOuts.set(id, reason);
496
+ invalidOptOuts.delete(id);
497
+ }
498
+ else if (!optOuts.has(id)) {
499
+ invalidOptOuts.set(id, "scope-omit marker must include a reason");
500
+ }
383
501
  }
384
502
  const present = [];
385
503
  const missing = [];
386
504
  const optedOut = [];
505
+ const invalid = [];
387
506
  for (const cap of caps) {
388
507
  if (optOuts.has(cap.id)) {
389
508
  optedOut.push({ id: cap.id, reason: optOuts.get(cap.id) });
@@ -392,10 +511,18 @@ export function assessCompleteness(source, outcome) {
392
511
  present.push({ id: cap.id, label: cap.label });
393
512
  }
394
513
  else {
395
- missing.push({ id: cap.id, label: cap.label, hint: cap.hint });
514
+ const invalidReason = invalidOptOuts.get(cap.id);
515
+ if (invalidReason) {
516
+ invalid.push({ id: cap.id, reason: invalidReason });
517
+ }
518
+ missing.push({
519
+ id: cap.id,
520
+ label: cap.label,
521
+ hint: invalidReason ? `${cap.hint}; found scope-omit marker without a reason, so add a reason or implement it` : cap.hint,
522
+ });
396
523
  }
397
524
  }
398
- return { outcome, present, missing, optedOut, note: ADVISORY_NOTE };
525
+ return { outcome, present, missing, optedOut, invalidOptOuts: invalid, note: ADVISORY_NOTE };
399
526
  }
400
527
  // ── Bounded source read (advisory; perf-bounded like the design check scan) ──
401
528
  const SCAN_EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".dart", ".kt", ".java", ".swift", ".vue"]);
@@ -403,9 +530,15 @@ const SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build", ".next", "ou
403
530
  const MAX_FILES = 1500;
404
531
  const MAX_FILE_BYTES = 1_000_000;
405
532
  export async function assessProjectCompleteness(root, outcome) {
406
- if (CAPABILITIES.every((c) => !c.outcomes.includes(outcome))) {
533
+ if (baselineCapabilities(outcome).length === 0) {
407
534
  return null; // outcome has no completeness checklist
408
535
  }
536
+ return assessCompleteness(await readProjectSource(root), outcome);
537
+ }
538
+ export async function assessProjectSelectedOptionalCapabilities(root, outcome, selectedIds = []) {
539
+ return assessSelectedOptionalCapabilities(await readProjectSource(root), outcome, selectedIds);
540
+ }
541
+ async function readProjectSource(root) {
409
542
  const resolved = path.resolve(root);
410
543
  const parts = [];
411
544
  const stack = [resolved];
@@ -443,5 +576,5 @@ export async function assessProjectCompleteness(root, outcome) {
443
576
  }
444
577
  }
445
578
  }
446
- return assessCompleteness(parts.join("\n"), outcome);
579
+ return parts.join("\n");
447
580
  }
package/dist/server.js CHANGED
@@ -218,8 +218,8 @@ async function handleCli(args) {
218
218
  return "exit";
219
219
  }
220
220
  if (command === "init") {
221
- assertOnlyKnownFlags(args, ["request", "surface", "surface-path"], "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")), null, 2));
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));
223
223
  return "exit";
224
224
  }
225
225
  if (command === "check") {
@@ -501,7 +501,8 @@ Usage:
501
501
  Initialize the local sp-vise compliance sidecar for one integration request.
502
502
 
503
503
  Usage:
504
- vise init [repoPath] --request "Add a social feed" [--surface apps/web]`;
504
+ 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`;
505
506
  }
506
507
  if (command === "check") {
507
508
  return `${packageName} check
@@ -2,7 +2,7 @@ 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 } from "../capabilities.js";
5
+ import { assessProjectCompleteness, assessProjectSelectedOptionalCapabilities, selectedOptionalCapabilityIds, } from "../capabilities.js";
6
6
  import { classifyOutcome } from "../outcomes.js";
7
7
  import { objectInput, optionalStringField, stringField, textResult } from "../types.js";
8
8
  import { packageVersion } from "../version.js";
@@ -21,13 +21,18 @@ export const initComplianceTool = {
21
21
  repoPath: { type: "string" },
22
22
  request: { type: "string" },
23
23
  surfacePath: { type: "string" },
24
+ answers: {
25
+ type: "object",
26
+ description: "Optional intake answers from vise plan, including feed_optional_capabilities.",
27
+ additionalProperties: { type: "string" },
28
+ },
24
29
  },
25
30
  required: ["repoPath", "request"],
26
31
  additionalProperties: false,
27
32
  },
28
33
  async call(input) {
29
34
  const args = objectInput(input);
30
- return textResult(await initCompliance(stringField(args, "repoPath"), stringField(args, "request"), optionalStringField(args, "surfacePath")));
35
+ return textResult(await initCompliance(stringField(args, "repoPath"), stringField(args, "request"), optionalStringField(args, "surfacePath"), answersFromInput(args.answers)));
31
36
  },
32
37
  };
33
38
  export const checkComplianceTool = {
@@ -46,6 +51,18 @@ export const checkComplianceTool = {
46
51
  return textResult(await checkCompliance(stringField(args, "repoPath")));
47
52
  },
48
53
  };
54
+ function answersFromInput(raw) {
55
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
56
+ return {};
57
+ }
58
+ const answers = {};
59
+ for (const [key, value] of Object.entries(raw)) {
60
+ if (typeof value === "string" && value.trim() !== "") {
61
+ answers[key] = value;
62
+ }
63
+ }
64
+ return answers;
65
+ }
49
66
  export const syncComplianceTool = {
50
67
  name: "sync_compliance",
51
68
  description: "Persist current deterministic-pass compliance results into the sp-vise sidecar.",
@@ -225,10 +242,11 @@ async function readEngagement(repoRoot) {
225
242
  function engagementPath(repoRoot) {
226
243
  return path.join(sidecarDir(repoRoot), "engagement.json");
227
244
  }
228
- export async function initCompliance(repoPath, request, surfacePath) {
245
+ export async function initCompliance(repoPath, request, surfacePath, answers = {}) {
229
246
  const repoRoot = path.resolve(repoPath);
230
247
  const inspection = await inspectProject(repoRoot, surfacePath);
231
248
  const outcome = classifyOutcome(request);
249
+ const selectedOptionalCapabilities = selectedOptionalCapabilityIds(outcome, answers, request);
232
250
  const rules = await applicableRules(outcome, inspection.platforms);
233
251
  const refs = rules.map(ruleRef); // minimal shape — stable digest input
234
252
  const fileRefs = rules.map(ruleRefForFile); // adds title for human/agent readers
@@ -254,6 +272,7 @@ export async function initCompliance(repoPath, request, surfacePath) {
254
272
  inferred_tokens: designContract.stats.inferred_tokens,
255
273
  }
256
274
  : undefined,
275
+ selected_optional_capabilities: selectedOptionalCapabilities.length > 0 ? selectedOptionalCapabilities : undefined,
257
276
  };
258
277
  await mkdir(attestationsDir(repoRoot), { recursive: true });
259
278
  await writeJson(compliancePath(repoRoot), compliance);
@@ -282,6 +301,7 @@ export async function initCompliance(repoPath, request, surfacePath) {
282
301
  rules: refs.length,
283
302
  engagement_id: engagement?.engagement_id,
284
303
  ...(compliance.design_contract && { design_contract: compliance.design_contract }),
304
+ ...(selectedOptionalCapabilities.length > 0 && { selected_optional_capabilities: selectedOptionalCapabilities }),
285
305
  ...(warnings.length > 0 && { warnings }),
286
306
  nextStep: "Run vise check, then implement until rules pass deterministically or are attested.",
287
307
  };
@@ -454,31 +474,48 @@ export async function checkCompliance(repoPath) {
454
474
  const hasDeterministicFailure = results.some((result) => result.status === "deterministic-fail");
455
475
  // "advisory" status is intentionally excluded — advisory rules surface but never block.
456
476
  const needsAttestation = results.some((result) => result.status === "attestation-needed" || result.status === "stale");
457
- // Precedence: blocked (3) > deterministic-failures (2) > needs-attestation (1) > completeness-gap (5) > green (0).
477
+ // Precedence: blocked (3) > deterministic-failures (2) > needs-attestation (1) >
478
+ // completeness-gap (5) > selected-capability-failures (6) > green (0).
458
479
  // Contract drift (exit 4) is handled earlier and short-circuits the loop.
459
- // Completeness-gap: capabilities that are neither present nor opted-out require an explicit decision
480
+ // Completeness-gap: capabilities that are neither present nor validly opted-out require an explicit decision
460
481
  // (build it, or place // vise: scope-omit <id> — <reason>). The scope-omit escape hatch keeps this
461
- // FP-free any capability can be excluded with a recorded reason. Failure to assess is silently ignored.
482
+ // FP-free because any capability can be excluded with a recorded reason. Failure to assess is silently ignored.
462
483
  const completeness = (await assessProjectCompleteness(inspection.effectiveRoot, compliance.outcome).catch(() => null)) ?? undefined;
463
484
  const hasCompletenessGap = (completeness?.missing.length ?? 0) > 0;
485
+ const selectedOptionalCapabilities = (await assessProjectSelectedOptionalCapabilities(inspection.effectiveRoot, compliance.outcome, compliance.selected_optional_capabilities ?? []).catch(() => null)) ?? undefined;
486
+ const hasSelectedOptionalFailures = ((selectedOptionalCapabilities?.failed.length ?? 0) > 0) || ((selectedOptionalCapabilities?.unknown.length ?? 0) > 0);
464
487
  // Blocked wins because the agent cannot proceed without customer input;
465
488
  // surfacing a smaller failure first would distract from the real blocker.
489
+ const status = hasBlocked
490
+ ? "blocked"
491
+ : hasDeterministicFailure
492
+ ? "deterministic-failures"
493
+ : needsAttestation
494
+ ? "needs-attestation"
495
+ : hasCompletenessGap
496
+ ? "completeness-gap"
497
+ : hasSelectedOptionalFailures
498
+ ? "selected-capability-failures"
499
+ : "green";
466
500
  return {
467
- status: hasBlocked
468
- ? "blocked"
501
+ status,
502
+ exitCode: hasBlocked
503
+ ? 3
469
504
  : hasDeterministicFailure
470
- ? "deterministic-failures"
505
+ ? 2
471
506
  : needsAttestation
472
- ? "needs-attestation"
507
+ ? 1
473
508
  : hasCompletenessGap
474
- ? "completeness-gap"
475
- : "green",
476
- exitCode: hasBlocked ? 3 : hasDeterministicFailure ? 2 : needsAttestation ? 1 : hasCompletenessGap ? 5 : 0,
509
+ ? 5
510
+ : hasSelectedOptionalFailures
511
+ ? 6
512
+ : 0,
477
513
  outcome: compliance.outcome,
478
514
  surfacePath: compliance.surface?.path,
479
515
  summary,
480
516
  rules: results,
481
517
  ...(completeness && (completeness.missing.length > 0 || completeness.optedOut.length > 0 || completeness.present.length > 0) ? { completeness } : {}),
518
+ ...(selectedOptionalCapabilities ? { selectedOptionalCapabilities } : {}),
482
519
  };
483
520
  }
484
521
  export async function syncCompliance(repoPath) {
@@ -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, classifyOutcome, getOutcomeDefinition, hasAnswer, planContextFor, } from "../outcomes.js";
4
4
  import { objectInput, optionalStringField, stringField, textResult } from "../types.js";
5
- import { capabilityChecklist } from "../capabilities.js";
5
+ import { capabilityChecklist, optionalCapabilityChecklist, selectedOptionalCapabilityIds } from "../capabilities.js";
6
6
  import { applicableComplianceRuleSummaries } from "./compliance.js";
7
7
  import { buildDesignBrief, readDesignContract } from "./design.js";
8
8
  import { sdkVersionGuidance } from "./sdkVersion.js";
@@ -122,6 +122,7 @@ async function buildIntegrationPlan(repoPath, request, surfacePath, answers = {}
122
122
  evidencePolicy: "Every implementation step must cite at least one detected file, docs page, validator rule, or required user input. If evidence is missing, stop and ask the user instead of inventing details.",
123
123
  designContract: designContract ? designContractGuidance(designContract) : undefined,
124
124
  completenessChecklist: completenessChecklistFor(outcome),
125
+ optionalCapabilities: optionalCapabilitiesFor(outcome, ctx.answers, request),
125
126
  sdkVersion,
126
127
  };
127
128
  }
@@ -139,6 +140,18 @@ function completenessChecklistFor(outcome) {
139
140
  capabilities: items,
140
141
  };
141
142
  }
143
+ function optionalCapabilitiesFor(outcome, answers, request) {
144
+ const choices = optionalCapabilityChecklist(outcome);
145
+ if (choices.length === 0) {
146
+ return undefined;
147
+ }
148
+ return {
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.",
151
+ choices,
152
+ selected: selectedOptionalCapabilityIds(outcome, answers, request),
153
+ };
154
+ }
142
155
  // Build advisory UI-generation guidance from an extracted design contract.
143
156
  // Declared tokens are surfaced with their custom-property name (so the agent
144
157
  // references `var(--x)` / maps it per platform); inferred tokens carry their
@@ -216,6 +229,16 @@ function intakeFor(ctx, outcomeQuestions, outcome, brief) {
216
229
  blocksImplementationWhenMissing: false,
217
230
  });
218
231
  }
232
+ if (outcome === "add-feed" && optionalCapabilityChecklist(outcome).length > 0 && !hasAnswer(ctx.answers, "feed_optional_capabilities")) {
233
+ questions.push({
234
+ id: "feed_optional_capabilities",
235
+ question: "Which optional feed capabilities should be in scope: post-image-upload, post-poll-creation, post-edit, or none?",
236
+ why: "These capabilities are useful for full feeds, but should become enforceable only after the customer explicitly opts in.",
237
+ required: false,
238
+ blocksImplementationWhenMissing: false,
239
+ options: ["none", ...optionalCapabilityChecklist(outcome).map((capability) => capability.id)],
240
+ });
241
+ }
219
242
  const remainingBlocking = questions.filter((question) => question.blocksImplementationWhenMissing).length;
220
243
  return {
221
244
  status: remainingBlocking > 0 ? "needs-clarification" : "ready",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amityco/social-plus-vise",
3
- "version": "0.14.3",
3
+ "version": "0.14.5",
4
4
  "description": "Skill-guided deterministic CLI for social.plus SDK integration assistance.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",
@@ -37,7 +37,7 @@ 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
- **Completeness is also a stop condition.** When `vise check .` exits with status `completeness-gap` (exit code 5), one or more expected capabilities are neither implemented nor opted-out. 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 capability that is neither built nor opted-out is a silent drop — the check will not pass green until every capability is resolved.
40
+ **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
41
 
42
42
  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.
43
43
 
@@ -244,13 +244,15 @@ vise plan . --request "<feed or post request>"
244
244
 
245
245
  Require a concrete target from the app: current user feed, selected community, selected channel, or another user-provided domain object. Do not hardcode random target IDs.
246
246
 
247
- **Decide engagement scope explicitly — Vise authors the checklist, you subtract with a reason.** `vise plan` returns a `completenessChecklist` (the canonical capabilities for the outcome, e.g. comments, reactions, pagination, polls, media, moderation) and `vise check` reports each as present / missing / opted-out. The check gate never blocks on completeness (to avoid false positives from legitimately scope-limited integrations), but **missing items that are neither built nor opted-out are silent drops treat them as a stop condition, not a suggestion.** For each capability: build it, or explicitly opt out with a recorded marker in the code so the omission is reviewable, not accidental:
247
+ **Decide engagement scope explicitly — Vise authors the baseline checklist, and optional feed capabilities require user opt-in.** `vise plan` returns a `completenessChecklist` for baseline capabilities (for add-feed today: pagination) and an `optionalCapabilities` block for feed-forward choices such as image upload, poll creation, and edit post. `vise check` reports baseline capabilities as present / missing / opted-out. Missing baseline items that are neither built nor validly opted-out produce `completeness-gap` (exit code 5), because they are silent drops. For each baseline capability: build it, or explicitly opt out with a recorded marker in the code so the omission is reviewable, not accidental:
248
248
 
249
249
  ```
250
- // vise: scope-omit polltext + image feed only; polls disabled for this tenant
250
+ // vise: scope-omit paginationsingle-screen feed; no load-more affordance in this integration
251
251
  ```
252
252
 
253
- Do not report the integration complete while any capability is `missing`. Re-run `vise check .` after placing a scope-omit marker to confirm it moves from `missing` to `opted-out`.
253
+ Do not report the integration complete while any baseline capability is `missing`. A scope-omit marker must include a reason after the capability id; otherwise it remains invalid and the capability still counts as missing. Re-run `vise check .` after placing a scope-omit marker to confirm it moves from `missing` to `opted-out`.
254
+
255
+ Optional feed choices are not baseline compliance. If the user explicitly chooses one during planning, carry that decision into init, for example `vise init . --request "Add a social feed" --answer feed_optional_capabilities=post-poll-creation`. Once recorded, `vise check .` runs the selected source sensors and exits `selected-capability-failures` (exit code 6) until the selected capability is implemented.
254
256
 
255
257
  **Demo wiring (when the app needs to compile before the real target source exists):** even at the top-level `App` / `MaterialApp` / `_app.tsx` / `AppDelegate` wiring site, do not pass a literal string. Use the host platform's compile-time env channel so the rule sees no string literal at the call site:
256
258