@decantr/verifier 3.4.0 → 3.5.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/dist/index.js CHANGED
@@ -344,6 +344,300 @@ function auditComponentReuse(projectRoot, sourceFiles) {
344
344
  };
345
345
  }
346
346
 
347
+ // src/contracts-v2.ts
348
+ var VERIFICATION_COMMON_V2_SCHEMA_URL = "https://decantr.ai/schemas/verification-report.common.v2.json";
349
+ var PROJECT_HEALTH_REPORT_V2_SCHEMA_URL = "https://decantr.ai/schemas/project-health-report.v2.json";
350
+ var DECANTR_CI_REPORT_V2_SCHEMA_URL = "https://decantr.ai/schemas/decantr-ci-report.v2.json";
351
+ var WORKSPACE_HEALTH_REPORT_V2_SCHEMA_URL = "https://decantr.ai/schemas/workspace-health-report.v2.json";
352
+ var EVIDENCE_BUNDLE_V2_SCHEMA_URL = "https://decantr.ai/schemas/evidence-bundle.v2.json";
353
+ var RUNTIME_PROBE_PAYLOAD_V2_SCHEMA_URL = "https://decantr.ai/schemas/runtime-probe-payload.v2.json";
354
+ var LOOP_READINESS_V2_SCHEMA_URL = "https://decantr.ai/schemas/loop-readiness.v2.json";
355
+ var AUTHORITY_RESOLUTION_V2_SCHEMA_URL = "https://decantr.ai/schemas/authority-resolution.v2.json";
356
+ var PROOF_FIELD_REPORT_V2_SCHEMA_URL = "https://decantr.ai/schemas/proof-field-report.v2.json";
357
+ function unique(values) {
358
+ return [...new Set(values)];
359
+ }
360
+ function rounded(value) {
361
+ return Number(Math.max(0, Math.min(1, value)).toFixed(2));
362
+ }
363
+ function createEvidenceTier(report, options = {}) {
364
+ const runtimeProbeCount = options.runtimeProbeCount ?? (report.summary.runtimeAuditChecked ? 1 : 0);
365
+ const visualArtifactCount = options.visualArtifactCount ?? 0;
366
+ const findingsAnchored = report.findings.filter((finding) => finding.graph).length;
367
+ const findingsWithRepairPlan = report.findings.filter((finding) => finding.repairPlan).length;
368
+ const capabilities = ["static-audit", "project-health"];
369
+ if (report.graph.ready) capabilities.push("typed-graph");
370
+ if (runtimeProbeCount > 0) capabilities.push("runtime-probe");
371
+ if (report.summary.runtimeAuditChecked || visualArtifactCount > 0)
372
+ capabilities.push("browser-evidence");
373
+ if (visualArtifactCount > 0) capabilities.push("visual-baseline");
374
+ if (findingsWithRepairPlan > 0) capabilities.push("repair-plan");
375
+ if (options.benchmarkReplay) capabilities.push("benchmark-replay");
376
+ let stage = "static";
377
+ if (options.benchmarkReplay) stage = "proof";
378
+ else if (findingsWithRepairPlan > 0) stage = "repair";
379
+ else if (visualArtifactCount > 0) stage = "visual";
380
+ else if (runtimeProbeCount > 0 || report.summary.runtimeAuditChecked) stage = "runtime";
381
+ else if (report.graph.ready) stage = "graph";
382
+ const routeCoverage = report.routes.declared.length > 0 ? Math.min(1, report.routes.runtimeChecked.length / report.routes.declared.length) : report.summary.runtimeAuditChecked ? 1 : 0;
383
+ const graphCoverage = report.findings.length > 0 ? findingsAnchored / report.findings.length : report.graph.ready ? 1 : 0;
384
+ const repairCoverage = report.findings.length > 0 ? findingsWithRepairPlan / report.findings.length : 1;
385
+ const runtimeCoverage = runtimeProbeCount > 0 || report.summary.runtimeAuditChecked ? 1 : 0;
386
+ const score = rounded(
387
+ 0.22 + (report.graph.ready ? 0.22 : 0) + routeCoverage * 0.16 + graphCoverage * 0.16 + repairCoverage * 0.14 + runtimeCoverage * 0.1
388
+ );
389
+ const level = score >= 0.78 ? "high" : score >= 0.48 ? "moderate" : "low";
390
+ const reasons = [];
391
+ reasons.push(report.graph.ready ? "typed graph is ready" : "typed graph is missing or stale");
392
+ reasons.push(
393
+ runtimeProbeCount > 0 || report.summary.runtimeAuditChecked ? "runtime evidence is present" : "runtime evidence has not run"
394
+ );
395
+ reasons.push(
396
+ report.findings.length === 0 ? "no open findings" : `${findingsAnchored}/${report.findings.length} finding(s) have graph anchors`
397
+ );
398
+ reasons.push(
399
+ report.findings.length === 0 ? "repair context is not needed" : `${findingsWithRepairPlan}/${report.findings.length} finding(s) include repair plans`
400
+ );
401
+ return {
402
+ schemaVersion: 2,
403
+ stage,
404
+ status: report.status === "healthy" ? "healthy" : report.status,
405
+ capabilities: unique(capabilities),
406
+ coverage: {
407
+ declaredRoutes: report.routes.declared.length,
408
+ runtimeRoutesChecked: report.routes.runtimeChecked.length,
409
+ findingsAnchored,
410
+ findingsWithRepairPlan,
411
+ runtimeProbeCount,
412
+ visualArtifactCount
413
+ },
414
+ confidence: {
415
+ level,
416
+ score,
417
+ reasons
418
+ }
419
+ };
420
+ }
421
+ function createAuthorityOrder() {
422
+ return [
423
+ {
424
+ id: "production-source",
425
+ label: "Production source",
426
+ role: "Existing runtime/source wins first in Brownfield unless a human explicitly accepts a contract change.",
427
+ rank: 1
428
+ },
429
+ {
430
+ id: "local-law",
431
+ label: "Accepted local law",
432
+ role: "Project-owned rules and codified patterns constrain edits after they are accepted locally.",
433
+ rank: 2
434
+ },
435
+ {
436
+ id: "style-bridge",
437
+ label: "Accepted style bridge",
438
+ role: "Mapped native style tokens/components govern Hybrid styling once accepted locally.",
439
+ rank: 3
440
+ },
441
+ {
442
+ id: "essence-contract",
443
+ label: "Essence V4 contract",
444
+ role: "Structural route, section, page, guard, and DNA contract for Decantr context.",
445
+ rank: 4
446
+ },
447
+ {
448
+ id: "registry-guidance",
449
+ label: "Hosted packs and registry",
450
+ role: "Advisory guidance unless mapped into accepted local law or the local style bridge.",
451
+ rank: 5
452
+ }
453
+ ];
454
+ }
455
+ function laneForFinding(finding) {
456
+ if (finding.source === "style-bridge") return "style-bridge";
457
+ if (finding.source === "brownfield" || finding.category.toLowerCase().includes("drift")) {
458
+ return "production-source";
459
+ }
460
+ if (finding.source === "check" || finding.source === "assertion") return "essence-contract";
461
+ if (finding.source === "pack") return "registry-guidance";
462
+ return "local-law";
463
+ }
464
+ function actionsForFinding(finding) {
465
+ const commands = finding.repairPlan?.commands ?? finding.remediation?.commands ?? [];
466
+ const repairCommand = commands[0] ?? null;
467
+ const source = finding.source;
468
+ const actions = [];
469
+ if (source === "graph") {
470
+ actions.push({
471
+ kind: "regenerate_graph",
472
+ label: "Regenerate graph/context evidence",
473
+ command: "decantr graph --check || decantr graph",
474
+ writes: true,
475
+ rationale: "Graph drift should be resolved by regenerating derived graph artifacts from current source."
476
+ });
477
+ } else if (source === "style-bridge") {
478
+ actions.push({
479
+ kind: "update_style_bridge",
480
+ label: "Update accepted style bridge",
481
+ command: "decantr codify --style-bridge",
482
+ writes: true,
483
+ rationale: "Style conflicts should be accepted through the explicit Hybrid style bridge workflow."
484
+ });
485
+ } else if (source === "brownfield") {
486
+ actions.push({
487
+ kind: "accept_observed_source",
488
+ label: "Accept observed source into contract",
489
+ command: "decantr init --existing --merge-proposal",
490
+ writes: true,
491
+ rationale: "Brownfield source is first authority; accept it explicitly before treating contract drift as repaired."
492
+ });
493
+ } else if (source === "pack") {
494
+ actions.push({
495
+ kind: "regenerate_context",
496
+ label: "Regenerate Decantr context",
497
+ command: "decantr refresh",
498
+ writes: true,
499
+ rationale: "Generated context and pack artifacts are derived from the local contract."
500
+ });
501
+ } else {
502
+ actions.push({
503
+ kind: "repair_source",
504
+ label: "Repair source to satisfy accepted authority",
505
+ command: repairCommand,
506
+ writes: false,
507
+ rationale: "The finding can be repaired by editing the relevant application source under human/agent control."
508
+ });
509
+ }
510
+ actions.push({
511
+ kind: "defer_to_drift_log",
512
+ label: "Defer to drift log",
513
+ command: `decantr resolve --defer ${finding.id}`,
514
+ writes: true,
515
+ rationale: "Record an explicit human deferral when this conflict should not block the current task."
516
+ });
517
+ if (finding.severity !== "error") {
518
+ actions.push({
519
+ kind: "mark_advisory",
520
+ label: "Mark advisory",
521
+ command: `decantr resolve --mark-advisory ${finding.id}`,
522
+ writes: true,
523
+ rationale: "Warnings can be documented as advisory when the observed source is intentionally different."
524
+ });
525
+ }
526
+ return actions;
527
+ }
528
+ function createAuthorityResolution(report) {
529
+ const conflicts = report.findings.filter((finding) => finding.severity !== "info").map((finding) => ({
530
+ id: finding.id,
531
+ source: finding.source,
532
+ category: finding.category,
533
+ severity: finding.severity,
534
+ message: finding.message,
535
+ ...finding.graph ? { graphAnchor: finding.graph } : {},
536
+ lane: laneForFinding(finding),
537
+ status: finding.severity === "error" ? "blocking" : finding.source === "brownfield" || finding.source === "style-bridge" ? "repairable" : "advisory",
538
+ recommendedActions: actionsForFinding(finding)
539
+ }));
540
+ const activeLane = report.summary.workflowMode === "brownfield-attach" ? "production-source" : "essence-contract";
541
+ return {
542
+ schemaVersion: 2,
543
+ order: createAuthorityOrder(),
544
+ activeLane,
545
+ summary: activeLane === "production-source" ? "Brownfield authority: preserve observed production source first, then accepted local law/style bridge, then Essence V4 structure, with hosted packs advisory." : "Contract authority: Essence V4 and accepted local project law guide source changes; hosted packs remain advisory until accepted locally.",
546
+ conflicts,
547
+ stopRule: "If runtime source and Decantr context disagree, stop and report drift instead of guessing which truth to overwrite."
548
+ };
549
+ }
550
+ function readTargetsForFindings(report) {
551
+ const targets = /* @__PURE__ */ new Set(["DECANTR.md", "decantr.essence.json"]);
552
+ for (const finding of report.findings) {
553
+ for (const target of finding.repairPlan?.readTargets ?? []) targets.add(target);
554
+ if (finding.file) targets.add(finding.file);
555
+ if (finding.source === "graph") targets.add(".decantr/graph/graph.manifest.json");
556
+ if (finding.source === "style-bridge") targets.add(".decantr/style-bridge.json");
557
+ }
558
+ return [...targets].sort();
559
+ }
560
+ function createLoopReadiness(report, authority = createAuthorityResolution(report), evidenceTier = createEvidenceTier(report)) {
561
+ const hasEssence = Boolean(report.summary.essenceVersion);
562
+ const graphMissing = !report.graph.ready;
563
+ const blockingConflicts = authority.conflicts.filter(
564
+ (conflict) => conflict.status === "blocking"
565
+ );
566
+ const repairableConflicts = authority.conflicts.filter(
567
+ (conflict) => conflict.status === "repairable"
568
+ );
569
+ let state = "verified";
570
+ const blockingReasons = [];
571
+ if (!hasEssence) {
572
+ state = "blocked_missing_context";
573
+ blockingReasons.push("Essence V4 context is missing.");
574
+ } else if (graphMissing) {
575
+ state = "blocked_missing_graph";
576
+ blockingReasons.push("Typed Contract graph is missing, stale, or incomplete.");
577
+ } else if (blockingConflicts.length > 0 && repairableConflicts.length > 0) {
578
+ state = "human_resolution_required";
579
+ blockingReasons.push("Blocking findings conflict with Brownfield or Hybrid authority.");
580
+ } else if (blockingConflicts.length > 0 || report.summary.errorCount > 0) {
581
+ state = "repair_required";
582
+ blockingReasons.push("Project Health has blocking errors.");
583
+ } else if (repairableConflicts.length > 0 || report.summary.warnCount > 0) {
584
+ state = "repair_required";
585
+ blockingReasons.push("Project Health has repairable warnings or drift.");
586
+ } else if (report.status === "healthy") {
587
+ state = "verified";
588
+ } else {
589
+ state = "verify_required";
590
+ blockingReasons.push("Verification needs to be rerun before the loop is complete.");
591
+ }
592
+ const nextActions = state === "verified" ? ["Continue normal route/task workflow."] : state === "blocked_missing_context" ? ["Run `decantr scan` for a read-only preview, then `decantr adopt` when ready."] : state === "blocked_missing_graph" ? ["Run `decantr graph` to regenerate typed graph artifacts, then rerun verification."] : state === "human_resolution_required" ? ["Run `decantr resolve` and choose an explicit resolution action before editing."] : state === "repair_required" ? ["Repair the highest-severity finding, then rerun the verify command."] : ["Run the verify command before treating the task as complete."];
593
+ const status = state.startsWith("blocked") || state === "human_resolution_required" ? "blocked" : report.status;
594
+ return {
595
+ schemaVersion: 2,
596
+ state,
597
+ status,
598
+ verdict: state === "verified" ? "Loop verified." : "Loop needs attention before it should be considered complete.",
599
+ summary: `${report.status} Project Health, ${evidenceTier.confidence.level} evidence confidence, ${authority.conflicts.length} authority conflict(s).`,
600
+ authority: {
601
+ activeLane: authority.activeLane,
602
+ summary: authority.summary,
603
+ stopRule: authority.stopRule
604
+ },
605
+ evidenceTier,
606
+ blockingReasons,
607
+ nextActions,
608
+ maker: {
609
+ title: "Maker instructions",
610
+ instructions: [
611
+ "Read the route/task context before editing source.",
612
+ "Preserve production source behavior and accepted local law unless a human chooses a resolution action.",
613
+ "Stop and report drift when runtime source and Decantr context disagree."
614
+ ]
615
+ },
616
+ checker: {
617
+ title: "Checker instructions",
618
+ instructions: [
619
+ "Rerun the verify command after edits.",
620
+ "Check graph anchors and repair plans before accepting a fix.",
621
+ "Treat critique-only or advisory evidence as warning-level unless runtime evidence proves a failure."
622
+ ]
623
+ },
624
+ readTargets: readTargetsForFindings(report),
625
+ graphImpact: {
626
+ status: !report.graph.present ? "missing" : report.graph.ready && report.graph.current !== false ? "ready" : "stale",
627
+ snapshotId: report.graph.snapshotId,
628
+ sourceHash: report.graph.sourceHash,
629
+ sourceArtifactCount: report.graph.sourceArtifactCount,
630
+ staleArtifacts: report.graph.staleArtifacts
631
+ },
632
+ stopConditions: [
633
+ "Runtime source and Decantr context disagree.",
634
+ "Typed graph is missing for the route or changed file.",
635
+ "A finding requires contract/source/local-law mutation outside the current explicit workflow."
636
+ ],
637
+ verifyCommand: report.ci.recommendedCommand
638
+ };
639
+ }
640
+
347
641
  // src/runtime.ts
348
642
  import { existsSync, readdirSync, readFileSync as readFileSync2, statSync } from "fs";
349
643
  import { createServer } from "http";
@@ -1160,13 +1454,16 @@ function isAcceptedTokenValue(value, tokenHints) {
1160
1454
  return value.includes(normalizedHint) || value.includes(`var(${normalizedHint})`);
1161
1455
  });
1162
1456
  }
1163
- function arbitraryInlineStyleValue(property, value, tokenHints) {
1457
+ function arbitraryInlineStyleValue(property, value, tokenHints, options = {}) {
1164
1458
  const normalizedProperty = property.trim();
1165
1459
  const normalizedValue = value.trim();
1166
1460
  if (!normalizedProperty || !normalizedValue) return null;
1167
1461
  const isVisualProperty = INLINE_STYLE_PROPERTIES.has(normalizedProperty) || STYLESHEET_VISUAL_PROPERTIES.has(normalizedProperty) || normalizedProperty.startsWith("--");
1168
1462
  if (!isVisualProperty) return null;
1169
1463
  if (isAcceptedTokenValue(normalizedValue, tokenHints)) return null;
1464
+ if (options.allowCssVariableValues && /\bvar\(\s*--[A-Za-z0-9_-]+/i.test(normalizedValue)) {
1465
+ return null;
1466
+ }
1170
1467
  if (/(?:^|[\s,(])#[0-9a-f]{3,8}\b|(?:rgba?|hsla?|oklch|oklab|lch|lab|color-mix)\(/i.test(
1171
1468
  normalizedValue
1172
1469
  )) {
@@ -1286,7 +1583,9 @@ function collectStylesheetBridgeDriftFindings(input) {
1286
1583
  const property = match.groups?.property;
1287
1584
  const rawValue = match.groups?.value;
1288
1585
  if (!property || !rawValue) continue;
1289
- const value = arbitraryInlineStyleValue(property, rawValue, input.bridge.tokenHints);
1586
+ const value = arbitraryInlineStyleValue(property, rawValue, input.bridge.tokenHints, {
1587
+ allowCssVariableValues: true
1588
+ });
1290
1589
  if (!value) continue;
1291
1590
  const lineNumber = lineIndex + 1;
1292
1591
  const key = `${file}:${lineNumber}:${value}`;
@@ -2005,9 +2304,9 @@ function sourceLineCount(source) {
2005
2304
  }
2006
2305
  function verifyInteractionsInSource(interactions, sources) {
2007
2306
  if (interactions.length === 0 || sources.size === 0) return [];
2008
- const unique = Array.from(new Set(interactions));
2307
+ const unique2 = Array.from(new Set(interactions));
2009
2308
  const missing = [];
2010
- for (const interaction of unique) {
2309
+ for (const interaction of unique2) {
2011
2310
  const requirement = INTERACTION_SIGNALS[interaction];
2012
2311
  if (!requirement) {
2013
2312
  continue;
@@ -3729,17 +4028,17 @@ function resolveSourceSymbolOrigin(context, pathOrSourceFile, localName, options
3729
4028
 
3730
4029
  // src/index.ts
3731
4030
  var VERIFICATION_SCHEMA_URLS = {
3732
- common: "https://decantr.ai/schemas/verification-report.common.v1.json",
4031
+ common: VERIFICATION_COMMON_V2_SCHEMA_URL,
3733
4032
  projectAudit: "https://decantr.ai/schemas/project-audit-report.v1.json",
3734
- projectHealth: "https://decantr.ai/schemas/project-health-report.v1.json",
3735
- decantrCi: "https://decantr.ai/schemas/decantr-ci-report.v1.json",
3736
- evidenceBundle: "https://decantr.ai/schemas/evidence-bundle.v1.json",
4033
+ projectHealth: PROJECT_HEALTH_REPORT_V2_SCHEMA_URL,
4034
+ decantrCi: DECANTR_CI_REPORT_V2_SCHEMA_URL,
4035
+ evidenceBundle: EVIDENCE_BUNDLE_V2_SCHEMA_URL,
3737
4036
  scanReport: "https://decantr.ai/schemas/scan-report.v1.json",
3738
- workspaceHealth: "https://decantr.ai/schemas/workspace-health-report.v1.json",
4037
+ workspaceHealth: WORKSPACE_HEALTH_REPORT_V2_SCHEMA_URL,
3739
4038
  fileCritique: "https://decantr.ai/schemas/file-critique-report.v1.json",
3740
4039
  showcaseShortlist: "https://decantr.ai/schemas/showcase-shortlist-report.v1.json"
3741
4040
  };
3742
- var EVIDENCE_BUNDLE_SCHEMA_URL = "https://decantr.ai/schemas/evidence-bundle.v1.json";
4041
+ var EVIDENCE_BUNDLE_SCHEMA_URL = EVIDENCE_BUNDLE_V2_SCHEMA_URL;
3743
4042
  function hashString(value) {
3744
4043
  return createHash("sha256").update(value).digest("hex").slice(0, 16);
3745
4044
  }
@@ -3941,7 +4240,8 @@ function createContractAssertions(projectRoot, audit) {
3941
4240
  if (v4) {
3942
4241
  const declaredRoutes = Object.keys(v4.blueprint.routes ?? {}).sort();
3943
4242
  for (const route of declaredRoutes) {
3944
- const routeTarget = (v4.blueprint.routes ?? {})[route];
4243
+ const routeTarget = v4.blueprint.routes?.[route];
4244
+ if (!routeTarget) continue;
3945
4245
  const section = v4.blueprint.sections.find((entry) => entry.id === routeTarget.section);
3946
4246
  const page = section?.pages.find((entry) => entry.id === routeTarget.page);
3947
4247
  assertions.push(
@@ -4027,6 +4327,8 @@ function createContractAssertions(projectRoot, audit) {
4027
4327
  }
4028
4328
  function createEvidenceBundle(input) {
4029
4329
  const assertions = input.assertions ?? createContractAssertions(input.projectRoot, input.audit);
4330
+ const evidenceTier = input.report.evidenceTier ?? createEvidenceTier(input.report);
4331
+ const authority = input.report.authority ?? createAuthorityResolution(input.report);
4030
4332
  let resolvedProjectRoot = input.projectRoot;
4031
4333
  try {
4032
4334
  resolvedProjectRoot = realpathSync2(input.projectRoot);
@@ -4039,13 +4341,18 @@ function createEvidenceBundle(input) {
4039
4341
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
4040
4342
  project: {
4041
4343
  id: projectId,
4042
- rootLabel: basename2(input.projectRoot) || "project"
4344
+ rootLabel: basename2(input.projectRoot) || "project",
4345
+ workflowMode: input.report.summary.workflowMode,
4346
+ adoptionMode: input.report.summary.adoptionMode
4043
4347
  },
4044
4348
  toolchain: {
4045
- verifierVersion: input.verifierVersion ?? null
4349
+ verifierVersion: input.verifierVersion ?? null,
4350
+ cliVersion: input.cliVersion ?? null,
4351
+ mcpServerVersion: input.mcpServerVersion ?? null
4046
4352
  },
4047
4353
  privacy: {
4048
4354
  localOnly: true,
4355
+ sourceIncluded: false,
4049
4356
  redactedFields: [
4050
4357
  "source",
4051
4358
  "prompt",
@@ -4054,8 +4361,10 @@ function createEvidenceBundle(input) {
4054
4361
  "absolute_path",
4055
4362
  "repository_name"
4056
4363
  ],
4057
- screenshotsLocalOnly: true
4364
+ screenshotsLocalOnly: true,
4365
+ hostedUploadAllowed: false
4058
4366
  },
4367
+ evidenceTier,
4059
4368
  health: {
4060
4369
  status: input.report.status,
4061
4370
  score: input.report.score,
@@ -4073,7 +4382,9 @@ function createEvidenceBundle(input) {
4073
4382
  graphDiff: provenanceEntry(input.projectRoot, ".decantr/graph/graph.diff.json"),
4074
4383
  contractCapsule: provenanceEntry(input.projectRoot, ".decantr/graph/contract-capsule.json"),
4075
4384
  ...input.workspaceConfigPath ? { workspaceConfig: provenanceForPath(input.projectRoot, input.workspaceConfigPath) } : {},
4076
- ...input.designTokensPath ? { designTokens: provenanceForPath(input.projectRoot, input.designTokensPath) } : {}
4385
+ ...input.designTokensPath ? { designTokens: provenanceForPath(input.projectRoot, input.designTokensPath) } : {},
4386
+ ...input.runtimeProbePath ? { runtimeProbe: provenanceForPath(input.projectRoot, input.runtimeProbePath) } : {},
4387
+ ...input.visualManifestPath ? { visualManifest: provenanceForPath(input.projectRoot, input.visualManifestPath) } : {}
4077
4388
  },
4078
4389
  assertions,
4079
4390
  findings: input.report.findings.map((finding) => ({
@@ -4090,10 +4401,37 @@ function createEvidenceBundle(input) {
4090
4401
  graph: finding.graph,
4091
4402
  repair: redactRepairAction(input.projectRoot, finding.repair),
4092
4403
  repairPlan: buildProjectHealthRepairPlan(input.projectRoot, finding),
4404
+ evidenceTier: finding.evidenceTier ?? evidenceTier,
4405
+ authorityLane: finding.authorityLane ?? authority.conflicts.find((conflict) => conflict.id === finding.id)?.lane ?? authority.activeLane,
4406
+ resolutionActions: finding.resolutionActions ?? authority.conflicts.find((conflict) => conflict.id === finding.id)?.recommendedActions,
4407
+ privacy: finding.privacy ?? {
4408
+ sourceIncluded: false,
4409
+ redacted: true,
4410
+ localOnly: true
4411
+ },
4412
+ loopVerdict: finding.loopVerdict ?? input.report.loop?.state,
4093
4413
  remediationSummary: finding.remediation.summary,
4094
4414
  commands: finding.remediation.commands,
4095
4415
  promptCommand: `decantr health --prompt ${finding.id}`
4096
4416
  })),
4417
+ runtimeProbe: input.runtimeProbe ?? null,
4418
+ artifacts: input.artifacts ?? [
4419
+ {
4420
+ id: "artifact:project-health",
4421
+ kind: "project-health",
4422
+ path: "decantr-health.json",
4423
+ hash: null,
4424
+ localOnly: true,
4425
+ redacted: true
4426
+ }
4427
+ ],
4428
+ dashboard: {
4429
+ safeToUpload: false,
4430
+ retentionClass: "local-only",
4431
+ redactionNotes: [
4432
+ "Evidence Bundle v2 does not include raw source, prompts, secrets, absolute paths, or uploaded screenshots by default."
4433
+ ]
4434
+ },
4097
4435
  ...input.browser ? { browser: input.browser } : {},
4098
4436
  ...input.designTokens ? { designTokens: input.designTokens } : {}
4099
4437
  };
@@ -4848,6 +5186,10 @@ function buildRegistryContext(projectRoot) {
4848
5186
  const patternRegistry = /* @__PURE__ */ new Map();
4849
5187
  const cacheDir = join6(projectRoot, ".decantr", "cache");
4850
5188
  const customDir = join6(projectRoot, ".decantr", "custom");
5189
+ const addPattern = (id, source, data = null) => {
5190
+ if (typeof id !== "string" || !id.trim() || patternRegistry.has(id)) return;
5191
+ patternRegistry.set(id, data ?? { id, source });
5192
+ };
4851
5193
  const cachedThemesDir = join6(cacheDir, "@official", "themes");
4852
5194
  try {
4853
5195
  if (existsSync6(cachedThemesDir)) {
@@ -4881,17 +5223,27 @@ function buildRegistryContext(projectRoot) {
4881
5223
  (name) => name.endsWith(".json") && name !== "index.json"
4882
5224
  )) {
4883
5225
  const data = JSON.parse(readFileSync6(join6(cachedPatternsDir, file), "utf-8"));
4884
- if (data.id && !patternRegistry.has(data.id)) {
4885
- patternRegistry.set(data.id, data);
4886
- }
5226
+ addPattern(data.id, "cache", data);
4887
5227
  }
4888
5228
  }
4889
5229
  } catch {
4890
5230
  }
4891
- for (const id of ["content-section", "footer", "form-basic", "hero", "nav-header"]) {
4892
- if (!patternRegistry.has(id)) {
4893
- patternRegistry.set(id, { id, source: "bundled" });
5231
+ const customPatternsDir = join6(customDir, "patterns");
5232
+ try {
5233
+ if (existsSync6(customPatternsDir)) {
5234
+ for (const file of readdirSync5(customPatternsDir).filter((name) => name.endsWith(".json"))) {
5235
+ const data = JSON.parse(readFileSync6(join6(customPatternsDir, file), "utf-8"));
5236
+ addPattern(data.id, "custom", data);
5237
+ }
4894
5238
  }
5239
+ } catch {
5240
+ }
5241
+ const localPatterns = readJsonIfExists(join6(projectRoot, ".decantr", "local-patterns.json"));
5242
+ for (const pattern of localPatterns?.patterns ?? []) {
5243
+ addPattern(pattern.id, "local-law", pattern);
5244
+ }
5245
+ for (const id of ["content-section", "footer", "form-basic", "hero", "nav-header"]) {
5246
+ addPattern(id, "bundled");
4895
5247
  }
4896
5248
  return { themeRegistry, patternRegistry };
4897
5249
  }
@@ -7666,9 +8018,15 @@ function getJsxTextContent(node) {
7666
8018
  });
7667
8019
  return text;
7668
8020
  }
8021
+ function hasNonEmptyJsxAttribute(attributes, ...names) {
8022
+ const attribute = getJsxAttribute(attributes, ...names);
8023
+ if (!attribute?.initializer) return false;
8024
+ const literalValue = getJsxAttributeLiteralValue(attribute);
8025
+ return literalValue === null || literalValue.trim().length > 0;
8026
+ }
7669
8027
  function hasAccessibleLabel(attributes, textContent) {
7670
8028
  return Boolean(
7671
- getJsxAttribute(attributes, "aria-label", "aria-labelledby", "title") || textContent.trim().length > 0
8029
+ hasNonEmptyJsxAttribute(attributes, "aria-label", "aria-labelledby", "title") || textContent.trim().length > 0
7672
8030
  );
7673
8031
  }
7674
8032
  function isNonSemanticInteractiveTag(tagName) {
@@ -7693,6 +8051,18 @@ function collectLabelForIds(root) {
7693
8051
  walk(root);
7694
8052
  return ids;
7695
8053
  }
8054
+ function collectElementIds(root) {
8055
+ const ids = /* @__PURE__ */ new Set();
8056
+ const walk = (node) => {
8057
+ if (ts6.isJsxSelfClosingElement(node) || ts6.isJsxOpeningElement(node)) {
8058
+ const idValue = getJsxAttributeLiteralValue(getJsxAttribute(node.attributes, "id"));
8059
+ if (idValue?.trim()) ids.add(idValue.trim());
8060
+ }
8061
+ ts6.forEachChild(node, walk);
8062
+ };
8063
+ walk(root);
8064
+ return ids;
8065
+ }
7696
8066
  function isWrappedInJsxLabel(node) {
7697
8067
  let current = node.parent;
7698
8068
  while (current) {
@@ -7715,21 +8085,37 @@ function hasAncestorJsxTag(node, tagName) {
7715
8085
  }
7716
8086
  function isLabelableFormControl(tagName, attributes) {
7717
8087
  if (!tagName) return false;
7718
- if (tagName === "select" || tagName === "textarea") return true;
7719
- if (tagName !== "input") return false;
8088
+ const normalizedTagName = tagName.toLowerCase();
8089
+ if (normalizedTagName === "select" || normalizedTagName === "textarea") return true;
8090
+ if (normalizedTagName !== "input" && !isLikelyProjectFormControlTag(tagName)) return false;
7720
8091
  const inputType = (getJsxAttributeLiteralValue(getJsxAttribute(attributes, "type")) ?? "text").toLowerCase();
7721
8092
  return !["hidden", "submit", "reset", "button"].includes(inputType);
7722
8093
  }
8094
+ function isLikelyProjectFormControlTag(tagName) {
8095
+ if (tagName === tagName.toLowerCase()) return false;
8096
+ const normalizedTagName = tagName.toLowerCase();
8097
+ return /(?:^|(?:text|email|password|search|number|tel|url|date))input$/.test(normalizedTagName) || /^(?:textarea|textArea|select|combobox|comboBox|checkbox|radio|switch)$/i.test(tagName);
8098
+ }
7723
8099
  function getNormalizedInputType(attributes) {
7724
8100
  return (getJsxAttributeLiteralValue(getJsxAttribute(attributes, "type")) ?? "text").trim().toLowerCase();
7725
8101
  }
7726
- function hasFormControlLabel(node, tagName, attributes, labelForIds, textContent) {
8102
+ function hasFormControlLabel(node, tagName, attributes, labelForIds, elementIds, textContent) {
7727
8103
  if (!isLabelableFormControl(tagName, attributes)) return true;
7728
- if (hasAccessibleLabel(attributes, textContent)) return true;
8104
+ if (hasNonEmptyJsxAttribute(attributes, "aria-label", "title")) return true;
8105
+ if (hasAriaLabelledByReference(attributes, elementIds)) return true;
8106
+ if (textContent.trim().length > 0) return true;
7729
8107
  if (isWrappedInJsxLabel(node)) return true;
7730
8108
  const idValue = getJsxAttributeLiteralValue(getJsxAttribute(attributes, "id"));
7731
8109
  return Boolean(idValue && labelForIds.has(idValue));
7732
8110
  }
8111
+ function hasAriaLabelledByReference(attributes, elementIds) {
8112
+ const attribute = getJsxAttribute(attributes, "aria-labelledby");
8113
+ if (!attribute?.initializer) return false;
8114
+ const literalValue = getJsxAttributeLiteralValue(attribute);
8115
+ if (literalValue === null) return true;
8116
+ const references = literalValue.trim().split(/\s+/).filter(Boolean);
8117
+ return references.some((reference) => elementIds.has(reference));
8118
+ }
7733
8119
  function isExternalLinkTargetBlankWithoutRel(attributes) {
7734
8120
  const targetValue = getJsxAttributeLiteralValue(getJsxAttribute(attributes, "target"));
7735
8121
  if (targetValue !== "_blank") return false;
@@ -9414,7 +9800,7 @@ function countAuthAnonymousRedirectSignals(code) {
9414
9800
  ];
9415
9801
  return patterns.reduce((count, pattern) => count + (pattern.test(code) ? 1 : 0), 0);
9416
9802
  }
9417
- var OPEN_REDIRECT_QUERY_KEY_PATTERN = String.raw`next|redirect(?:To|[_-]to)?|return(?:To|[_-]to)?|callback(?:Url|[_-]url)?|continue(?:Url|[_-]url)?|from`;
9803
+ var OPEN_REDIRECT_QUERY_KEY_PATTERN = "next|redirect(?:To|[_-]to)?|return(?:To|[_-]to)?|callback(?:Url|[_-]url)?|continue(?:Url|[_-]url)?|from";
9418
9804
  var OPEN_REDIRECT_SOURCE_PATTERN = String.raw`\b(?:searchParams|(?:request|req)\.nextUrl\.searchParams|url\.searchParams)\.get\s*\(\s*['"\`](?:${OPEN_REDIRECT_QUERY_KEY_PATTERN})['"\`]\s*\)|\b(?:router\.query|route\.query|query)\.(?:${OPEN_REDIRECT_QUERY_KEY_PATTERN})\b|\b(?:new\s+)?URLSearchParams\s*\(\s*(?:(?:window|globalThis|document|self|parent|top)\.)?location\.(?:search|hash)(?:\.slice\(\s*1\s*\)|\.replace\([^)]*\))?\s*\)\.get\s*\(\s*['"\`](?:${OPEN_REDIRECT_QUERY_KEY_PATTERN})['"\`]\s*\)|\bnew\s+URL\(\s*(?:request|req)\.url\s*\)\.searchParams\.get\s*\(\s*['"\`](?:${OPEN_REDIRECT_QUERY_KEY_PATTERN})['"\`]\s*\)`;
9419
9805
  var OPEN_REDIRECT_SOURCE_REGEX = new RegExp(OPEN_REDIRECT_SOURCE_PATTERN, "i");
9420
9806
  var OPEN_REDIRECT_QUERY_KEY_REGEX = new RegExp(`^(?:${OPEN_REDIRECT_QUERY_KEY_PATTERN})$`, "i");
@@ -13330,6 +13716,7 @@ function analyzeAstSignals(filePath, code) {
13330
13716
  const namedExpressionInitializers = collectNamedExpressionInitializers(sourceFile);
13331
13717
  const namedPropertyAliases = collectNamedPropertyAliases(sourceFile);
13332
13718
  const labelForIds = collectLabelForIds(sourceFile);
13719
+ const elementIds = collectElementIds(sourceFile);
13333
13720
  let navigationLandmarkCount = 0;
13334
13721
  let unlabeledNavigationLandmarkCount = 0;
13335
13722
  const walk = (node) => {
@@ -13736,7 +14123,7 @@ function analyzeAstSignals(filePath, code) {
13736
14123
  signals.authInputWithoutNameCount += 1;
13737
14124
  }
13738
14125
  }
13739
- if (!hasFormControlLabel(node, tagName, node.attributes, labelForIds, "")) {
14126
+ if (!hasFormControlLabel(node, tagName, node.attributes, labelForIds, elementIds, "")) {
13740
14127
  signals.formControlWithoutLabelCount += 1;
13741
14128
  }
13742
14129
  }
@@ -13851,6 +14238,7 @@ function analyzeAstSignals(filePath, code) {
13851
14238
  tagName,
13852
14239
  node.openingElement.attributes,
13853
14240
  labelForIds,
14241
+ elementIds,
13854
14242
  textContent
13855
14243
  )) {
13856
14244
  signals.formControlWithoutLabelCount += 1;
@@ -15636,14 +16024,23 @@ async function critiqueFile(filePath, projectRoot) {
15636
16024
  });
15637
16025
  }
15638
16026
  export {
16027
+ AUTHORITY_RESOLUTION_V2_SCHEMA_URL,
15639
16028
  COMPONENT_REUSE_RULE_ID,
16029
+ DECANTR_CI_REPORT_V2_SCHEMA_URL,
15640
16030
  EVIDENCE_BUNDLE_SCHEMA_URL,
16031
+ EVIDENCE_BUNDLE_V2_SCHEMA_URL,
15641
16032
  INTERACTION_SIGNALS,
15642
16033
  KNOWN_VERIFICATION_DIAGNOSTICS,
16034
+ LOOP_READINESS_V2_SCHEMA_URL,
16035
+ PROJECT_HEALTH_REPORT_V2_SCHEMA_URL,
16036
+ PROOF_FIELD_REPORT_V2_SCHEMA_URL,
15643
16037
  RAW_CONTROL_REUSE_RULE_ID,
16038
+ RUNTIME_PROBE_PAYLOAD_V2_SCHEMA_URL,
15644
16039
  SCAN_REPORT_SCHEMA_URL,
15645
16040
  STYLE_BRIDGE_ARBITRARY_VALUE_RULE_ID,
16041
+ VERIFICATION_COMMON_V2_SCHEMA_URL,
15646
16042
  VERIFICATION_SCHEMA_URLS,
16043
+ WORKSPACE_HEALTH_REPORT_V2_SCHEMA_URL,
15647
16044
  anchorFindingsToGraph,
15648
16045
  auditBuiltDist,
15649
16046
  auditComponentReuse,
@@ -15653,8 +16050,12 @@ export {
15653
16050
  collectMissingPackManifestFiles,
15654
16051
  collectProjectSourceFiles,
15655
16052
  collectSourceImports,
16053
+ createAuthorityOrder,
16054
+ createAuthorityResolution,
15656
16055
  createContractAssertions,
15657
16056
  createEvidenceBundle,
16057
+ createEvidenceTier,
16058
+ createLoopReadiness,
15658
16059
  createProjectSourceProgram,
15659
16060
  createSourceInventory,
15660
16061
  createUnavailableScanReport,