@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/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.
@@ -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: refs,
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(ruleRef);
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 platform = inspection.platforms[0] ?? "unknown";
282
- const findings = await validateSetup(inspection.effectiveRoot, platform);
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 finding = deterministicFinding(rule, findingsById);
310
- if (!finding) {
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.recommendation,
370
+ recommendation: finding?.recommendation,
371
+ rationale: rule.rationale,
336
372
  current_rule: ruleSummary(rule),
337
373
  });
338
374
  continue;
339
375
  }
340
- const exactMatch = attestation.rule_digest === ref.rule_digest && attestation.ruleset_digest === compliance.ruleset_digest;
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.recommendation,
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: rule.enforcement.attestation.allowed ? "attestation-needed" : "deterministic-fail",
383
- reason: rule.enforcement.attestation.allowed ? "Deterministic check failed and no valid attestation exists." : "This rule does not allow attestation.",
428
+ status: baseStatus,
429
+ reason: fallbackReason,
384
430
  finding,
385
- recommendation: finding.recommendation,
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
- result.set(attestation.rule_id, attestation);
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
- "Run `vise check` to verify current status and `vise sync` to persist deterministic-pass evidence.",
902
- "Host-agent and human attestations include source fingerprints for cited files; `vise check` marks them stale if those files change.",
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
  }