@amityco/social-plus-vise 1.6.0 → 1.7.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 +21 -0
- package/README.md +10 -4
- package/dist/flow.js +6 -0
- package/dist/server.js +56 -11
- package/dist/tools/blocks.js +1 -1
- package/dist/tools/compliance.js +223 -18
- package/dist/tools/internalWorkspace.js +44 -0
- package/dist/tools/sdkFacts.js +233 -7
- package/package.json +1 -1
- 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,27 @@ 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.7.0 — 2026-07-09
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **Day-2 governance: adding features to an app that already uses social.plus is now a first-class, governed flow.**
|
|
11
|
+
- **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`).
|
|
12
|
+
- **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`.
|
|
13
|
+
- **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`).
|
|
14
|
+
- **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.
|
|
15
|
+
- **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.
|
|
16
|
+
- New skill section — "Adding Features To An App That Already Uses social.plus" — teaches agents both variants (hand-rolled integration and prior governed engagement).
|
|
17
|
+
- **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.
|
|
18
|
+
- **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.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- **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`.
|
|
22
|
+
|
|
23
|
+
### Compatibility
|
|
24
|
+
- **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.
|
|
25
|
+
- **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.
|
|
26
|
+
- **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.
|
|
27
|
+
|
|
7
28
|
## 1.6.0 — 2026-07-06
|
|
8
29
|
|
|
9
30
|
### 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
|
}
|
|
@@ -462,9 +474,19 @@ async function handleCli(args) {
|
|
|
462
474
|
return "exit";
|
|
463
475
|
}
|
|
464
476
|
if (command === "init") {
|
|
465
|
-
assertOnlyKnownFlags(args, ["request", "surface", "surface-path", "answer", "allow-unresolved-intake", "baseline"], "init");
|
|
477
|
+
assertOnlyKnownFlags(args, ["request", "surface", "surface-path", "answer", "allow-unresolved-intake", "baseline", "allow-internal-workspace", "supersede-engagement"], "init");
|
|
466
478
|
const initRepo = positionalRepoPath(args.slice(1));
|
|
467
479
|
const initRequest = requiredFlagValue(args, "request", "init requires --request.");
|
|
480
|
+
const initWorkspace = await detectInternalWorkspace(initRepo);
|
|
481
|
+
if (initWorkspace.isInternalWorkspace && !hasFlag(args, "allow-internal-workspace")) {
|
|
482
|
+
console.log(JSON.stringify({
|
|
483
|
+
status: "internal-workspace",
|
|
484
|
+
...internalWorkspaceWarning(initWorkspace),
|
|
485
|
+
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.",
|
|
486
|
+
}, null, 2));
|
|
487
|
+
process.exitCode = 10;
|
|
488
|
+
return "exit";
|
|
489
|
+
}
|
|
468
490
|
const initWorkplanPlan = await workplanPlan(initRepo, initRequest, args);
|
|
469
491
|
const { sequence: initSequence, omitted: initOmitted, canonicalRequest: initCanonicalRequest } = await activeBlueprint(initRepo, initRequest, initWorkplanPlan);
|
|
470
492
|
if (initSequence.length >= 2 && initWorkplanPlan.platform !== "unknown") {
|
|
@@ -474,15 +496,16 @@ async function handleCli(args) {
|
|
|
474
496
|
process.exitCode = 7;
|
|
475
497
|
return "exit";
|
|
476
498
|
}
|
|
477
|
-
const workplanInitGate = await requireNextWorkplanSurfaceInit(initRepo, initCanonicalRequest, initSequence, keyValueFlag(args, "answer"), initOmitted, initRequest);
|
|
499
|
+
const workplanInitGate = await requireNextWorkplanSurfaceInit(initRepo, initCanonicalRequest, initSequence, keyValueFlag(args, "answer"), initOmitted, initRequest, initWorkplanPlan.outcome);
|
|
478
500
|
if (workplanInitGate) {
|
|
479
501
|
console.log(JSON.stringify(workplanInitGate, null, 2));
|
|
480
502
|
process.exitCode = 7;
|
|
481
503
|
return "exit";
|
|
482
504
|
}
|
|
483
505
|
}
|
|
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"
|
|
506
|
+
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") });
|
|
507
|
+
if ((result.status === "needs-clarification" || result.status === "needs-engagement-supersede-confirmation") &&
|
|
508
|
+
typeof result.exitCode === "number") {
|
|
486
509
|
process.exitCode = result.exitCode;
|
|
487
510
|
}
|
|
488
511
|
if (hasFlag(args, "baseline") && result.status === "initialized") {
|
|
@@ -492,7 +515,26 @@ async function handleCli(args) {
|
|
|
492
515
|
return "exit";
|
|
493
516
|
}
|
|
494
517
|
if (command === "check") {
|
|
495
|
-
assertOnlyKnownFlags(args, ["ci", "format", "new-only", "allow-proof-waiver"], "check");
|
|
518
|
+
assertOnlyKnownFlags(args, ["ci", "format", "new-only", "allow-proof-waiver", "engagement", "all-engagements"], "check");
|
|
519
|
+
const engagementOutcome = flagValue(args, "engagement");
|
|
520
|
+
if (engagementOutcome) {
|
|
521
|
+
if (hasFlag(args, "new-only") || hasFlag(args, "ci") || hasFlag(args, "all-engagements")) {
|
|
522
|
+
throw new Error("--engagement re-verdicts one recorded engagement and cannot combine with --new-only, --ci, or --all-engagements.");
|
|
523
|
+
}
|
|
524
|
+
const reVerdict = await checkEngagementReVerdict(positionalRepoPath(args.slice(1)), engagementOutcome);
|
|
525
|
+
emitResult("check", reVerdict, args);
|
|
526
|
+
process.exitCode = typeof reVerdict.exitCode === "number" ? reVerdict.exitCode : 0;
|
|
527
|
+
return "exit";
|
|
528
|
+
}
|
|
529
|
+
if (hasFlag(args, "all-engagements")) {
|
|
530
|
+
if (hasFlag(args, "ci")) {
|
|
531
|
+
throw new Error("--all-engagements cannot combine with --ci (the CI shape covers the active engagement only).");
|
|
532
|
+
}
|
|
533
|
+
const combined = await checkAllEngagements(positionalRepoPath(args.slice(1)), { newOnly: hasFlag(args, "new-only"), allowProofWaiver: hasFlag(args, "allow-proof-waiver") });
|
|
534
|
+
emitResult("check", combined, args);
|
|
535
|
+
process.exitCode = typeof combined.exitCode === "number" ? combined.exitCode : 0;
|
|
536
|
+
return "exit";
|
|
537
|
+
}
|
|
496
538
|
const result = await checkCompliance(positionalRepoPath(args.slice(1)), { newOnly: hasFlag(args, "new-only"), allowProofWaiver: hasFlag(args, "allow-proof-waiver") });
|
|
497
539
|
emitResult("check", hasFlag(args, "ci") ? ciCheckResult(result) : result, args);
|
|
498
540
|
process.exitCode = result.exitCode;
|
|
@@ -657,7 +699,6 @@ function internalCommandsSection() {
|
|
|
657
699
|
return `
|
|
658
700
|
|
|
659
701
|
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
702
|
vise blocks <list|plan|add> ... Install and validate social.plus blocks in customer projects.
|
|
662
703
|
vise resolve --request "..." Resolve a request into the closest supported Vise outcome.
|
|
663
704
|
vise skill-path Print the bundled skill source path and install targets.
|
|
@@ -1105,6 +1146,7 @@ Usage:
|
|
|
1105
1146
|
vise learning show [repoPath] Show local learning summary
|
|
1106
1147
|
vise debug [repoPath] --error ... Debug an SDK-specific runtime error and emit a repair brief
|
|
1107
1148
|
vise explore "<request>" Discover what social.plus offers for a request (no project/credentials needed)
|
|
1149
|
+
vise sdk-facts --platform <p> Read proven SDK surface facts (symbols, capabilities, model schemas). Projectless, read-only
|
|
1108
1150
|
vise plan [repoPath] --request "..." Create an implementation plan
|
|
1109
1151
|
vise workplan next [repoPath] --request "..." Get the next broad-social surface to implement
|
|
1110
1152
|
vise init [repoPath] --request "..." Initialize compliance sidecar (add --baseline on a brownfield app)
|
|
@@ -2186,13 +2228,16 @@ function socialWorkplanSequence(plan) {
|
|
|
2186
2228
|
}
|
|
2187
2229
|
return workplan.sequence;
|
|
2188
2230
|
}
|
|
2189
|
-
async function requireNextWorkplanSurfaceInit(repoRoot, request, sequence, answers, omitted = [], requestedAs) {
|
|
2231
|
+
async function requireNextWorkplanSurfaceInit(repoRoot, request, sequence, answers, omitted = [], requestedAs, requestedOutcome) {
|
|
2190
2232
|
const progress = await verifiedWorkplanProgress(repoRoot, request, sequence);
|
|
2191
2233
|
const completedIds = new Set(progress.completed.map((item) => item.surface));
|
|
2192
2234
|
const nextSurface = sequence.find((surface) => !completedIds.has(surface.id));
|
|
2193
2235
|
const selectedSurface = resolveWorkplanSurfaceAnswer(sequence, answers.feature_surface);
|
|
2194
2236
|
const selectedOmittedSurface = resolveOmittedSurfaceAnswer(omitted, answers.feature_surface);
|
|
2195
2237
|
if (!nextSurface) {
|
|
2238
|
+
if (!completedWorkplanBlocksOutcome(sequence, requestedOutcome)) {
|
|
2239
|
+
return null;
|
|
2240
|
+
}
|
|
2196
2241
|
return {
|
|
2197
2242
|
status: "workplan-complete",
|
|
2198
2243
|
exitCode: 7,
|
package/dist/tools/blocks.js
CHANGED
|
@@ -242,7 +242,7 @@ async function packageChangeFor(root, platform, packageInfo, packageSource) {
|
|
|
242
242
|
};
|
|
243
243
|
}
|
|
244
244
|
const file = "package.json";
|
|
245
|
-
const packageJson = await readPackageJson(path.join(root, file));
|
|
245
|
+
const packageJson = await readPackageJson(path.join(root, file)).catch(() => ({}));
|
|
246
246
|
return {
|
|
247
247
|
file,
|
|
248
248
|
dependency: packageInfo.dependencyName,
|
package/dist/tools/compliance.js
CHANGED
|
@@ -416,6 +416,22 @@ export async function initCompliance(repoPath, request, surfacePath, answers = {
|
|
|
416
416
|
"The gate still blocks until every id above is answered; pass --allow-unresolved-intake only for retrospective/harness initialization.",
|
|
417
417
|
};
|
|
418
418
|
}
|
|
419
|
+
const previousCompliance = await readJsonIfExists(compliancePath(repoRoot));
|
|
420
|
+
let engagementSwitch;
|
|
421
|
+
if (previousCompliance && previousCompliance.outcome !== outcome) {
|
|
422
|
+
const finalCheck = await checkCompliance(repoPath).catch(() => undefined);
|
|
423
|
+
if (finalCheck?.status === "green" && !options.supersedeEngagement) {
|
|
424
|
+
return {
|
|
425
|
+
status: "needs-engagement-supersede-confirmation",
|
|
426
|
+
exitCode: 7,
|
|
427
|
+
from: previousCompliance.outcome,
|
|
428
|
+
to: outcome,
|
|
429
|
+
reason: `The current engagement (outcome "${previousCompliance.outcome}") is green — finished, verified work. Initializing "${outcome}" replaces its contract, and its completeness will no longer be checked (it moves to the engagement ledger; see \`vise status\` previousEngagements and \`vise check --all-engagements\`).`,
|
|
430
|
+
instruction: `Re-run with --supersede-engagement to proceed. The green final state will be recorded in sp-vise/engagements/${previousCompliance.outcome}/ first.`,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
engagementSwitch = await recordEngagementOnSwitch(repoRoot, previousCompliance, outcome, finalCheck);
|
|
434
|
+
}
|
|
419
435
|
const compliance = {
|
|
420
436
|
schema_version: schemaVersion,
|
|
421
437
|
vise_version: packageVersion,
|
|
@@ -468,6 +484,17 @@ export async function initCompliance(repoPath, request, surfacePath, answers = {
|
|
|
468
484
|
if (engagement && engagement.scope.outcomes.length > 0 && !engagement.scope.outcomes.includes(outcome)) {
|
|
469
485
|
warnings.push(`Outcome "${outcome}" is not in the engagement scope (${engagement.scope.outcomes.join(", ")}). Compliance was still initialized; extend the scope in engagement.json or re-run vise engagement init.`);
|
|
470
486
|
}
|
|
487
|
+
const preExistingFindingFiles = [
|
|
488
|
+
...new Set(checkSnapshot.rules
|
|
489
|
+
.filter((rule) => BASELINE_NON_GREEN.has(rule.status) && rule.finding?.file)
|
|
490
|
+
.map((rule) => rule.finding?.file)),
|
|
491
|
+
];
|
|
492
|
+
const brownfieldHint = !options.baselinePlanned && preExistingFindingFiles.length > 0 && !(await readBaseline(repoRoot))
|
|
493
|
+
? {
|
|
494
|
+
pre_existing_finding_files: preExistingFindingFiles.slice(0, 5),
|
|
495
|
+
hint: "This repo already contains social.plus integration with non-green findings that predate this engagement. Record `vise baseline .` NOW (before writing any code) so `vise check --new-only` gates only findings you introduce. The outcome's completeness checklist is never baselined — the requested feature still has to be built. Never record a baseline mid-build: it would swallow your own findings.",
|
|
496
|
+
}
|
|
497
|
+
: undefined;
|
|
471
498
|
return {
|
|
472
499
|
status: "initialized",
|
|
473
500
|
sidecar: complianceDirName,
|
|
@@ -480,6 +507,8 @@ export async function initCompliance(repoPath, request, surfacePath, answers = {
|
|
|
480
507
|
...(compliance.design_contract && { design_contract: compliance.design_contract }),
|
|
481
508
|
...(selectedOptionalCapabilities.length > 0 && { selected_optional_capabilities: selectedOptionalCapabilities }),
|
|
482
509
|
...(runtimeSmokeTemplate ? { runtime_smoke: runtimeSmokeTemplate } : {}),
|
|
510
|
+
...(brownfieldHint ? { brownfield: brownfieldHint } : {}),
|
|
511
|
+
...(engagementSwitch ? { engagement_switch: engagementSwitch } : {}),
|
|
483
512
|
intake: {
|
|
484
513
|
status: intake.status,
|
|
485
514
|
remainingBlocking: intake.remainingBlocking,
|
|
@@ -862,7 +891,7 @@ async function ruleFreshness(rule) {
|
|
|
862
891
|
}
|
|
863
892
|
export async function checkCompliance(repoPath, options = {}) {
|
|
864
893
|
const repoRoot = path.resolve(repoPath);
|
|
865
|
-
const compliance = await readCompliance(repoRoot);
|
|
894
|
+
const compliance = options.contract ?? (await readCompliance(repoRoot));
|
|
866
895
|
const rules = await rulesById();
|
|
867
896
|
const drift = contractDrift(compliance, rules);
|
|
868
897
|
if (drift.length > 0) {
|
|
@@ -996,6 +1025,27 @@ export async function checkCompliance(repoPath, options = {}) {
|
|
|
996
1025
|
const exactMatch = attestation.rule_digest === ref.rule_digest;
|
|
997
1026
|
const grandfathered = !exactMatch && isAttestationGrandfathered(rule, attestation);
|
|
998
1027
|
if (exactMatch || grandfathered) {
|
|
1028
|
+
if (attestation.outcome !== undefined &&
|
|
1029
|
+
attestation.outcome !== compliance.outcome &&
|
|
1030
|
+
!(attestation.file_scoped === true && isFileScopableRule(rule))) {
|
|
1031
|
+
const crossEngagementStatus = rule.advisory ? "advisory" : rule.enforcement.attestation.allowed ? "attestation-needed" : "deterministic-fail";
|
|
1032
|
+
results.push({
|
|
1033
|
+
...checkRuleIdentity(rule.id),
|
|
1034
|
+
title: rule.title,
|
|
1035
|
+
severity: rule.severity,
|
|
1036
|
+
status: crossEngagementStatus,
|
|
1037
|
+
reason: rule.advisory
|
|
1038
|
+
? "Advisory: informational only — does not affect compliance status."
|
|
1039
|
+
: `Attestation was recorded during a different engagement (outcome "${attestation.outcome}", attested ${attestation.attested_at}); the current engagement is "${compliance.outcome}". Re-verify the claim still holds for the code this engagement adds, then re-attest.`,
|
|
1040
|
+
finding,
|
|
1041
|
+
recommendation: finding?.recommendation,
|
|
1042
|
+
rationale: rule.rationale,
|
|
1043
|
+
current_rule: ruleSummary(rule),
|
|
1044
|
+
stale_engagement_attestation: { outcome: attestation.outcome, attested_at: attestation.attested_at },
|
|
1045
|
+
...(crossEngagementStatus === "attestation-needed" && rule.enforcement.attestation.allowed && attestHint(rule, compliance)),
|
|
1046
|
+
});
|
|
1047
|
+
continue;
|
|
1048
|
+
}
|
|
999
1049
|
const sourceFingerprintStatus = await checkSourceFingerprints(repoRoot, inspection.effectiveRoot, attestation.source_fingerprints ?? []);
|
|
1000
1050
|
const staleFingerprints = sourceFingerprintStatus.filter((item) => item.status !== "match");
|
|
1001
1051
|
if (staleFingerprints.length > 0) {
|
|
@@ -1153,22 +1203,23 @@ export async function checkCompliance(repoPath, options = {}) {
|
|
|
1153
1203
|
result.baselined = true;
|
|
1154
1204
|
}
|
|
1155
1205
|
}
|
|
1156
|
-
const
|
|
1157
|
-
const baselinedSelected = new Set(baselineFile.selected_optional);
|
|
1206
|
+
const legacyDeliverableEntries = (baselineFile.completeness_missing?.length ?? 0) + (baselineFile.selected_optional?.length ?? 0);
|
|
1158
1207
|
gatedResults = results.filter((result) => !result.baselined);
|
|
1159
|
-
gatedMissing = (completeness?.missing ?? []).filter((item) => !baselinedMissing.has(item.id));
|
|
1160
|
-
gatedSelectedFailed = (selectedOptionalCapabilities?.failed ?? []).filter((item) => !baselinedSelected.has(item.id));
|
|
1161
|
-
gatedSelectedUnknown = (selectedOptionalCapabilities?.unknown ?? []).filter((id) => !baselinedSelected.has(id));
|
|
1162
1208
|
baselineReport = {
|
|
1163
1209
|
present: true,
|
|
1164
1210
|
applied: true,
|
|
1165
1211
|
baselined_at: baselineFile.baselined_at,
|
|
1166
1212
|
excluded: {
|
|
1167
1213
|
rules: results.filter((result) => result.baselined).length,
|
|
1168
|
-
completeness:
|
|
1169
|
-
selected_optional:
|
|
1170
|
-
((selectedOptionalCapabilities?.unknown.length ?? 0) - gatedSelectedUnknown.length),
|
|
1214
|
+
completeness: 0,
|
|
1215
|
+
selected_optional: 0,
|
|
1171
1216
|
},
|
|
1217
|
+
...(legacyDeliverableEntries > 0
|
|
1218
|
+
? {
|
|
1219
|
+
ignored_legacy_deliverable_entries: legacyDeliverableEntries,
|
|
1220
|
+
ignored_legacy_note: "This baseline predates the deliverables fix and recorded completeness/selected-capability entries; they are ignored — the engagement's deliverables always gate. Re-run `vise baseline` to refresh the file.",
|
|
1221
|
+
}
|
|
1222
|
+
: {}),
|
|
1172
1223
|
residual_caveat: "Baseline excludes pre-existing findings keyed by rule+file (count-based). A NEW violation of an already-baselined rule in the SAME file can be masked at this granularity — review the touched files directly for a precise per-change gate.",
|
|
1173
1224
|
};
|
|
1174
1225
|
}
|
|
@@ -1668,6 +1719,29 @@ export async function statusCompliance(repoPath, options = {}) {
|
|
|
1668
1719
|
reviewer_assignment: engagement.reviewer_assignment,
|
|
1669
1720
|
},
|
|
1670
1721
|
}),
|
|
1722
|
+
...(await previousEngagementsSection(repoRoot, check.outcome)),
|
|
1723
|
+
};
|
|
1724
|
+
}
|
|
1725
|
+
async function previousEngagementsSection(repoRoot, activeOutcome) {
|
|
1726
|
+
const dir = engagementsDir(repoRoot);
|
|
1727
|
+
const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
1728
|
+
const previous = [];
|
|
1729
|
+
for (const entry of entries) {
|
|
1730
|
+
if (!entry.isDirectory() || entry.name === activeOutcome) {
|
|
1731
|
+
continue;
|
|
1732
|
+
}
|
|
1733
|
+
const record = await readJsonIfExists(path.join(dir, entry.name, "record.json"));
|
|
1734
|
+
if (record) {
|
|
1735
|
+
previous.push(record);
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
if (previous.length === 0) {
|
|
1739
|
+
return {};
|
|
1740
|
+
}
|
|
1741
|
+
previous.sort((a, b) => String(a.outcome).localeCompare(String(b.outcome)));
|
|
1742
|
+
return {
|
|
1743
|
+
previousEngagements: previous,
|
|
1744
|
+
previousEngagements_note: "Superseded engagements' last recorded state (latest per outcome; snapshots, not live verdicts). Their completeness is NOT checked by the active contract — to re-verify one today, re-run `vise init` with its original request and `vise check`. On-demand re-verification lands with `vise check --all-engagements` (docs/ENGAGEMENTS_DESIGN.md, B2).",
|
|
1671
1745
|
};
|
|
1672
1746
|
}
|
|
1673
1747
|
async function applicableRules(outcome, platforms) {
|
|
@@ -2435,6 +2509,7 @@ function buildAttestation(compliance, rule, signer, confidence, identity, ration
|
|
|
2435
2509
|
evidence,
|
|
2436
2510
|
source_fingerprints: sourceFingerprints,
|
|
2437
2511
|
...(fileScoped ? { file_scoped: true } : {}),
|
|
2512
|
+
outcome: compliance.outcome,
|
|
2438
2513
|
rationale_for_attestation: rationale,
|
|
2439
2514
|
attested_at: new Date().toISOString(),
|
|
2440
2515
|
};
|
|
@@ -2791,6 +2866,8 @@ function sidecarReadme(compliance) {
|
|
|
2791
2866
|
"",
|
|
2792
2867
|
"Attestations include source fingerprints; `vise check` marks them stale if the cited files change.",
|
|
2793
2868
|
"",
|
|
2869
|
+
"`engagements/<outcome>/` (when present) is the engagement ledger: each superseded engagement's final check, contract, and intake, recorded when a `vise init` switched to a different outcome. `vise status` lists them under `previousEngagements`; re-verify with `vise check --engagement <outcome>` or `--all-engagements`.",
|
|
2870
|
+
"",
|
|
2794
2871
|
].join("\n");
|
|
2795
2872
|
}
|
|
2796
2873
|
function attestationsDir(repoRoot) {
|
|
@@ -2827,6 +2904,129 @@ function deriveGate(flags) {
|
|
|
2827
2904
|
return { status: "runtime-proof-waived", exitCode: flags.allowProofWaiver ? 0 : 9 };
|
|
2828
2905
|
return { status: "green", exitCode: 0 };
|
|
2829
2906
|
}
|
|
2907
|
+
async function recordEngagementOnSwitch(repoRoot, previous, nextOutcome, finalCheck) {
|
|
2908
|
+
const previousIntake = await readJsonIfExists(path.join(sidecarDir(repoRoot), "intake.json"));
|
|
2909
|
+
const dir = path.join(engagementsDir(repoRoot), String(previous.outcome));
|
|
2910
|
+
await rm(dir, { recursive: true, force: true });
|
|
2911
|
+
await mkdir(dir, { recursive: true });
|
|
2912
|
+
const verdict = finalCheck
|
|
2913
|
+
? { status: finalCheck.status, exitCode: finalCheck.exitCode }
|
|
2914
|
+
: { status: "unrecorded", exitCode: null, note: "The final check could not run at switch time; contract and intake are still recorded." };
|
|
2915
|
+
const record = {
|
|
2916
|
+
outcome: previous.outcome,
|
|
2917
|
+
...(typeof previousIntake?.request === "string" ? { request: previousIntake.request } : {}),
|
|
2918
|
+
recorded_at: new Date().toISOString(),
|
|
2919
|
+
reason: `superseded by ${nextOutcome} init`,
|
|
2920
|
+
ruleset_digest: previous.ruleset_digest,
|
|
2921
|
+
verdict,
|
|
2922
|
+
};
|
|
2923
|
+
await writeJson(path.join(dir, "record.json"), record);
|
|
2924
|
+
if (finalCheck) {
|
|
2925
|
+
await writeJson(path.join(dir, "final-check.json"), finalCheck);
|
|
2926
|
+
}
|
|
2927
|
+
await writeJson(path.join(dir, "contract.json"), previous);
|
|
2928
|
+
if (previousIntake) {
|
|
2929
|
+
await writeJson(path.join(dir, "intake.json"), previousIntake);
|
|
2930
|
+
}
|
|
2931
|
+
return {
|
|
2932
|
+
from: previous.outcome,
|
|
2933
|
+
to: nextOutcome,
|
|
2934
|
+
recorded: path.join(complianceDirName, "engagements", String(previous.outcome)),
|
|
2935
|
+
last_verdict: verdict,
|
|
2936
|
+
note: "The previous engagement's final state is recorded in the ledger; its completeness is no longer checked by this contract. `vise status` lists it under previousEngagements. On-demand re-verification (`vise check --all-engagements`) lands with B2 (docs/ENGAGEMENTS_DESIGN.md).",
|
|
2937
|
+
};
|
|
2938
|
+
}
|
|
2939
|
+
function engagementsDir(repoRoot) {
|
|
2940
|
+
return path.join(sidecarDir(repoRoot), "engagements");
|
|
2941
|
+
}
|
|
2942
|
+
async function reVerdictOne(repoPath, repoRoot, outcome) {
|
|
2943
|
+
const dir = path.join(engagementsDir(repoRoot), outcome);
|
|
2944
|
+
const record = await readJsonIfExists(path.join(dir, "record.json"));
|
|
2945
|
+
const recorded = await readJsonIfExists(path.join(dir, "contract.json"));
|
|
2946
|
+
if (!record || !recorded) {
|
|
2947
|
+
const available = (await readdir(engagementsDir(repoRoot), { withFileTypes: true }).catch(() => []))
|
|
2948
|
+
.filter((entry) => entry.isDirectory())
|
|
2949
|
+
.map((entry) => entry.name);
|
|
2950
|
+
throw new Error(`No recorded engagement for outcome "${outcome}".${available.length > 0 ? ` Recorded engagements: ${available.join(", ")}.` : " The engagement ledger is empty (it is written when an init switches away from an outcome)."}`);
|
|
2951
|
+
}
|
|
2952
|
+
const inspection = await inspectProject(repoRoot, recorded.surface?.path === "." ? undefined : recorded.surface?.path);
|
|
2953
|
+
const platforms = Array.from(new Set([...inspection.platforms, ...(recorded.surface?.platforms ?? [])]));
|
|
2954
|
+
const rules = await applicableRules(recorded.outcome, platforms.length > 0 ? platforms : ["unknown"]);
|
|
2955
|
+
const synthetic = {
|
|
2956
|
+
...recorded,
|
|
2957
|
+
ruleset_digest: digestJson(rules.map(ruleRef)),
|
|
2958
|
+
rules: rules.map(ruleRefForFile),
|
|
2959
|
+
generated_at: new Date().toISOString(),
|
|
2960
|
+
};
|
|
2961
|
+
const check = await checkCompliance(repoPath, { contract: synthetic });
|
|
2962
|
+
return { record, check, ruleset_upgraded: recorded.ruleset_digest !== synthetic.ruleset_digest };
|
|
2963
|
+
}
|
|
2964
|
+
export async function checkEngagementReVerdict(repoPath, outcome) {
|
|
2965
|
+
const repoRoot = path.resolve(repoPath);
|
|
2966
|
+
const { record, check, ruleset_upgraded } = await reVerdictOne(repoPath, repoRoot, outcome);
|
|
2967
|
+
const drifted = check.status !== "green";
|
|
2968
|
+
return {
|
|
2969
|
+
mode: "re-verdict",
|
|
2970
|
+
engagement: outcome,
|
|
2971
|
+
...(typeof record.request === "string" ? { request: record.request } : {}),
|
|
2972
|
+
recorded_at: record.recorded_at,
|
|
2973
|
+
recorded_verdict: record.verdict,
|
|
2974
|
+
ruleset_upgraded,
|
|
2975
|
+
status: check.status,
|
|
2976
|
+
exitCode: drifted ? 11 : 0,
|
|
2977
|
+
summary: check.summary,
|
|
2978
|
+
...(check.completeness ? { completeness: check.completeness } : {}),
|
|
2979
|
+
rules: check.rules.filter((rule) => BASELINE_NON_GREEN.has(rule.status)),
|
|
2980
|
+
note: "Re-verdict of a recorded engagement against the current ruleset and code. It does not change the active engagement's gate; exit 11 signals engagement drift.",
|
|
2981
|
+
};
|
|
2982
|
+
}
|
|
2983
|
+
export async function checkAllEngagements(repoPath, options = {}) {
|
|
2984
|
+
const repoRoot = path.resolve(repoPath);
|
|
2985
|
+
const active = await checkCompliance(repoPath, options);
|
|
2986
|
+
const activeNonGreenKeys = new Set(active.rules.filter((rule) => BASELINE_NON_GREEN.has(rule.status)).map((rule) => baselineKeyFor(rule)));
|
|
2987
|
+
const entries = await readdir(engagementsDir(repoRoot), { withFileTypes: true }).catch(() => []);
|
|
2988
|
+
const engagements = [];
|
|
2989
|
+
let anyDrift = false;
|
|
2990
|
+
for (const entry of entries) {
|
|
2991
|
+
if (!entry.isDirectory() || entry.name === active.outcome) {
|
|
2992
|
+
continue;
|
|
2993
|
+
}
|
|
2994
|
+
const { record, check, ruleset_upgraded } = await reVerdictOne(repoPath, repoRoot, entry.name);
|
|
2995
|
+
const nonGreen = check.rules.filter((rule) => BASELINE_NON_GREEN.has(rule.status));
|
|
2996
|
+
const gating = nonGreen.filter((rule) => !activeNonGreenKeys.has(baselineKeyFor(rule)));
|
|
2997
|
+
const shared = nonGreen.length - gating.length;
|
|
2998
|
+
const completenessMissing = check.completeness?.missing ?? [];
|
|
2999
|
+
const drifted = gating.length > 0 || completenessMissing.length > 0 || check.status === "contract-drift";
|
|
3000
|
+
anyDrift = anyDrift || drifted;
|
|
3001
|
+
engagements.push({
|
|
3002
|
+
outcome: entry.name,
|
|
3003
|
+
...(typeof record.request === "string" ? { request: record.request } : {}),
|
|
3004
|
+
recorded_at: record.recorded_at,
|
|
3005
|
+
recorded_verdict: record.verdict,
|
|
3006
|
+
ruleset_upgraded,
|
|
3007
|
+
status: drifted ? check.status : "green",
|
|
3008
|
+
drift: drifted,
|
|
3009
|
+
...(shared > 0 ? { shared_with_active: shared } : {}),
|
|
3010
|
+
...(gating.length > 0 ? { gating } : {}),
|
|
3011
|
+
...(completenessMissing.length > 0 ? { completeness_missing: completenessMissing.map((item) => item.id) } : {}),
|
|
3012
|
+
});
|
|
3013
|
+
}
|
|
3014
|
+
engagements.sort((a, b) => String(a.outcome).localeCompare(String(b.outcome)));
|
|
3015
|
+
const activeGreen = active.exitCode === 0;
|
|
3016
|
+
return {
|
|
3017
|
+
...active,
|
|
3018
|
+
...(activeGreen && anyDrift
|
|
3019
|
+
? {
|
|
3020
|
+
status: "engagement-drift",
|
|
3021
|
+
exitCode: 11,
|
|
3022
|
+
active_status: active.status,
|
|
3023
|
+
active_exitCode: active.exitCode,
|
|
3024
|
+
}
|
|
3025
|
+
: {}),
|
|
3026
|
+
engagements,
|
|
3027
|
+
engagements_note: "Re-verdicts of recorded engagements against the current ruleset and code. Findings already gating the active engagement are counted once there (shared_with_active). Exit 11 = active engagement green but a recorded engagement drifted.",
|
|
3028
|
+
};
|
|
3029
|
+
}
|
|
2830
3030
|
export async function recordBaseline(repoPath) {
|
|
2831
3031
|
const repoRoot = path.resolve(repoPath);
|
|
2832
3032
|
const compliance = await readCompliance(repoRoot);
|
|
@@ -2841,19 +3041,18 @@ export async function recordBaseline(repoPath) {
|
|
|
2841
3041
|
const key = baselineKeyFor(result);
|
|
2842
3042
|
findings[key] = (findings[key] ?? 0) + 1;
|
|
2843
3043
|
}
|
|
2844
|
-
const
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
];
|
|
3044
|
+
const deliverables_not_baselined = {
|
|
3045
|
+
completeness_missing: (check.completeness?.missing ?? []).length,
|
|
3046
|
+
selected_optional: (check.selectedOptionalCapabilities?.failed ?? []).length + (check.selectedOptionalCapabilities?.unknown ?? []).length,
|
|
3047
|
+
};
|
|
2849
3048
|
const baseline = {
|
|
2850
3049
|
baselined_at: new Date().toISOString(),
|
|
2851
3050
|
vise_version: packageVersion,
|
|
2852
3051
|
outcome: compliance.outcome,
|
|
2853
3052
|
ruleset_digest: compliance.ruleset_digest,
|
|
2854
3053
|
findings,
|
|
2855
|
-
completeness_missing,
|
|
2856
|
-
selected_optional,
|
|
3054
|
+
completeness_missing: [],
|
|
3055
|
+
selected_optional: [],
|
|
2857
3056
|
};
|
|
2858
3057
|
await writeJson(baselinePath(repoRoot), baseline);
|
|
2859
3058
|
return {
|
|
@@ -2862,9 +3061,15 @@ export async function recordBaseline(repoPath) {
|
|
|
2862
3061
|
outcome: baseline.outcome,
|
|
2863
3062
|
recorded: {
|
|
2864
3063
|
findings: Object.values(findings).reduce((a, b) => a + b, 0),
|
|
2865
|
-
completeness_missing: completeness_missing.length,
|
|
2866
|
-
selected_optional: selected_optional.length,
|
|
2867
3064
|
},
|
|
3065
|
+
...(deliverables_not_baselined.completeness_missing + deliverables_not_baselined.selected_optional > 0
|
|
3066
|
+
? {
|
|
3067
|
+
deliverables_not_baselined: {
|
|
3068
|
+
...deliverables_not_baselined,
|
|
3069
|
+
note: "The outcome's completeness checklist and selected optional capabilities are this engagement's deliverables — they are never baselined and still gate `vise check --new-only`. Use a completeness opt-out for capabilities the product genuinely does not want.",
|
|
3070
|
+
},
|
|
3071
|
+
}
|
|
3072
|
+
: {}),
|
|
2868
3073
|
nextStep: "Implement your change, then run `vise check --new-only` to gate only on findings introduced since this baseline. Re-run `vise baseline` after the contract or outcome changes.",
|
|
2869
3074
|
};
|
|
2870
3075
|
}
|