@amityco/social-plus-vise 1.6.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +44 -0
- package/README.md +10 -4
- package/dist/flow.js +6 -0
- package/dist/server.js +71 -18
- package/dist/tools/blocks.js +82 -9
- package/dist/tools/compliance.js +315 -27
- package/dist/tools/internalWorkspace.js +44 -0
- package/dist/tools/project.js +116 -29
- package/dist/tools/sdkFacts.js +233 -7
- package/dist/tools/sensors.js +102 -36
- package/package.json +2 -2
- package/packages/intelligence/catalog/catalog.schema.json +6 -0
- package/packages/intelligence/catalog/experience-objects.json +11 -11
- package/sdk-surface/manifest.json +4 -4
- package/sdk-surface/models.typescript.json +2903 -463
- package/skills/social-plus-vise/SKILL.md +20 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,50 @@ 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
|
+
## 1.8.0 — 2026-07-10
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **Complete-scan receipts and gates.** Project inspection and platform validators now report bounded scan coverage. A truncated inventory cannot initialize a compliance sidecar, select sensors, or produce a green `vise check`; large monorepos must select the concrete app surface.
|
|
11
|
+
- **Pre-launch release contracts.** Hosted CI now smokes the declared Node 20 minimum, release metadata/docs are regression-tested, and tag publishing uses npm trusted publishing with OIDC, a modern npm CLI, main-ancestry validation, and no long-lived token fallback.
|
|
12
|
+
- **Critical regression coverage.** New tests lock scan truncation, mixed-platform inventory detection, strict sensor selection, bounded command output, descendant cleanup on timeout, block mount proof, and the release workflow contract.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- **CI covers finished work by default.** `vise check --ci` now runs the active gate plus every recorded engagement re-verdict. A previously completed surface that drifted exits `11` even when the active engagement is green; explicit `--all-engagements` remains valid with `--ci`.
|
|
16
|
+
- **Block capabilities require a live mount.** The blocks sidecar is reset to `2026-07-10.vise-blocks-sidecar.v2`. `blocks add --apply` records `pending-mount`; `vise check` counts `providesCapabilities` only while the dependency, touched files, package import, and actual block-symbol usage remain present. `blocks validate` persists `verified` or `invalid` and exits non-zero on failure.
|
|
17
|
+
- **Explicit sensor selection is strict.** A requested `--include` name that does not match a detected sensor returns `invalid-selection` and exits non-zero. Sensor stdout/stderr is bounded, and timeouts terminate the command process group rather than leaving descendants running.
|
|
18
|
+
- **Node.js 20 is the minimum supported runtime.** Package metadata, `vise doctor`, release documentation, and hosted compatibility proof now agree.
|
|
19
|
+
- **The compatibility corpus is reset to the 1.8.0 launch contract.** Experimental pre-launch sidecars are not grandfathered; future published versions must remain backward-readable from this baseline.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
- **Ducati dogfood false positives.** Built-in `{ text }` shorthand payloads no longer look like undeclared custom post types, and comment modules that carry a bare `postId` no longer look like post cards missing their body.
|
|
23
|
+
- **Engagement-aware evidence hygiene.** Matching attestations retained by completed engagement contracts are no longer reported as generic stale files. Off-contract attestations with changed source fingerprints are identified explicitly.
|
|
24
|
+
- **Flutter hosted block dependencies.** Hosted constraints now render as `social_plus_blocks: ^0.1.0`; only local sources render a nested `path:` dependency.
|
|
25
|
+
- **Fixture-symmetry parsing.** The supplementary symmetry gate parses only `failingFixturesByFinding`, so later no-fire maps cannot overwrite the failing fixture associated with a rule.
|
|
26
|
+
|
|
27
|
+
### Compatibility
|
|
28
|
+
- This is an intentional pre-launch contract hardening release. The stricter scan, sensor, block-mount, Node, CI, and sidecar behaviors differ from the experimental 1.7.0 line, but Vise and Blocks have not launched and have no customer contract to migrate. The release version remains 1.8.0 as requested; the new sidecar schema keeps its independent `.v2` schema identifier.
|
|
29
|
+
|
|
30
|
+
## 1.7.0 — 2026-07-09
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
- **Day-2 governance: adding features to an app that already uses social.plus is now a first-class, governed flow.**
|
|
34
|
+
- **Brownfield adoption honesty.** `vise init` on a repo with pre-existing non-green findings surfaces a `brownfield` hint: record `vise baseline .` before writing code, then gate with `vise check --new-only`. A baseline excludes pre-existing *rule findings only* — the requested outcome's completeness checklist and selected capabilities are never baselined, so a baselined repo can't go green without the feature actually built. Baseline files recorded by older versions that carried deliverable entries are ignored and disclosed (`ignored_legacy_deliverable_entries`).
|
|
35
|
+
- **Engagement ledger.** Initializing a different outcome records the outgoing engagement's final check, contract, and intake to `sp-vise/engagements/<outcome>/` (latest per outcome) and discloses the switch in the init result. `vise status` lists superseded engagements under `previousEngagements`. Replacing a **green** engagement asks for an explicit `--supersede-engagement`.
|
|
36
|
+
- **Re-verification of finished work.** `vise check --engagement <outcome>` re-verdicts one recorded engagement against the current rules and code (exit `11` on drift, `0` when it holds; `ruleset_upgraded` disclosed). `vise check --all-engagements` runs the normal gate plus re-verdicts of every recorded engagement — active gate non-green keeps its own exit code; active green with a drifted recorded engagement exits `11` (`engagement-drift`). Findings already gating the active engagement are counted once there (`shared_with_active`).
|
|
37
|
+
- **Attestations are bound to their engagement.** `vise attest` stamps the active outcome; after an outcome switch, a rule-global attestation re-gates (`stale_engagement_attestation`) until re-verified — file-scoped attestations survive the switch because new files re-gate them anyway. Attestations recorded by older versions keep their historical behavior.
|
|
38
|
+
- **Completed journeys don't dead-end new requests.** After a finished multi-surface journey, `vise init` for a genuinely new outcome now proceeds (recording the completed journey to the ledger) instead of stopping at `workplan-complete`; re-initializing a surface the journey already completed still gets the honest stop.
|
|
39
|
+
- New skill section — "Adding Features To An App That Already Uses social.plus" — teaches agents both variants (hand-rolled integration and prior governed engagement).
|
|
40
|
+
- **SDK facts: broader capability coverage and deeper model grounding.** `vise sdk-facts` (now listed in the public `vise --help`) gains capability anchors for `community`, `chat`, `livestream` (Room API only), `notification-tray`, and `events` on TypeScript, plus per-platform anchor sets with Flutter coverage for six families (`comments`, `reactions`, `posts`, `users`, `community`, `chat`) — a partially anchored platform's result names its coverage explicitly. Field-level model schemas grew from 6 to 14 TypeScript models (`Amity.Community`, `Amity.Channel`, `Amity.Message`, `Amity.SubChannel`, `Amity.Room`, `Amity.NotificationTrayItem`, `Amity.NotificationTraySeen`, `Amity.Event`), all extraction-grounded with real field names and types.
|
|
41
|
+
- **Development-workspace guard.** Customer-integration commands now recognize the Vise / social.plus blocks development workspace itself: `vise init` refuses with `status: "internal-workspace"` (exit `10`, `--allow-internal-workspace` to override for harness runs), and `inspect`/`plan` attach an advisory warning — so the projectless `vise sdk-facts` persona and the customer persona can't be mixed up silently.
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
- **Blocks installer on native projects.** `vise blocks plan`/`add`/`validate` pointed at a project without a package manifest (an iOS or Android app) now return the graceful `needs-review` plan with unsupported-platform stop conditions instead of crashing on a missing `package.json`.
|
|
45
|
+
|
|
46
|
+
### Compatibility
|
|
47
|
+
- **Additive exit codes.** `10` (`internal-workspace`, init-only) and `11` (`engagement-drift`, only under the new `--engagement`/`--all-engagements` flags). The existing 0–9 exit surface and precedence are unchanged; default `vise check` behavior is unchanged, so pipelines gating on `--ci` or `exitCode === 0` are unaffected.
|
|
48
|
+
- **One intentional init behavior change.** `vise init` replacing a **green** engagement with a different outcome now requires `--supersede-engagement` (exit `7` otherwise); red or in-progress engagements switch as before, and the outgoing engagement's final state is recorded to the ledger either way.
|
|
49
|
+
- **Sidecar additions are additive.** `sp-vise/engagements/` and the new result fields (`brownfield`, `engagement_switch`, `previousEngagements`, `stale_engagement_attestation`) are new; no existing file changes shape. Legacy baselines and attestations continue to parse — with the honesty adjustments disclosed above.
|
|
50
|
+
|
|
7
51
|
## 1.6.0 — 2026-07-06
|
|
8
52
|
|
|
9
53
|
### Added
|
package/README.md
CHANGED
|
@@ -206,7 +206,7 @@ Run `vise <command> --help` for full flags. JSON output is the default for agent
|
|
|
206
206
|
| `vise plan [path] --request "..." [--summary]` | Grounded implementation plan with intake questions and docs citations; `--summary` prints a compact route/intake view |
|
|
207
207
|
| `vise plan --summary "..."` | Shortcut for quick routing or discovery when the current directory is the repo |
|
|
208
208
|
| `vise plan-harness [path] --request "..."` | Pre-planning step: build the harness around the request |
|
|
209
|
-
| `vise init [path] --request "..." [--answer key=value]` | Write the `sp-vise/` compliance contract once blocking intake is answered; exits 7 (`needs-clarification`) otherwise |
|
|
209
|
+
| `vise init [path] --request "..." [--answer key=value]` | Write the `sp-vise/` compliance contract once blocking intake is answered; exits 7 (`needs-clarification`) otherwise. Refuses to initialize the Vise / social.plus blocks development workspace itself (`status: "internal-workspace"`, exit 10 — use `vise sdk-facts` there, or `--allow-internal-workspace` for an intentional harness run). Replacing a **green** engagement with a different outcome requires `--supersede-engagement` (exit 7 otherwise); the outgoing engagement's final state is recorded to `sp-vise/engagements/` either way |
|
|
210
210
|
| `vise workplan next [path] --request "..."` | For broad social requests: print the next uncompleted surface and its focused commands |
|
|
211
211
|
| `vise workplan status [path] --request "..."` | Show the workplan sequence and completed surfaces; ignores unverifiable or drifted snapshot evidence. If the in-scope surface's live `vise check` is already green, auto-records that one surface (same evidence bar as `workplan complete`) so a working, check-green surface isn't left uncounted |
|
|
212
212
|
| `vise workplan complete [path] --request "..." --surface <id>` | Record a green-checked surface; snapshots evidence plus an integrity manifest under `sp-vise/workplan-snapshots/<surface>/` |
|
|
@@ -255,11 +255,13 @@ Everything in this group is local and advisory: no uploads, no `vise check` exit
|
|
|
255
255
|
| `vise check [path]` | Re-validate against the recorded contract: `green`, `needs-attestation`, `deterministic-failures`, `blocked`, `contract-drift`, `completeness-gap`, `selected-capability-failures`, `no-platform` (no SDK platform detected — reported as a failure, not a vacuous green), or `runtime-proof-waived` |
|
|
256
256
|
| `vise check [path] --ci` | Read-only variant that exits non-zero unless green (for CI) |
|
|
257
257
|
| `vise check [path] --allow-proof-waiver` | Accept an honest `runtime-proof-waived` result (from `vise smoke waive`) as passing (exit 0) instead of blocking; without it a waiver blocks CI by default |
|
|
258
|
-
| `vise check [path] --new-only` | **Brownfield gate.** With a recorded baseline, gate only on findings introduced *since* the baseline (pre-existing ones are reported but excluded). Default `check` always gates on everything |
|
|
259
|
-
| `vise
|
|
258
|
+
| `vise check [path] --new-only` | **Brownfield gate.** With a recorded baseline, gate only on rule findings introduced *since* the baseline (pre-existing ones are reported but excluded). The outcome's completeness checklist and selected optional capabilities are **never** excluded — they are the engagement's deliverables and gate like a default check. Default `check` always gates on everything |
|
|
259
|
+
| `vise check [path] --engagement <outcome>` | **Re-verdict one recorded engagement** (from `sp-vise/engagements/`) against the current ruleset and code. Exit `11` on drift, `0` when it still holds; never the active engagement's codes. `ruleset_upgraded` discloses when the installed ruleset moved since it was recorded |
|
|
260
|
+
| `vise check [path] --all-engagements` | Run the normal active gate **plus** re-verdicts of every recorded engagement. Active gate non-green → its normal exit code; active green but a recorded engagement drifted → exit `11` (`engagement-drift`). Findings already gating the active engagement are counted once there (`shared_with_active`) |
|
|
261
|
+
| `vise baseline [path]` | Snapshot the current pre-existing rule findings to `sp-vise/baseline.json` so `check --new-only` can separate legacy debt from new gaps. Deliverables (completeness/selected capabilities) are never baselined. Record it right after `vise init`, before writing code — a mid-build baseline would swallow your own findings. `vise init --baseline` records it at init time |
|
|
260
262
|
| `vise validate [path]` | Run the deterministic validators only (no attestation comparison) |
|
|
261
263
|
| `vise sync [path]` | Persist deterministic-pass evidence to `sp-vise/attestations/` |
|
|
262
|
-
| `vise attest [path] --rule <id> --signer host-agent --confidence high --evidence-file evidence.json --rationale "..."` | Record an attestation when a rule passes through architecture the deterministic check can't see |
|
|
264
|
+
| `vise attest [path] --rule <id> --signer host-agent --confidence high --evidence-file evidence.json --rationale "..."` | Record an attestation when a rule passes through architecture the deterministic check can't see. Attestations are bound to the engagement outcome they were reviewed under: after `vise init` switches the sidecar to a different outcome, a rule-global attestation re-gates (`stale_engagement_attestation`) until re-attested — file-scoped attestations survive the switch because new files re-gate them anyway |
|
|
263
265
|
| `vise explain <ruleId>` | Print a rule's rationale, evidence requirements, and remediation |
|
|
264
266
|
| `vise status [path]` | Summarize the current compliance state |
|
|
265
267
|
|
|
@@ -330,6 +332,9 @@ jobs:
|
|
|
330
332
|
| `4` | Contract drift — recorded rules no longer match the current ruleset |
|
|
331
333
|
| `5` | Baseline capability neither implemented nor opted-out (add it or place `// vise: scope-omit <id> — <reason>`) |
|
|
332
334
|
| `6` | An explicitly selected optional capability failed its source sensors |
|
|
335
|
+
| `8` | No SDK platform detected (`no-platform`) — reported as a failure, never a vacuous green |
|
|
336
|
+
| `9` | Runtime proof waived (`runtime-proof-waived`) — accept explicitly with `--allow-proof-waiver` |
|
|
337
|
+
| `11` | Engagement drift (`--engagement` / `--all-engagements` only): the active gate is green but a recorded engagement re-verdicts non-green — a previously finished surface no longer holds |
|
|
333
338
|
|
|
334
339
|
## Compliance Contract
|
|
335
340
|
|
|
@@ -341,6 +346,7 @@ Vise writes local planning, compliance, design, and evidence artifacts under `sp
|
|
|
341
346
|
| `sp-vise/intake.json` | `vise init` | Request, outcome, intake answers, design-review status |
|
|
342
347
|
| `sp-vise/inspection.json` | `vise init` | Platform, surface, and design signals detected at init |
|
|
343
348
|
| `sp-vise/attestations/*.json` | `vise sync` / `vise attest` | Per-rule evidence: signer, confidence, rationale, source fingerprints for drift detection |
|
|
349
|
+
| `sp-vise/engagements/<outcome>/*.json` | `vise init` (on an outcome switch) | Engagement ledger: the superseded engagement's final check, contract, and intake (latest per outcome) — surfaced by `vise status` as `previousEngagements` |
|
|
344
350
|
| `sp-vise/creative-brief.json` + `creative-brief.md` | `vise creative` | Advisory brief: goals, archetypes, candidate variants (JSON + human-readable) |
|
|
345
351
|
| `sp-vise/candidate-ranking-preview.json` | `vise creative --ranking-preview` | Opt-in local ranking preview for review context; `experience_score: null`, no uploads, no default-order change, no top-1 confidence claim |
|
|
346
352
|
| `sp-vise/creative-selection.json` | `vise creative accept` | Accepted variant and plan/workplan feed-forward context |
|
package/dist/flow.js
CHANGED
|
@@ -2,6 +2,12 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { sidecarDir } from "./sidecar.js";
|
|
5
|
+
export function completedWorkplanBlocksOutcome(stages, requestedOutcome) {
|
|
6
|
+
if (!requestedOutcome || requestedOutcome === "unknown") {
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
return stages.some((stage) => stage.outcome === requestedOutcome);
|
|
10
|
+
}
|
|
5
11
|
export function blueprintDigest(input) {
|
|
6
12
|
const canonical = JSON.stringify({
|
|
7
13
|
path: input.path ?? null,
|
package/dist/server.js
CHANGED
|
@@ -5,18 +5,19 @@ import os from "node:os";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { sidecarDir as sidecarPath } from "./sidecar.js";
|
|
8
|
-
import { blueprintDigest, readFlowState, readOmittedSurfaces, renderFlowBlueprint, runtimeReadinessForStages, writeFlowBlueprintHtml, writeFlowState, } from "./flow.js";
|
|
8
|
+
import { blueprintDigest, completedWorkplanBlocksOutcome, readFlowState, readOmittedSurfaces, renderFlowBlueprint, runtimeReadinessForStages, writeFlowBlueprintHtml, writeFlowState, } from "./flow.js";
|
|
9
9
|
import { normalizeSolutionPathAnswer } from "./solutionPath.js";
|
|
10
10
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
11
11
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
12
12
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
13
|
-
import { attestRule, attestRuleTool, checkCompliance, checkComplianceTool, explainRule, explainRuleTool, experienceReportTool, initCompliance, initComplianceTool, recordBaseline, initEngagement, initEngagementTool, listRules, showEngagement, showEngagementTool, statusCompliance, syncCompliance, syncComplianceTool, } from "./tools/compliance.js";
|
|
13
|
+
import { attestRule, attestRuleTool, checkAllEngagements, checkCompliance, checkEngagementReVerdict, checkComplianceTool, explainRule, explainRuleTool, experienceReportTool, initCompliance, initComplianceTool, recordBaseline, initEngagement, initEngagementTool, listRules, showEngagement, showEngagementTool, statusCompliance, syncCompliance, syncComplianceTool, } from "./tools/compliance.js";
|
|
14
14
|
import { designCheckTool, designContrastTool, designExtractTool, designInitTokensTool, designPreviewTool, designReferenceTool } from "./tools/design.js";
|
|
15
15
|
import { getDocPageTool, searchDocsTool } from "./tools/docs.js";
|
|
16
16
|
import { compileExperienceTool } from "./tools/experienceCompiler.js";
|
|
17
17
|
import { experienceSensorsTool } from "./tools/experienceSensors.js";
|
|
18
18
|
import { planHarnessTool } from "./tools/harness.js";
|
|
19
19
|
import { planIntegrationTool } from "./tools/integration.js";
|
|
20
|
+
import { detectInternalWorkspace, internalWorkspaceWarning } from "./tools/internalWorkspace.js";
|
|
20
21
|
import { recordLearningTool, showLearningTool } from "./tools/learning.js";
|
|
21
22
|
import { inspectProjectTool, validateSetupTool } from "./tools/project.js";
|
|
22
23
|
import { resolveRequestTool, suggestPatchTool } from "./tools/resolve.js";
|
|
@@ -153,10 +154,17 @@ async function handleCli(args) {
|
|
|
153
154
|
return "exit";
|
|
154
155
|
}
|
|
155
156
|
if (command === "inspect") {
|
|
156
|
-
|
|
157
|
-
|
|
157
|
+
const inspectRepo = positionalRepoPath(args.slice(1));
|
|
158
|
+
const inspectResult = await inspectProjectTool.call({
|
|
159
|
+
repoPath: inspectRepo,
|
|
158
160
|
surfacePath: flagValue(args, "surface") ?? flagValue(args, "surface-path"),
|
|
159
161
|
});
|
|
162
|
+
const inspectPayload = JSON.parse(inspectResult.content.map((item) => item.text).join("\n"));
|
|
163
|
+
const inspectWorkspace = await detectInternalWorkspace(inspectRepo);
|
|
164
|
+
if (inspectWorkspace.isInternalWorkspace) {
|
|
165
|
+
inspectPayload.internalWorkspaceWarning = internalWorkspaceWarning(inspectWorkspace);
|
|
166
|
+
}
|
|
167
|
+
console.log(JSON.stringify(inspectPayload, null, 2));
|
|
160
168
|
return "exit";
|
|
161
169
|
}
|
|
162
170
|
if (command === "creative") {
|
|
@@ -300,6 +308,10 @@ async function handleCli(args) {
|
|
|
300
308
|
const result = await planIntegrationTool.call(input);
|
|
301
309
|
const text = result.content.map((item) => item.text).join("\n");
|
|
302
310
|
const payload = JSON.parse(text);
|
|
311
|
+
const planWorkspace = await detectInternalWorkspace(positionalRepoPath(args.slice(1)));
|
|
312
|
+
if (planWorkspace.isInternalWorkspace) {
|
|
313
|
+
payload.internalWorkspaceWarning = internalWorkspaceWarning(planWorkspace);
|
|
314
|
+
}
|
|
303
315
|
emitResult("plan", hasFlag(args, "summary") ? planSummary(payload) : payload, args);
|
|
304
316
|
return "exit";
|
|
305
317
|
}
|
|
@@ -357,7 +369,7 @@ async function handleCli(args) {
|
|
|
357
369
|
surfacePath: flagValue(args, "surface") ?? flagValue(args, "surface-path"),
|
|
358
370
|
});
|
|
359
371
|
const status = toolResultStatus(printed);
|
|
360
|
-
if (status === "failed" || status === "timed-out") {
|
|
372
|
+
if (status === "failed" || status === "timed-out" || status === "invalid-selection" || status === "incomplete-analysis") {
|
|
361
373
|
process.exitCode = 1;
|
|
362
374
|
}
|
|
363
375
|
return "exit";
|
|
@@ -449,12 +461,16 @@ async function handleCli(args) {
|
|
|
449
461
|
if (sub === "validate") {
|
|
450
462
|
assertOnlyKnownFlags(subArgs, ["block", "surface", "surface-path", "registry", "format"], "blocks validate");
|
|
451
463
|
assertJsonFormat(subArgs, "blocks validate");
|
|
452
|
-
|
|
464
|
+
const validation = await validateBlockInstall({
|
|
453
465
|
repoPath: positionalRepoPath(subArgs),
|
|
454
466
|
blockId: flagValue(subArgs, "block"),
|
|
455
467
|
registryPath: requiredFlagValue(subArgs, "registry", "blocks validate requires --registry."),
|
|
456
468
|
surfacePath: flagValue(subArgs, "surface") ?? flagValue(subArgs, "surface-path"),
|
|
457
|
-
})
|
|
469
|
+
});
|
|
470
|
+
console.log(JSON.stringify(validation, null, 2));
|
|
471
|
+
if (validation.validationStatus === "failed" || validation.status === "needs-review") {
|
|
472
|
+
process.exitCode = 1;
|
|
473
|
+
}
|
|
458
474
|
return "exit";
|
|
459
475
|
}
|
|
460
476
|
console.error(`Unknown blocks subcommand: ${sub ?? "(none)"}. Expected "list", "plan", "add", or "validate".`);
|
|
@@ -462,9 +478,19 @@ async function handleCli(args) {
|
|
|
462
478
|
return "exit";
|
|
463
479
|
}
|
|
464
480
|
if (command === "init") {
|
|
465
|
-
assertOnlyKnownFlags(args, ["request", "surface", "surface-path", "answer", "allow-unresolved-intake", "baseline"], "init");
|
|
481
|
+
assertOnlyKnownFlags(args, ["request", "surface", "surface-path", "answer", "allow-unresolved-intake", "baseline", "allow-internal-workspace", "supersede-engagement"], "init");
|
|
466
482
|
const initRepo = positionalRepoPath(args.slice(1));
|
|
467
483
|
const initRequest = requiredFlagValue(args, "request", "init requires --request.");
|
|
484
|
+
const initWorkspace = await detectInternalWorkspace(initRepo);
|
|
485
|
+
if (initWorkspace.isInternalWorkspace && !hasFlag(args, "allow-internal-workspace")) {
|
|
486
|
+
console.log(JSON.stringify({
|
|
487
|
+
status: "internal-workspace",
|
|
488
|
+
...internalWorkspaceWarning(initWorkspace),
|
|
489
|
+
instruction: "Refusing to initialize the compliance sidecar here. If this really is intentional (e.g. a harness run at the workspace root), re-run with --allow-internal-workspace.",
|
|
490
|
+
}, null, 2));
|
|
491
|
+
process.exitCode = 10;
|
|
492
|
+
return "exit";
|
|
493
|
+
}
|
|
468
494
|
const initWorkplanPlan = await workplanPlan(initRepo, initRequest, args);
|
|
469
495
|
const { sequence: initSequence, omitted: initOmitted, canonicalRequest: initCanonicalRequest } = await activeBlueprint(initRepo, initRequest, initWorkplanPlan);
|
|
470
496
|
if (initSequence.length >= 2 && initWorkplanPlan.platform !== "unknown") {
|
|
@@ -474,15 +500,16 @@ async function handleCli(args) {
|
|
|
474
500
|
process.exitCode = 7;
|
|
475
501
|
return "exit";
|
|
476
502
|
}
|
|
477
|
-
const workplanInitGate = await requireNextWorkplanSurfaceInit(initRepo, initCanonicalRequest, initSequence, keyValueFlag(args, "answer"), initOmitted, initRequest);
|
|
503
|
+
const workplanInitGate = await requireNextWorkplanSurfaceInit(initRepo, initCanonicalRequest, initSequence, keyValueFlag(args, "answer"), initOmitted, initRequest, initWorkplanPlan.outcome);
|
|
478
504
|
if (workplanInitGate) {
|
|
479
505
|
console.log(JSON.stringify(workplanInitGate, null, 2));
|
|
480
506
|
process.exitCode = 7;
|
|
481
507
|
return "exit";
|
|
482
508
|
}
|
|
483
509
|
}
|
|
484
|
-
const result = await initCompliance(initRepo, initRequest, flagValue(args, "surface") ?? flagValue(args, "surface-path"), keyValueFlag(args, "answer"), { allowUnresolvedIntake: hasFlag(args, "allow-unresolved-intake") });
|
|
485
|
-
if (result.status === "needs-clarification"
|
|
510
|
+
const result = await initCompliance(initRepo, initRequest, flagValue(args, "surface") ?? flagValue(args, "surface-path"), keyValueFlag(args, "answer"), { allowUnresolvedIntake: hasFlag(args, "allow-unresolved-intake"), baselinePlanned: hasFlag(args, "baseline"), supersedeEngagement: hasFlag(args, "supersede-engagement") });
|
|
511
|
+
if ((result.status === "needs-clarification" || result.status === "needs-engagement-supersede-confirmation") &&
|
|
512
|
+
typeof result.exitCode === "number") {
|
|
486
513
|
process.exitCode = result.exitCode;
|
|
487
514
|
}
|
|
488
515
|
if (hasFlag(args, "baseline") && result.status === "initialized") {
|
|
@@ -492,9 +519,26 @@ async function handleCli(args) {
|
|
|
492
519
|
return "exit";
|
|
493
520
|
}
|
|
494
521
|
if (command === "check") {
|
|
495
|
-
assertOnlyKnownFlags(args, ["ci", "format", "new-only", "allow-proof-waiver"], "check");
|
|
522
|
+
assertOnlyKnownFlags(args, ["ci", "format", "new-only", "allow-proof-waiver", "engagement", "all-engagements"], "check");
|
|
523
|
+
const engagementOutcome = flagValue(args, "engagement");
|
|
524
|
+
if (engagementOutcome) {
|
|
525
|
+
if (hasFlag(args, "new-only") || hasFlag(args, "ci") || hasFlag(args, "all-engagements")) {
|
|
526
|
+
throw new Error("--engagement re-verdicts one recorded engagement and cannot combine with --new-only, --ci, or --all-engagements.");
|
|
527
|
+
}
|
|
528
|
+
const reVerdict = await checkEngagementReVerdict(positionalRepoPath(args.slice(1)), engagementOutcome);
|
|
529
|
+
emitResult("check", reVerdict, args);
|
|
530
|
+
process.exitCode = typeof reVerdict.exitCode === "number" ? reVerdict.exitCode : 0;
|
|
531
|
+
return "exit";
|
|
532
|
+
}
|
|
533
|
+
const ciMode = hasFlag(args, "ci");
|
|
534
|
+
if (hasFlag(args, "all-engagements") || ciMode) {
|
|
535
|
+
const combined = await checkAllEngagements(positionalRepoPath(args.slice(1)), { newOnly: hasFlag(args, "new-only"), allowProofWaiver: hasFlag(args, "allow-proof-waiver") });
|
|
536
|
+
emitResult("check", ciMode ? ciCheckResult(combined) : combined, args);
|
|
537
|
+
process.exitCode = typeof combined.exitCode === "number" ? combined.exitCode : 0;
|
|
538
|
+
return "exit";
|
|
539
|
+
}
|
|
496
540
|
const result = await checkCompliance(positionalRepoPath(args.slice(1)), { newOnly: hasFlag(args, "new-only"), allowProofWaiver: hasFlag(args, "allow-proof-waiver") });
|
|
497
|
-
emitResult("check",
|
|
541
|
+
emitResult("check", result, args);
|
|
498
542
|
process.exitCode = result.exitCode;
|
|
499
543
|
return "exit";
|
|
500
544
|
}
|
|
@@ -657,7 +701,6 @@ function internalCommandsSection() {
|
|
|
657
701
|
return `
|
|
658
702
|
|
|
659
703
|
Internal commands (not part of the public surface — see docs/TOOLS.md):
|
|
660
|
-
vise sdk-facts --platform <p> [--capability <c>] Read bundled SDK surface facts. Projectless, read-only.
|
|
661
704
|
vise blocks <list|plan|add> ... Install and validate social.plus blocks in customer projects.
|
|
662
705
|
vise resolve --request "..." Resolve a request into the closest supported Vise outcome.
|
|
663
706
|
vise skill-path Print the bundled skill source path and install targets.
|
|
@@ -943,7 +986,9 @@ Usage:
|
|
|
943
986
|
vise blocks validate <repoPath> [--block comments] --registry <path> --format json
|
|
944
987
|
|
|
945
988
|
Safety:
|
|
946
|
-
Dry-run is the default. Apply mode only edits package manifests, explicit source anchors, and sp-vise/blocks.json
|
|
989
|
+
Dry-run is the default. Apply mode only edits package manifests, explicit source anchors, and sp-vise/blocks.json.
|
|
990
|
+
Apply records pending-mount. A block capability counts only after the imported symbol is mounted/used;
|
|
991
|
+
run blocks validate after mounting it to persist verified evidence.`;
|
|
947
992
|
}
|
|
948
993
|
if (command === "init") {
|
|
949
994
|
return `${packageName} init
|
|
@@ -960,6 +1005,7 @@ Usage:
|
|
|
960
1005
|
return `${packageName} check
|
|
961
1006
|
|
|
962
1007
|
Check the current source and recorded attestations against the compliance contract. Read-only.
|
|
1008
|
+
--ci also re-verdicts every recorded engagement and exits 11 if completed work drifted.
|
|
963
1009
|
|
|
964
1010
|
Usage:
|
|
965
1011
|
vise check [repoPath] [--ci] [--format human]
|
|
@@ -1105,6 +1151,7 @@ Usage:
|
|
|
1105
1151
|
vise learning show [repoPath] Show local learning summary
|
|
1106
1152
|
vise debug [repoPath] --error ... Debug an SDK-specific runtime error and emit a repair brief
|
|
1107
1153
|
vise explore "<request>" Discover what social.plus offers for a request (no project/credentials needed)
|
|
1154
|
+
vise sdk-facts --platform <p> Read proven SDK surface facts (symbols, capabilities, model schemas). Projectless, read-only
|
|
1108
1155
|
vise plan [repoPath] --request "..." Create an implementation plan
|
|
1109
1156
|
vise workplan next [repoPath] --request "..." Get the next broad-social surface to implement
|
|
1110
1157
|
vise init [repoPath] --request "..." Initialize compliance sidecar (add --baseline on a brownfield app)
|
|
@@ -2186,13 +2233,16 @@ function socialWorkplanSequence(plan) {
|
|
|
2186
2233
|
}
|
|
2187
2234
|
return workplan.sequence;
|
|
2188
2235
|
}
|
|
2189
|
-
async function requireNextWorkplanSurfaceInit(repoRoot, request, sequence, answers, omitted = [], requestedAs) {
|
|
2236
|
+
async function requireNextWorkplanSurfaceInit(repoRoot, request, sequence, answers, omitted = [], requestedAs, requestedOutcome) {
|
|
2190
2237
|
const progress = await verifiedWorkplanProgress(repoRoot, request, sequence);
|
|
2191
2238
|
const completedIds = new Set(progress.completed.map((item) => item.surface));
|
|
2192
2239
|
const nextSurface = sequence.find((surface) => !completedIds.has(surface.id));
|
|
2193
2240
|
const selectedSurface = resolveWorkplanSurfaceAnswer(sequence, answers.feature_surface);
|
|
2194
2241
|
const selectedOmittedSurface = resolveOmittedSurfaceAnswer(omitted, answers.feature_surface);
|
|
2195
2242
|
if (!nextSurface) {
|
|
2243
|
+
if (!completedWorkplanBlocksOutcome(sequence, requestedOutcome)) {
|
|
2244
|
+
return null;
|
|
2245
|
+
}
|
|
2196
2246
|
return {
|
|
2197
2247
|
status: "workplan-complete",
|
|
2198
2248
|
exitCode: 7,
|
|
@@ -2543,8 +2593,11 @@ function ciCheckResult(result) {
|
|
|
2543
2593
|
"needs-attestation",
|
|
2544
2594
|
"completeness-gap",
|
|
2545
2595
|
"selected-capability-failures",
|
|
2596
|
+
"engagement-drift",
|
|
2546
2597
|
],
|
|
2547
|
-
message: result.exitCode === 0
|
|
2598
|
+
message: result.exitCode === 0
|
|
2599
|
+
? "Active and recorded engagement compliance is green for CI."
|
|
2600
|
+
: `Compliance failed for CI with status: ${result.status}.`,
|
|
2548
2601
|
},
|
|
2549
2602
|
};
|
|
2550
2603
|
}
|
|
@@ -2746,7 +2799,7 @@ function doctorResult() {
|
|
|
2746
2799
|
packageRoot,
|
|
2747
2800
|
status: nodeMajor >= 18 ? "ok" : "unsupported-node",
|
|
2748
2801
|
node: process.versions.node,
|
|
2749
|
-
requiredNodeMajor: ">=
|
|
2802
|
+
requiredNodeMajor: ">=20",
|
|
2750
2803
|
docsSource: process.env.SOCIAL_PLUS_DOCS_ROOT ? "local" : "hosted",
|
|
2751
2804
|
docsRoot: process.env.SOCIAL_PLUS_DOCS_ROOT,
|
|
2752
2805
|
docsBaseUrl: process.env.SOCIAL_PLUS_DOCS_BASE_URL ?? "https://learn.social.plus",
|
package/dist/tools/blocks.js
CHANGED
|
@@ -5,7 +5,7 @@ import { packageVersion } from "../version.js";
|
|
|
5
5
|
import { inspectProject } from "./project.js";
|
|
6
6
|
import { detectCommandSensors } from "./harness.js";
|
|
7
7
|
import { readDesignContract } from "./design.js";
|
|
8
|
-
const blocksSidecarSchemaVersion = "2026-
|
|
8
|
+
const blocksSidecarSchemaVersion = "2026-07-10.vise-blocks-sidecar.v2";
|
|
9
9
|
const registryPlatformByVisePlatform = {
|
|
10
10
|
typescript: "react",
|
|
11
11
|
"react-native": "react-native",
|
|
@@ -74,11 +74,21 @@ export async function validateBlockInstall(options) {
|
|
|
74
74
|
if (installed.length === 0) {
|
|
75
75
|
findings.push({
|
|
76
76
|
ruleId: "blocks.sidecar.installed",
|
|
77
|
-
severity: "
|
|
77
|
+
severity: "error",
|
|
78
78
|
message: "No installed block sidecar entries exist.",
|
|
79
79
|
recommendation: "Run `vise blocks add <repoPath> --block <id> --registry <path> --apply` after reviewing the dry-run plan.",
|
|
80
80
|
});
|
|
81
81
|
}
|
|
82
|
+
for (const entry of installed) {
|
|
83
|
+
if (!(await installedEntryLocallyValid(repoPath, entry))) {
|
|
84
|
+
findings.push({
|
|
85
|
+
ruleId: "blocks.install.verified",
|
|
86
|
+
severity: "error",
|
|
87
|
+
message: `Installed block ${entry.blockId} is not currently verified: its dependency, touched files, or live mount evidence is missing.`,
|
|
88
|
+
recommendation: `Run \`vise blocks validate <repoPath> --block ${entry.blockId} --registry <path>\` and resolve every reported error.`,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
82
92
|
return {
|
|
83
93
|
status: findings.some((finding) => finding.severity === "error") ? "needs-review" : "ready",
|
|
84
94
|
mode: "validate",
|
|
@@ -96,7 +106,7 @@ export async function validateBlockInstall(options) {
|
|
|
96
106
|
if (installed.length === 0) {
|
|
97
107
|
findings.push({
|
|
98
108
|
ruleId: "blocks.sidecar.installed",
|
|
99
|
-
severity: "
|
|
109
|
+
severity: "error",
|
|
100
110
|
message: options.blockId ? `No sidecar entry exists for block ${options.blockId}.` : "No installed block sidecar entries exist.",
|
|
101
111
|
recommendation: "Run `vise blocks add <repoPath> --block <id> --registry <path> --apply` after reviewing the dry-run plan.",
|
|
102
112
|
});
|
|
@@ -119,12 +129,30 @@ export async function validateBlockInstall(options) {
|
|
|
119
129
|
});
|
|
120
130
|
}
|
|
121
131
|
}
|
|
132
|
+
const mountEvidence = await findBlockMountEvidence(repoPath, {
|
|
133
|
+
platform: plan.registryPlatform,
|
|
134
|
+
publicEntrypoint: plan.package.publicEntrypoint,
|
|
135
|
+
importName: plan.package.importName,
|
|
136
|
+
mountFiles: plan.targetFiles.map((targetFile) => targetFile.path),
|
|
137
|
+
});
|
|
138
|
+
if (!mountEvidence) {
|
|
139
|
+
findings.push({
|
|
140
|
+
ruleId: "blocks.mount.present",
|
|
141
|
+
severity: "error",
|
|
142
|
+
message: `Block ${plan.block.id} is installed but ${plan.package.importName} is not mounted in the target source.`,
|
|
143
|
+
recommendation: `Render or invoke ${plan.package.importName} in ${plan.targetFiles.map((targetFile) => targetFile.path).join(", ")}, then re-run block validation. An import alone does not deliver the block capability.`,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
122
146
|
const hasError = findings.some((finding) => finding.severity === "error");
|
|
147
|
+
await updateBlockValidationState(repoPath, plan.block.id, hasError ? "invalid" : "verified", mountEvidence);
|
|
148
|
+
const updatedSidecar = await readSidecar(repoPath);
|
|
149
|
+
const updatedInstalled = (updatedSidecar?.installed ?? []).filter((entry) => entry.blockId === options.blockId);
|
|
123
150
|
return {
|
|
124
151
|
...plan,
|
|
125
152
|
status: hasError ? "needs-review" : plan.status,
|
|
126
153
|
validationStatus: hasError ? "failed" : "passed",
|
|
127
|
-
|
|
154
|
+
mountEvidence,
|
|
155
|
+
sidecarInstalled: updatedInstalled,
|
|
128
156
|
findings,
|
|
129
157
|
};
|
|
130
158
|
}
|
|
@@ -242,7 +270,7 @@ async function packageChangeFor(root, platform, packageInfo, packageSource) {
|
|
|
242
270
|
};
|
|
243
271
|
}
|
|
244
272
|
const file = "package.json";
|
|
245
|
-
const packageJson = await readPackageJson(path.join(root, file));
|
|
273
|
+
const packageJson = await readPackageJson(path.join(root, file)).catch(() => ({}));
|
|
246
274
|
return {
|
|
247
275
|
file,
|
|
248
276
|
dependency: packageInfo.dependencyName,
|
|
@@ -319,10 +347,13 @@ async function writeBlocksSidecar(repoPath, plan, packageSource, filesTouched) {
|
|
|
319
347
|
packageSource,
|
|
320
348
|
dependencyName: plan.package.dependencyName,
|
|
321
349
|
providesCapabilities: plan.providesCapabilities,
|
|
350
|
+
publicEntrypoint: plan.package.publicEntrypoint,
|
|
351
|
+
importName: plan.package.importName,
|
|
352
|
+
mountFiles: plan.targetFiles.map((targetFile) => targetFile.path),
|
|
322
353
|
filesTouched,
|
|
323
354
|
designContractDigest: designContract?.digest,
|
|
324
355
|
sdkFactsVersion: plan.block.version,
|
|
325
|
-
validationStatus: "
|
|
356
|
+
validationStatus: "pending-mount",
|
|
326
357
|
};
|
|
327
358
|
sidecar.schemaVersion = blocksSidecarSchemaVersion;
|
|
328
359
|
sidecar.viseVersion = packageVersion;
|
|
@@ -376,7 +407,46 @@ async function installedEntryLocallyValid(repoRoot, entry) {
|
|
|
376
407
|
return false;
|
|
377
408
|
}
|
|
378
409
|
}
|
|
379
|
-
return
|
|
410
|
+
return Boolean(await findBlockMountEvidence(repoRoot, entry));
|
|
411
|
+
}
|
|
412
|
+
async function findBlockMountEvidence(repoRoot, entry) {
|
|
413
|
+
if (!entry.publicEntrypoint || !entry.importName || !Array.isArray(entry.mountFiles) || entry.mountFiles.length === 0) {
|
|
414
|
+
return undefined;
|
|
415
|
+
}
|
|
416
|
+
for (const relativeFile of entry.mountFiles) {
|
|
417
|
+
if (typeof relativeFile !== "string" || relativeFile.trim() === "") {
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
const source = await readFile(path.join(repoRoot, relativeFile), "utf8").catch(() => "");
|
|
421
|
+
if (!source.includes(entry.publicEntrypoint) || !blockUsagePresent(source, entry.platform, entry.importName)) {
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
return { file: relativeFile, kind: "source-usage", importName: entry.importName };
|
|
425
|
+
}
|
|
426
|
+
return undefined;
|
|
427
|
+
}
|
|
428
|
+
function blockUsagePresent(source, platform, importName) {
|
|
429
|
+
const name = escapeRegExp(importName);
|
|
430
|
+
if (platform === "flutter") {
|
|
431
|
+
return new RegExp(`\\b${name}\\s*\\(`).test(source);
|
|
432
|
+
}
|
|
433
|
+
return [
|
|
434
|
+
new RegExp(`<\\s*${name}(?:\\s|/|>)`),
|
|
435
|
+
new RegExp(`(?:React\\.)?createElement\\s*\\(\\s*${name}\\b`),
|
|
436
|
+
new RegExp(`\\b${name}\\s*\\(`),
|
|
437
|
+
].some((pattern) => pattern.test(source));
|
|
438
|
+
}
|
|
439
|
+
async function updateBlockValidationState(repoRoot, blockId, validationStatus, mountEvidence) {
|
|
440
|
+
const sidecar = await readSidecar(repoRoot);
|
|
441
|
+
const entry = sidecar?.installed.find((item) => item.blockId === blockId);
|
|
442
|
+
if (!sidecar || !entry) {
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
entry.validationStatus = validationStatus;
|
|
446
|
+
entry.mountEvidence = mountEvidence;
|
|
447
|
+
sidecar.viseVersion = packageVersion;
|
|
448
|
+
sidecar.generatedAt = new Date().toISOString();
|
|
449
|
+
await writeFile(path.join(repoRoot, "sp-vise", "blocks.json"), `${JSON.stringify(sidecar, null, 2)}\n`, "utf8");
|
|
380
450
|
}
|
|
381
451
|
async function blockDependencyDeclared(repoRoot, entry) {
|
|
382
452
|
const dependencyName = entry.dependencyName;
|
|
@@ -433,12 +503,15 @@ function insertBlockSnippet(source, plan) {
|
|
|
433
503
|
return source;
|
|
434
504
|
}
|
|
435
505
|
function addPubspecDependency(source, dependencyName, value) {
|
|
506
|
+
const dependencyLines = value.startsWith("file:")
|
|
507
|
+
? [` ${dependencyName}:`, ` path: ${value.replace(/^file:/, "")}`]
|
|
508
|
+
: [` ${dependencyName}: ${value}`];
|
|
436
509
|
if (!source.includes("dependencies:")) {
|
|
437
|
-
return `${source.trimEnd()}\n\ndependencies:\n
|
|
510
|
+
return `${source.trimEnd()}\n\ndependencies:\n${dependencyLines.join("\n")}\n`;
|
|
438
511
|
}
|
|
439
512
|
const lines = source.split(/\r?\n/);
|
|
440
513
|
const insertIndex = lines.findIndex((line) => line.trim() === "dependencies:") + 1;
|
|
441
|
-
lines.splice(insertIndex, 0,
|
|
514
|
+
lines.splice(insertIndex, 0, ...dependencyLines);
|
|
442
515
|
return `${lines.join("\n").trimEnd()}\n`;
|
|
443
516
|
}
|
|
444
517
|
async function readPackageJson(filePath) {
|