@amityco/social-plus-vise 0.8.1 → 0.12.2
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 +207 -0
- package/README.md +107 -40
- package/dist/capabilities.js +447 -0
- package/dist/outcomes.js +463 -5
- package/dist/server.js +115 -3
- package/dist/tools/ast.js +25 -0
- package/dist/tools/compliance.js +88 -20
- package/dist/tools/debug.js +267 -0
- package/dist/tools/design.js +1496 -0
- package/dist/tools/docs.js +9 -4
- package/dist/tools/harness.js +17 -1
- package/dist/tools/integration.js +83 -7
- package/dist/tools/project.js +872 -67
- package/dist/tools/sdkVersion.js +129 -0
- package/dist/types.js +4 -0
- package/package.json +27 -6
- package/rules/auth.yaml +298 -38
- package/rules/comments.yaml +0 -72
- package/rules/feed.yaml +1151 -12
- package/rules/live-data.yaml +316 -36
- package/rules/push.yaml +140 -0
- package/rules/sdk-lifecycle.yaml +1428 -138
- package/rules/security.yaml +60 -0
- package/skills/social-plus-vise/SKILL.md +98 -55
- package/skills/social-plus-vise/reference/debugging.md +39 -0
- package/skills/social-plus-vise/reference/operations.md +59 -0
- package/skills/vise-harness-engineer/SKILL.md +35 -0
- package/social.plus-vise.png +0 -0
package/dist/server.js
CHANGED
|
@@ -7,12 +7,14 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
|
7
7
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8
8
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
9
9
|
import { attestRule, attestRuleTool, checkCompliance, checkComplianceTool, explainRule, explainRuleTool, initCompliance, initComplianceTool, initEngagement, initEngagementTool, showEngagement, showEngagementTool, statusCompliance, syncCompliance, syncComplianceTool, } from "./tools/compliance.js";
|
|
10
|
+
import { designCheckTool, designExtractTool, designPreviewTool } from "./tools/design.js";
|
|
10
11
|
import { getDocPageTool, searchDocsTool } from "./tools/docs.js";
|
|
11
12
|
import { planHarnessTool } from "./tools/harness.js";
|
|
12
13
|
import { planIntegrationTool } from "./tools/integration.js";
|
|
13
14
|
import { inspectProjectTool, validateSetupTool } from "./tools/project.js";
|
|
14
15
|
import { resolveRequestTool, suggestPatchTool } from "./tools/resolve.js";
|
|
15
16
|
import { runSensorsTool } from "./tools/sensors.js";
|
|
17
|
+
import { debugIssueTool, debugIssue } from "./tools/debug.js";
|
|
16
18
|
import { packageName, packageVersion } from "./version.js";
|
|
17
19
|
const tools = new Map([
|
|
18
20
|
searchDocsTool,
|
|
@@ -31,6 +33,10 @@ const tools = new Map([
|
|
|
31
33
|
runSensorsTool,
|
|
32
34
|
validateSetupTool,
|
|
33
35
|
suggestPatchTool,
|
|
36
|
+
debugIssueTool,
|
|
37
|
+
designExtractTool,
|
|
38
|
+
designCheckTool,
|
|
39
|
+
designPreviewTool,
|
|
34
40
|
].map((tool) => [tool.name, tool]));
|
|
35
41
|
const bundledSkillName = "social-plus-vise";
|
|
36
42
|
const cliResult = await handleCli(process.argv.slice(2));
|
|
@@ -119,6 +125,32 @@ async function handleCli(args) {
|
|
|
119
125
|
});
|
|
120
126
|
return "exit";
|
|
121
127
|
}
|
|
128
|
+
if (command === "debug") {
|
|
129
|
+
assertOnlyKnownFlags(args, ["error", "error-file", "brief"], "debug");
|
|
130
|
+
let errorMessage = flagValue(args, "error");
|
|
131
|
+
if (!errorMessage) {
|
|
132
|
+
const errorFile = flagValue(args, "error-file");
|
|
133
|
+
if (errorFile) {
|
|
134
|
+
errorMessage = await readFile(path.resolve(errorFile), "utf8");
|
|
135
|
+
}
|
|
136
|
+
else if (!process.stdin.isTTY) {
|
|
137
|
+
const { readFileSync } = await import("node:fs");
|
|
138
|
+
try {
|
|
139
|
+
errorMessage = readFileSync(0, "utf-8");
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
errorMessage = undefined;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (!errorMessage) {
|
|
147
|
+
console.error("debug requires --error, --error-file, or piped stdin.");
|
|
148
|
+
process.exitCode = 1;
|
|
149
|
+
return "exit";
|
|
150
|
+
}
|
|
151
|
+
console.log(JSON.stringify(await debugIssue(positionalRepoPath(args.slice(1)), errorMessage, { brief: hasFlag(args, "brief") }), null, 2));
|
|
152
|
+
return "exit";
|
|
153
|
+
}
|
|
122
154
|
if (command === "plan" || command === "plan-integration") {
|
|
123
155
|
await printToolResult(planIntegrationTool, {
|
|
124
156
|
repoPath: positionalRepoPath(args.slice(1)),
|
|
@@ -229,6 +261,44 @@ async function handleCli(args) {
|
|
|
229
261
|
process.exitCode = 1;
|
|
230
262
|
return "exit";
|
|
231
263
|
}
|
|
264
|
+
if (command === "design") {
|
|
265
|
+
const sub = args[1];
|
|
266
|
+
const subArgs = args.slice(2);
|
|
267
|
+
if (sub === "extract") {
|
|
268
|
+
assertOnlyKnownFlags(subArgs, ["repo", "no-write", "from-project"], "design extract");
|
|
269
|
+
if (hasFlag(subArgs, "from-project")) {
|
|
270
|
+
await printToolResult(designExtractTool, {
|
|
271
|
+
fromProject: true,
|
|
272
|
+
repoPath: flagValue(subArgs, "repo") ?? positionalRepoPath(subArgs),
|
|
273
|
+
write: !hasFlag(subArgs, "no-write"),
|
|
274
|
+
});
|
|
275
|
+
return "exit";
|
|
276
|
+
}
|
|
277
|
+
await printToolResult(designExtractTool, {
|
|
278
|
+
prototypePath: requiredPositionalText(subArgs, "design extract requires a prototype path (file or directory), or use --from-project."),
|
|
279
|
+
repoPath: flagValue(subArgs, "repo") ?? ".",
|
|
280
|
+
write: !hasFlag(subArgs, "no-write"),
|
|
281
|
+
});
|
|
282
|
+
return "exit";
|
|
283
|
+
}
|
|
284
|
+
if (sub === "check") {
|
|
285
|
+
assertOnlyKnownFlags(subArgs, [], "design check");
|
|
286
|
+
await printToolResult(designCheckTool, { repoPath: positionalRepoPath(subArgs) });
|
|
287
|
+
return "exit";
|
|
288
|
+
}
|
|
289
|
+
if (sub === "preview") {
|
|
290
|
+
assertOnlyKnownFlags(subArgs, ["reference", "no-write"], "design preview");
|
|
291
|
+
await printToolResult(designPreviewTool, {
|
|
292
|
+
repoPath: positionalRepoPath(subArgs),
|
|
293
|
+
reference: flagValue(subArgs, "reference"),
|
|
294
|
+
write: !hasFlag(subArgs, "no-write"),
|
|
295
|
+
});
|
|
296
|
+
return "exit";
|
|
297
|
+
}
|
|
298
|
+
console.error(`Unknown design subcommand: ${sub ?? "(none)"}. Expected "extract", "check", or "preview".`);
|
|
299
|
+
process.exitCode = 1;
|
|
300
|
+
return "exit";
|
|
301
|
+
}
|
|
232
302
|
}
|
|
233
303
|
catch (error) {
|
|
234
304
|
console.error(error instanceof Error ? error.message : String(error));
|
|
@@ -349,6 +419,16 @@ Run deterministic social.plus setup validation for the current project.
|
|
|
349
419
|
|
|
350
420
|
Usage:
|
|
351
421
|
vise validate [repoPath] [--platform typescript] [--surface apps/web]`;
|
|
422
|
+
}
|
|
423
|
+
if (command === "debug") {
|
|
424
|
+
return `${packageName} debug
|
|
425
|
+
|
|
426
|
+
Correlate an SDK-specific runtime failure to likely compliance issues and emit a minimal repair brief.
|
|
427
|
+
|
|
428
|
+
Usage:
|
|
429
|
+
vise debug [repoPath] --error "401 Unauthorized: TokenExpiredException during social.plus session renewal"
|
|
430
|
+
vise debug [repoPath] --error-file logs/crash.log
|
|
431
|
+
vise debug [repoPath] --error-file logs/crash.log --brief`;
|
|
352
432
|
}
|
|
353
433
|
if (command === "run-sensors" || command === "run-sensor" || command === "run_sensor") {
|
|
354
434
|
return `${packageName} run-sensors
|
|
@@ -414,6 +494,34 @@ Print a compact compliance summary.
|
|
|
414
494
|
Usage:
|
|
415
495
|
vise status [repoPath]`;
|
|
416
496
|
}
|
|
497
|
+
if (command === "design") {
|
|
498
|
+
return `${packageName} design
|
|
499
|
+
|
|
500
|
+
Design-contract tooling for social.plus UI generation.
|
|
501
|
+
|
|
502
|
+
Usage:
|
|
503
|
+
vise design extract <prototypePath> [--repo .] [--no-write]
|
|
504
|
+
vise design extract --from-project [repoPath] [--no-write]
|
|
505
|
+
vise design check [repoPath]
|
|
506
|
+
vise design preview [repoPath] [--reference <prototypePath>]
|
|
507
|
+
|
|
508
|
+
extract Build a graded design contract and write it to sp-vise/design-contract.json.
|
|
509
|
+
Declared CSS custom properties become exact tokens; repeated literal values
|
|
510
|
+
become inferred (advisory) tokens; single-use literals are treated as one-offs.
|
|
511
|
+
With --from-project (no external prototype), derive the contract from the host
|
|
512
|
+
project's OWN design system: CSS custom properties (incl. shadcn :root and
|
|
513
|
+
Tailwind v4 @theme), TS/JS token modules, inline tailwind configs, Android
|
|
514
|
+
colors.xml/dimens.xml, Flutter Color(0x..), and iOS .xcassets/.colorset +
|
|
515
|
+
Swift colors. Reference values (var()/theme()/calc()) are skipped, so a
|
|
516
|
+
var-mapped config contributes nothing rather than wrong tokens.
|
|
517
|
+
check Advisory, non-blocking report on how closely the project's UI code
|
|
518
|
+
matches the contract (token coverage + on/off-contract color literals).
|
|
519
|
+
Never fails a build; it is NOT a \`vise check\` gate.
|
|
520
|
+
preview Write a self-contained sp-vise/design-preview.html: the contract's tokens
|
|
521
|
+
as visual swatches + the conformance report + the HTML reference embedded
|
|
522
|
+
(with --reference) for side-by-side review. Vise renders; a human/VLM
|
|
523
|
+
judges the visual match. Dependency-free — NOT an automated pixel diff.`;
|
|
524
|
+
}
|
|
417
525
|
return `${packageName}
|
|
418
526
|
|
|
419
527
|
Skill-guided deterministic CLI for social.plus SDK integration assistance.
|
|
@@ -425,6 +533,7 @@ Usage:
|
|
|
425
533
|
vise install-skill --target codex Install bundled skill guidance
|
|
426
534
|
vise print-skill Print bundled skill markdown
|
|
427
535
|
vise inspect [repoPath] Inspect platform and design signals
|
|
536
|
+
vise debug [repoPath] --error ... Debug an SDK-specific runtime error and emit a repair brief
|
|
428
537
|
vise plan [repoPath] --request "..." Create an implementation plan
|
|
429
538
|
vise init [repoPath] --request "..." Initialize compliance sidecar
|
|
430
539
|
vise check [repoPath] Check compliance contract
|
|
@@ -434,6 +543,9 @@ Usage:
|
|
|
434
543
|
vise status [repoPath] Print compliance summary
|
|
435
544
|
vise validate [repoPath] Validate setup and common risks
|
|
436
545
|
vise run-sensors [repoPath] Run detected project sensors
|
|
546
|
+
vise design extract <prototype> Extract a design contract from an HTML/CSS prototype
|
|
547
|
+
vise design check [repoPath] Advisory (non-blocking) UI-vs-contract conformance report
|
|
548
|
+
vise design preview [repoPath] Write an HTML visual review of the contract + conformance
|
|
437
549
|
vise doctor Print install diagnostics
|
|
438
550
|
vise --help Show this help
|
|
439
551
|
vise --version Show package version
|
|
@@ -557,7 +669,7 @@ function skillPathResult() {
|
|
|
557
669
|
return {
|
|
558
670
|
skill: bundledSkillName,
|
|
559
671
|
source: skillSourceDir(),
|
|
560
|
-
files: ["SKILL.md"],
|
|
672
|
+
files: ["SKILL.md", "reference/debugging.md", "reference/operations.md"],
|
|
561
673
|
installExamples: [
|
|
562
674
|
"vise install-skill --target codex",
|
|
563
675
|
"vise install-skill --target claude",
|
|
@@ -658,7 +770,7 @@ function ciCheckResult(result) {
|
|
|
658
770
|
};
|
|
659
771
|
}
|
|
660
772
|
function positionalRepoPath(args) {
|
|
661
|
-
const flagsWithValues = new Set(["request", "surface", "surface-path", "platform", "include", "timeout-ms", "query", "path", "limit", "answer", "target", "dest", "destination", "rule", "confidence", "signer", "identity", "evidence-file", "rationale"]);
|
|
773
|
+
const flagsWithValues = new Set(["request", "surface", "surface-path", "platform", "include", "timeout-ms", "query", "path", "limit", "answer", "target", "dest", "destination", "rule", "confidence", "signer", "identity", "evidence-file", "rationale", "repo", "reference"]);
|
|
662
774
|
for (let index = 0; index < args.length; index += 1) {
|
|
663
775
|
const arg = args[index];
|
|
664
776
|
if (!arg) {
|
|
@@ -680,7 +792,7 @@ function positionalRepoPath(args) {
|
|
|
680
792
|
}
|
|
681
793
|
function requiredPositionalText(args, message) {
|
|
682
794
|
const values = [];
|
|
683
|
-
const flagsWithValues = new Set(["request", "surface", "surface-path", "platform", "include", "timeout-ms", "query", "path", "limit", "answer", "target", "dest", "destination", "rule", "confidence", "signer", "identity", "evidence-file", "rationale"]);
|
|
795
|
+
const flagsWithValues = new Set(["request", "surface", "surface-path", "platform", "include", "timeout-ms", "query", "path", "limit", "answer", "target", "dest", "destination", "rule", "confidence", "signer", "identity", "evidence-file", "rationale", "repo", "reference"]);
|
|
684
796
|
for (let index = 0; index < args.length; index += 1) {
|
|
685
797
|
const arg = args[index];
|
|
686
798
|
if (!arg) {
|
package/dist/tools/ast.js
CHANGED
|
@@ -67,13 +67,38 @@ function getParser(language) {
|
|
|
67
67
|
}
|
|
68
68
|
return parser;
|
|
69
69
|
}
|
|
70
|
+
// node-tree-sitter's native parser rejects sufficiently large inputs with a
|
|
71
|
+
// native "Invalid argument" error (~32KB string limit). It IS catchable, but an
|
|
72
|
+
// unguarded caller would otherwise abort the whole validate run. We short-circuit
|
|
73
|
+
// before the native call so callers get a clean, documented, catchable error and
|
|
74
|
+
// can degrade to regex-only for that file. Real codebases routinely have files
|
|
75
|
+
// past this size (1000+ line activities/view controllers).
|
|
76
|
+
export const MAX_PARSE_BYTES = 30000;
|
|
70
77
|
/**
|
|
71
78
|
* Parse source content into a tree-sitter syntax tree.
|
|
79
|
+
* Throws on oversized input (see MAX_PARSE_BYTES) — callers that want graceful
|
|
80
|
+
* degradation should use tryParse or wrap this in try/catch.
|
|
72
81
|
*/
|
|
73
82
|
export function parse(language, source) {
|
|
83
|
+
if (Buffer.byteLength(source, "utf8") > MAX_PARSE_BYTES) {
|
|
84
|
+
throw new Error(`source exceeds tree-sitter parse limit (${Buffer.byteLength(source, "utf8")} bytes > ${MAX_PARSE_BYTES})`);
|
|
85
|
+
}
|
|
74
86
|
const parser = getParser(language);
|
|
75
87
|
return parser.parse(source);
|
|
76
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Parse, returning null instead of throwing when the source can't be parsed
|
|
91
|
+
* (oversized input, native parser error). Lets validators skip AST analysis for
|
|
92
|
+
* one file and fall back to regex without aborting the run.
|
|
93
|
+
*/
|
|
94
|
+
export function tryParse(language, source) {
|
|
95
|
+
try {
|
|
96
|
+
return parse(language, source);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
77
102
|
/**
|
|
78
103
|
* Find all call expressions in the tree whose callee matches a pattern.
|
|
79
104
|
* Returns normalised callee strings and argument nodes.
|
package/dist/tools/compliance.js
CHANGED
|
@@ -2,9 +2,11 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
2
2
|
import { mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { assessProjectCompleteness } from "../capabilities.js";
|
|
5
6
|
import { classifyOutcome } from "../outcomes.js";
|
|
6
7
|
import { objectInput, optionalStringField, stringField, textResult } from "../types.js";
|
|
7
8
|
import { packageVersion } from "../version.js";
|
|
9
|
+
import { readDesignContract } from "./design.js";
|
|
8
10
|
import { inspectProject, validateSetup } from "./project.js";
|
|
9
11
|
const complianceDirName = "sp-vise";
|
|
10
12
|
const attestationsDirName = "attestations";
|
|
@@ -161,7 +163,7 @@ function stringArray(value) {
|
|
|
161
163
|
return value.filter((item) => typeof item === "string" && item.trim() !== "");
|
|
162
164
|
}
|
|
163
165
|
const validTiers = new Set(["free", "pro", "partner"]);
|
|
164
|
-
const validOutcomes = new Set(["setup-sdk", "setup-push", "setup-live-data", "add-feed", "troubleshoot", "validate-setup", "unknown"]);
|
|
166
|
+
const validOutcomes = new Set(["setup-sdk", "setup-push", "setup-live-data", "add-feed", "add-comments", "add-moderation", "add-chat", "troubleshoot", "validate-setup", "unknown"]);
|
|
165
167
|
export async function initEngagement(args) {
|
|
166
168
|
const repoRoot = path.resolve(args.repoPath);
|
|
167
169
|
const engagementFile = engagementPath(repoRoot);
|
|
@@ -227,23 +229,45 @@ export async function initCompliance(repoPath, request, surfacePath) {
|
|
|
227
229
|
const inspection = await inspectProject(repoRoot, surfacePath);
|
|
228
230
|
const outcome = classifyOutcome(request);
|
|
229
231
|
const rules = await applicableRules(outcome, inspection.platforms);
|
|
230
|
-
const refs = rules.map(ruleRef);
|
|
232
|
+
const refs = rules.map(ruleRef); // minimal shape — stable digest input
|
|
233
|
+
const fileRefs = rules.map(ruleRefForFile); // adds title for human/agent readers
|
|
231
234
|
const engagement = await readEngagement(repoRoot);
|
|
235
|
+
const designContract = await readDesignContract(repoRoot);
|
|
232
236
|
const compliance = {
|
|
233
237
|
schema_version: schemaVersion,
|
|
234
238
|
foundry_version: packageVersion,
|
|
235
|
-
ruleset_digest: digestJson(refs),
|
|
239
|
+
ruleset_digest: digestJson(refs), // hash of minimal refs (no title)
|
|
236
240
|
generated_at: new Date().toISOString(),
|
|
237
241
|
last_synced_at: null,
|
|
238
242
|
outcome,
|
|
239
243
|
engagement_id: engagement?.engagement_id,
|
|
240
244
|
surface: inspection.selectedSurface ? { path: inspection.selectedSurface.path, platforms: inspection.selectedSurface.platforms } : undefined,
|
|
241
|
-
rules:
|
|
245
|
+
rules: fileRefs, // file carries titles
|
|
246
|
+
design_contract: designContract
|
|
247
|
+
? {
|
|
248
|
+
digest: designContract.digest,
|
|
249
|
+
strength: designContract.stats.strength,
|
|
250
|
+
source_kind: designContract.source.kind,
|
|
251
|
+
declared_tokens: designContract.stats.declared_tokens,
|
|
252
|
+
inferred_tokens: designContract.stats.inferred_tokens,
|
|
253
|
+
}
|
|
254
|
+
: undefined,
|
|
242
255
|
};
|
|
243
256
|
await mkdir(attestationsDir(repoRoot), { recursive: true });
|
|
244
257
|
await writeJson(compliancePath(repoRoot), compliance);
|
|
245
258
|
await writeJson(path.join(sidecarDir(repoRoot), "inspection.json"), inspection);
|
|
246
259
|
await writeFile(path.join(sidecarDir(repoRoot), "README.md"), sidecarReadme(compliance), "utf8");
|
|
260
|
+
// Write a frozen check snapshot so agents can see current rule status immediately
|
|
261
|
+
// without having to run vise check themselves.
|
|
262
|
+
const checkSnapshot = await checkCompliance(repoPath);
|
|
263
|
+
await writeJson(path.join(sidecarDir(repoRoot), "findings.json"), {
|
|
264
|
+
snapshot_at: compliance.generated_at,
|
|
265
|
+
note: "Snapshot taken at vise init time. Re-run `vise check .` after editing code to see current status.",
|
|
266
|
+
outcome: checkSnapshot.outcome,
|
|
267
|
+
status: checkSnapshot.status,
|
|
268
|
+
summary: checkSnapshot.summary,
|
|
269
|
+
rules: checkSnapshot.rules,
|
|
270
|
+
});
|
|
247
271
|
const warnings = [];
|
|
248
272
|
if (engagement && engagement.scope.outcomes.length > 0 && !engagement.scope.outcomes.includes(outcome)) {
|
|
249
273
|
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.`);
|
|
@@ -255,12 +279,13 @@ export async function initCompliance(repoPath, request, surfacePath) {
|
|
|
255
279
|
surfacePath: inspection.selectedSurface?.path,
|
|
256
280
|
rules: refs.length,
|
|
257
281
|
engagement_id: engagement?.engagement_id,
|
|
282
|
+
...(compliance.design_contract && { design_contract: compliance.design_contract }),
|
|
258
283
|
...(warnings.length > 0 && { warnings }),
|
|
259
284
|
nextStep: "Run vise check, then implement until rules pass deterministically or are attested.",
|
|
260
285
|
};
|
|
261
286
|
}
|
|
262
287
|
export async function applicableComplianceRuleSummaries(outcome, platforms) {
|
|
263
|
-
return (await applicableRules(outcome, platforms)).map(
|
|
288
|
+
return (await applicableRules(outcome, platforms)).map(ruleRefForFile);
|
|
264
289
|
}
|
|
265
290
|
export async function checkCompliance(repoPath) {
|
|
266
291
|
const repoRoot = path.resolve(repoPath);
|
|
@@ -278,8 +303,12 @@ export async function checkCompliance(repoPath) {
|
|
|
278
303
|
};
|
|
279
304
|
}
|
|
280
305
|
const inspection = await inspectProject(repoRoot, compliance.surface?.path === "." ? undefined : compliance.surface?.path);
|
|
281
|
-
const
|
|
282
|
-
const
|
|
306
|
+
const detectedPlatforms = inspection.platforms;
|
|
307
|
+
const recordedPlatforms = compliance.surface?.platforms || [];
|
|
308
|
+
const platformsToValidate = Array.from(new Set([...detectedPlatforms, ...recordedPlatforms]));
|
|
309
|
+
const platforms = platformsToValidate.length > 0 ? platformsToValidate : ["unknown"];
|
|
310
|
+
const allFindings = await Promise.all(platforms.map((p) => validateSetup(inspection.effectiveRoot, p)));
|
|
311
|
+
const findings = allFindings.flat();
|
|
283
312
|
const findingsById = new Map(findings.map((finding) => [finding.ruleId, finding]));
|
|
284
313
|
const attestations = await readAttestations(repoRoot);
|
|
285
314
|
const results = [];
|
|
@@ -295,6 +324,7 @@ export async function checkCompliance(repoPath) {
|
|
|
295
324
|
// user knows what to provide.
|
|
296
325
|
const blockersFired = await runBlockers(rule, inspection.effectiveRoot);
|
|
297
326
|
if (blockersFired.length > 0) {
|
|
327
|
+
const attestable = rule.enforcement.attestation.allowed;
|
|
298
328
|
results.push({
|
|
299
329
|
ruleId: rule.id,
|
|
300
330
|
title: rule.title,
|
|
@@ -303,11 +333,16 @@ export async function checkCompliance(repoPath) {
|
|
|
303
333
|
reason: blockersFired.map((blocker) => blocker.reason).join(" "),
|
|
304
334
|
blockers_fired: blockersFired,
|
|
305
335
|
current_rule: ruleSummary(rule),
|
|
336
|
+
...(attestable && {
|
|
337
|
+
next_step: `Provide the file(s) listed in blockers_fired, then run \`vise attest . --rule ${rule.id}\` to record your review decision.`,
|
|
338
|
+
}),
|
|
306
339
|
});
|
|
307
340
|
continue;
|
|
308
341
|
}
|
|
309
|
-
const
|
|
310
|
-
|
|
342
|
+
const hasDeterministicChecks = (rule.enforcement.deterministic ?? []).length > 0;
|
|
343
|
+
const isInferential = !hasDeterministicChecks && !!rule.enforcement.inferential;
|
|
344
|
+
const finding = hasDeterministicChecks ? deterministicFinding(rule, findingsById) : undefined;
|
|
345
|
+
if (hasDeterministicChecks && !finding) {
|
|
311
346
|
results.push({
|
|
312
347
|
ruleId: rule.id,
|
|
313
348
|
title: rule.title,
|
|
@@ -332,12 +367,14 @@ export async function checkCompliance(repoPath) {
|
|
|
332
367
|
? "Current deterministic check failed; previously synced deterministic-pass evidence is stale."
|
|
333
368
|
: "Current deterministic check failed; this rule does not allow attestation.",
|
|
334
369
|
finding,
|
|
335
|
-
recommendation: finding
|
|
370
|
+
recommendation: finding?.recommendation,
|
|
371
|
+
rationale: rule.rationale,
|
|
336
372
|
current_rule: ruleSummary(rule),
|
|
337
373
|
});
|
|
338
374
|
continue;
|
|
339
375
|
}
|
|
340
|
-
|
|
376
|
+
// ruleset_digest is audit metadata; contractDrift above already guarantees the installed ruleset matches compliance.json.
|
|
377
|
+
const exactMatch = attestation.rule_digest === ref.rule_digest;
|
|
341
378
|
const grandfathered = !exactMatch && isAttestationGrandfathered(rule, attestation);
|
|
342
379
|
if (exactMatch || grandfathered) {
|
|
343
380
|
const sourceFingerprintStatus = await checkSourceFingerprints(repoRoot, inspection.effectiveRoot, attestation.source_fingerprints ?? []);
|
|
@@ -350,7 +387,8 @@ export async function checkCompliance(repoPath) {
|
|
|
350
387
|
status: rule.enforcement.attestation.allowed ? "attestation-needed" : "deterministic-fail",
|
|
351
388
|
reason: "Recorded attestation source fingerprints changed. Re-check the evidence and record a fresh attestation.",
|
|
352
389
|
finding,
|
|
353
|
-
recommendation: finding
|
|
390
|
+
recommendation: finding?.recommendation,
|
|
391
|
+
rationale: rule.rationale,
|
|
354
392
|
current_rule: ruleSummary(rule),
|
|
355
393
|
source_fingerprint_status: sourceFingerprintStatus,
|
|
356
394
|
});
|
|
@@ -375,15 +413,24 @@ export async function checkCompliance(repoPath) {
|
|
|
375
413
|
continue;
|
|
376
414
|
}
|
|
377
415
|
}
|
|
416
|
+
const baseStatus = (rule.enforcement.attestation.allowed || isInferential) ? "attestation-needed" : "deterministic-fail";
|
|
417
|
+
let fallbackReason = "This rule does not allow attestation.";
|
|
418
|
+
if (isInferential) {
|
|
419
|
+
fallbackReason = "Inferential check required. Please provide a host-agent attestation.";
|
|
420
|
+
}
|
|
421
|
+
else if (rule.enforcement.attestation.allowed) {
|
|
422
|
+
fallbackReason = "Deterministic check failed and no valid attestation exists.";
|
|
423
|
+
}
|
|
378
424
|
results.push({
|
|
379
425
|
ruleId: rule.id,
|
|
380
426
|
title: rule.title,
|
|
381
427
|
severity: rule.severity,
|
|
382
|
-
status:
|
|
383
|
-
reason:
|
|
428
|
+
status: baseStatus,
|
|
429
|
+
reason: fallbackReason,
|
|
384
430
|
finding,
|
|
385
|
-
recommendation: finding
|
|
431
|
+
recommendation: finding?.recommendation,
|
|
386
432
|
current_rule: ruleSummary(rule),
|
|
433
|
+
...(isInferential && { inferential_prompt: rule.enforcement.inferential?.prompt })
|
|
387
434
|
});
|
|
388
435
|
}
|
|
389
436
|
const summary = summarize(results);
|
|
@@ -392,6 +439,10 @@ export async function checkCompliance(repoPath) {
|
|
|
392
439
|
const needsAttestation = results.some((result) => result.status === "attestation-needed" || result.status === "stale");
|
|
393
440
|
// Precedence: blocked (exit 3) > deterministic-failures (2) > needs-attestation (1) > green (0).
|
|
394
441
|
// Contract drift (exit 4) is handled earlier and short-circuits the loop.
|
|
442
|
+
// Advisory feature-completeness — surfaced but NEVER part of status/exitCode
|
|
443
|
+
// (completeness is a "this is missing" claim, structurally FP-prone; see the
|
|
444
|
+
// validation-boundaries principle). Failure to assess is silently ignored.
|
|
445
|
+
const completeness = (await assessProjectCompleteness(inspection.effectiveRoot, compliance.outcome).catch(() => null)) ?? undefined;
|
|
395
446
|
// Blocked wins because the agent cannot proceed without customer input;
|
|
396
447
|
// surfacing a smaller failure first would distract from the real blocker.
|
|
397
448
|
return {
|
|
@@ -407,6 +458,7 @@ export async function checkCompliance(repoPath) {
|
|
|
407
458
|
surfacePath: compliance.surface?.path,
|
|
408
459
|
summary,
|
|
409
460
|
rules: results,
|
|
461
|
+
...(completeness && (completeness.missing.length > 0 || completeness.optedOut.length > 0 || completeness.present.length > 0) ? { completeness } : {}),
|
|
410
462
|
};
|
|
411
463
|
}
|
|
412
464
|
export async function syncCompliance(repoPath) {
|
|
@@ -503,12 +555,14 @@ export async function statusCompliance(repoPath) {
|
|
|
503
555
|
const repoRoot = path.resolve(repoPath);
|
|
504
556
|
const check = await checkCompliance(repoPath);
|
|
505
557
|
const engagement = await readEngagement(repoRoot);
|
|
558
|
+
const compliance = await readCompliance(repoRoot).catch(() => null);
|
|
506
559
|
return {
|
|
507
560
|
status: check.status,
|
|
508
561
|
exitCode: check.exitCode,
|
|
509
562
|
outcome: check.outcome,
|
|
510
563
|
surfacePath: check.surfacePath,
|
|
511
564
|
summary: check.summary,
|
|
565
|
+
...(compliance?.design_contract && { design_contract: compliance.design_contract }),
|
|
512
566
|
...(engagement && {
|
|
513
567
|
engagement: {
|
|
514
568
|
engagement_id: engagement.engagement_id,
|
|
@@ -542,7 +596,7 @@ async function loadRuleFiles() {
|
|
|
542
596
|
}
|
|
543
597
|
return loaded;
|
|
544
598
|
}
|
|
545
|
-
async function rulesById() {
|
|
599
|
+
export async function rulesById() {
|
|
546
600
|
const rules = (await loadRuleFiles()).flatMap((file) => file.rules);
|
|
547
601
|
return new Map(rules.map((rule) => [rule.id, rule]));
|
|
548
602
|
}
|
|
@@ -554,6 +608,11 @@ function ruleRef(rule) {
|
|
|
554
608
|
severity: rule.severity,
|
|
555
609
|
};
|
|
556
610
|
}
|
|
611
|
+
// Extends ruleRef with the human-readable title for file output (compliance.json,
|
|
612
|
+
// applicableRules in integration plans). Not used for digest computation.
|
|
613
|
+
function ruleRefForFile(rule) {
|
|
614
|
+
return { ...ruleRef(rule), title: rule.title };
|
|
615
|
+
}
|
|
557
616
|
function contractDrift(compliance, rules) {
|
|
558
617
|
const results = [];
|
|
559
618
|
const refs = compliance.rules.map((ref) => {
|
|
@@ -860,8 +919,11 @@ async function readAttestations(repoRoot) {
|
|
|
860
919
|
continue;
|
|
861
920
|
}
|
|
862
921
|
const attestation = await readJsonIfExists(path.join(dir, entry));
|
|
863
|
-
if (attestation?.rule_id) {
|
|
864
|
-
|
|
922
|
+
if (attestation?.rule_id && attestation.payload_hash) {
|
|
923
|
+
const { payload_hash, ...withoutHash } = attestation;
|
|
924
|
+
if (digestJson(withoutHash) === payload_hash) {
|
|
925
|
+
result.set(attestation.rule_id, attestation);
|
|
926
|
+
}
|
|
865
927
|
}
|
|
866
928
|
}
|
|
867
929
|
return result;
|
|
@@ -898,8 +960,14 @@ function sidecarReadme(compliance) {
|
|
|
898
960
|
`- Rules: ${compliance.rules.length}`,
|
|
899
961
|
`- Generated: ${compliance.generated_at}`,
|
|
900
962
|
"",
|
|
901
|
-
"
|
|
902
|
-
"
|
|
963
|
+
"## Quick start",
|
|
964
|
+
"",
|
|
965
|
+
"1. Read `findings.json` — it contains a snapshot of rule status taken at init time, including any violations found in the current code.",
|
|
966
|
+
"2. Fix the issues listed in `findings.json`, then run `npm run sp-check` (or `vise check .` if vise is on PATH) to verify.",
|
|
967
|
+
"3. Run `vise sync .` to persist deterministic-pass evidence once rules are green.",
|
|
968
|
+
"4. Run `vise attest . --rule <rule-id> ...` to sign off on intentional implementation decisions.",
|
|
969
|
+
"",
|
|
970
|
+
"Attestations include source fingerprints; `vise check` marks them stale if the cited files change.",
|
|
903
971
|
"",
|
|
904
972
|
].join("\n");
|
|
905
973
|
}
|