Skip to content

Shared Modules

Z-M-Huang edited this page Feb 25, 2026 · 8 revisions

Shared Modules

VCP's TypeScript modules live in plugins/vcp/lib/. They provide shared logic used by both hooks and skills (via CLI entrypoints).

Module Overview

plugins/vcp/lib/
├── global-config.ts               # Global config (~/.vcp/config.json) load/save/resolve
├── global-config.test.ts          # 42 tests
├── vcp-context-core.ts            # Core extraction logic (shared by hooks + CLI entrypoints)
├── vcp-context-core.test.ts       # 40 tests
├── vcp-logger.ts                  # Shared diagnostic logging (hooks + lib modules)
├── vcp-logger.test.ts             # 9 tests
├── resolve-config.ts              # CLI entrypoint for scanning skills
└── generate-context.ts            # CLI entrypoint for /vcp-context skill

global-config.ts

The module for loading, saving, and resolving global VCP configuration (~/.vcp/config.json).

Exported Types

Type Description
VcpGlobalConfig Parsed ~/.vcp/config.json: standards_url, pluginRoot, debug?, defaults?

Exported Constants

Constant Value
DEFAULT_MANIFEST_URL "https://raw.githubusercontent.com/Z-M-Huang/vcp/main/standards/manifest.json"

Exported Functions

  • globalConfigPath(): string — Returns path.join(os.homedir(), ".vcp", "config.json")
  • loadGlobalConfig(): Promise<VcpGlobalConfig | null> — Reads ~/.vcp/config.json. Returns parsed config or null on any error. Never throws.
  • saveGlobalConfig(config: VcpGlobalConfig): Promise<void> — Writes config. Creates ~/.vcp/ directory if needed.
  • ensureGlobalConfig(projectConfig): Promise<VcpGlobalConfig | null> — Returns existing global config, or auto-creates one from project config's pluginRoot + DEFAULT_MANIFEST_URL. Returns null if no config exists and project config lacks pluginRoot, or if the write fails. Security: Always uses DEFAULT_MANIFEST_URL — never promotes project config's standards_url to global config (project config is untrusted). Only used in the skills path (resolve-config.ts), never in hooks.
  • validateStandardsUrl(url): string | null — Validates a URL is safe to fetch. Rejects non-HTTPS, localhost, private/link-local IPs, IPv6 ULA, IPv4-mapped IPv6 loopback/private, trailing-dot localhost, and cloud metadata endpoints. Returns null if valid, error message string if not.
  • resolveStandardsUrl(globalConfig, projectConfig): string | null — Resolution: project → global → null. Validates the resolved URL via validateStandardsUrl().
  • resolvePluginRoot(globalConfig, projectConfig): string | null — Resolution: project → global → null
  • mergeIgnoreArrays(globalIgnore, projectIgnore): string[] — Deduplicated union of both arrays
  • applyGlobalDefaults(globalConfig, projectConfig): VcpConfig — Applies global defaults. severity: project wins. scopes/compliance: project wins. ignore: union.

vcp-context-core.ts

The central module that all context generation and config resolution flows through.

Exported Types

Type Description
VcpConfig Parsed .vcp/config.json with fields: version, scopes, compliance, ignore?, frameworks?, exclude?, severity?, pluginRoot?
ParsedIgnores Categorized ignore entries: { standards: Set, rules: Set, cwes: Set }
StandardEntry Single entry from manifest: { id, url, scope, severity, tags, applies }
Manifest Parsed manifest.json: { version, repository, scopes, standards: StandardEntry[] }
ScopedRules Map of scope to array of { standardId, severity, rules: Array<{number, title}> }

Exported Functions

loadConfig(projectRoot: string): Promise<VcpConfig | null>

Reads .vcp/config.json from the project root using Bun.file(). Returns parsed config or null if the file doesn't exist or can't be parsed.

parseIgnoreList(ignore: string[]): ParsedIgnores

Categorizes ignore entries into three sets:

  • Standards: Entries without /rule- and not matching CWE-\d+ (e.g., "core-architecture")
  • Rules: Entries containing /rule- (e.g., "core-security/rule-3")
  • CWEs: Entries matching CWE-\d+ (e.g., "CWE-798")

fetchManifest(url: string): Promise<Manifest>

Fetches standards/manifest.json from the provided URL. Detects manifest version: v1 manifests construct full URLs from standards_base_url + path for backward compatibility; v2 manifests use full HTTPS URLs directly. Each scope manifest URL is validated via validateStandardsUrl() before fetching — unsafe URLs cause the fetch to throw. Throws on any fetch failure (including individual scope manifests). No caching.

resolveApplicableStandards(manifest, config): StandardEntry[]

Filters the manifest's standards array by:

  1. applies === "always" (core standards, always included)
  2. applies matches an active scope from config.scopes
  3. applies matches "compliance:{framework}" where framework is in config.compliance

Then removes any standard whose id appears in the parsed ignore list's standards set.

Without config: returns only core standards (applies === "always").

fetchStandards(entries): Promise<Map<string, string>>

Fetches each standard's markdown content from entry.url in parallel via Promise.all. Each URL is validated via validateStandardsUrl() before fetching — unsafe URLs are skipped. Emits a console.warn to stderr listing any dropped standards (with reasons). Returns a map of standard ID to markdown content.

extractRuleSummaries(standards, entries, config?): ScopedRules

For each standard:

  1. Finds the ## Rules section in the markdown
  2. Extracts numbered rules using the regex /^(\d+)\.\s+\*\*(.+?)\*\*/gm
  3. Filters out rules matching the parsed ignore list's rules set
  4. Groups results by scope with severity annotation

formatContext(rules: ScopedRules): string

Formats the extracted rules as a markdown block:

  • Grouped by scope (Core first, then alphabetical)
  • Within each scope, ordered by severity (critical, high, medium, low)
  • Prefixed with ## VCP Standards Context
  • Suffixed with > Run /vcp-audit for deep analysis.
  • Applies token budget truncation if output exceeds limits

Token budget truncation algorithm:

  1. Build the full output
  2. If under budget, return as-is
  3. If over budget, greedily include standards from highest severity first
  4. Ensure at least one standard always survives

buildReferenceSection(entries: StandardEntry[], fetched: Map<string, string>): string

Builds a ### Standard References section listing URLs to full standard documents. Only includes entries that exist in the fetched Map (successfully fetched) AND pass validateStandardsUrl() re-validation (defense-in-depth against duplicate-ID URL mismatch). Output is appended after the budget-controlled rule summaries by generateContext().

generateContext(projectRoot: string): Promise<string>

Orchestrator function — the single entry point for hook wrappers:

  1. loadGlobalConfig() → global config or null
  2. loadConfig() → project config or null
  3. applyGlobalDefaults() if global config exists
  4. resolveStandardsUrl() to determine manifest URL
  5. fetchManifest(url)
  6. resolveApplicableStandards()
  7. fetchStandards()
  8. extractRuleSummaries()
  9. formatContext() — budget-controlled rule summaries
  10. buildReferenceSection() — appends standard URLs for on-demand loading

Wrapped in try/catch — returns FALLBACK_MESSAGE on any error.

Exported Constants

Constant Value
FALLBACK_MESSAGE "VCP active but not fully initialized. Run /vcp-init to configure, then /vcp-audit before committing."
SEVERITY_ORDER { critical: 0, high: 1, medium: 2, low: 3 }

resolve-config.ts

CLI entrypoint that scanning skills call via Bash to resolve project configuration.

Usage

bun resolve-config.ts [project-root]

Process

  1. Reads ~/.vcp/config.json (global config)
  2. If global config is missing, auto-creates it via ensureGlobalConfig() (uses DEFAULT_MANIFEST_URL + project's pluginRoot)
  3. Reads .vcp/config.json from the project root
  4. Merges ignore arrays from both configs
  5. Fetches the standards manifest
  6. Resolves applicable standards (with ignore filtering)
  7. Outputs JSON to stdout

Exit Codes

Code Meaning
0 Success — JSON output on stdout
1 Failure — error message on stderr

Output Format

{
  "standardsUrl": "https://raw.githubusercontent.com/.../standards/manifest.json",
  "applicableStandards": [
    {
      "id": "core-security",
      "url": "https://raw.githubusercontent.com/.../standards/core-security.md",
      "scope": "core",
      "severity": "critical",
      "tags": ["security", "owasp", "cwe"]
    }
  ],
  "ignoredRules": ["core-security/rule-3"],
  "severity": "medium",
  "exclude": ["node_modules/**", ".git/**", "dist/**"]
}

Why This Exists

Before resolve-config.ts, each of the 4 scanning skills had ~25 lines of duplicated SKILL.md prose for fetching the manifest, reading .vcp/config.json, resolving scopes, and applying ignores. This CLI centralizes that logic in TypeScript so skills only need to call it via Bash and parse the JSON output.


generate-context.ts

CLI entrypoint for the /vcp-context skill.

Usage

bun generate-context.ts [project-root]

Process

Calls generateContext(projectRoot) and outputs the result to stdout. Always exits 0.

Why This Exists

Skills can't import TypeScript modules directly — they execute via AI instructions, not code. This CLI provides a Bash-callable bridge between the skill's SKILL.md instructions and the shared TypeScript module.


vcp-logger.ts

Shared diagnostic logging utility used by all hooks and available to lib modules.

Exported Function

vcpLog(projectRoot: string, entry: LogEntry, debug?: boolean): Promise<void>

Appends a timestamped log entry to .vcp/vcp.log in the project root. Only writes when debug is true — otherwise returns immediately (no-op).

Parameters:

Parameter Type Description
projectRoot string Absolute path to the project root
entry.source string Hook or module name (e.g., "security-gate", "stop-reminder")
entry.event string Hook event (e.g., "PreToolUse", "SessionStart", "Stop")
entry.decision "allow" | "block" | "warn" | "info" | "error" What the hook decided
entry.details string? Optional — additional context (CWE IDs, char counts, etc.)
debug boolean Whether to write the log entry. Defaults to false. Hooks read this from globalConfig?.debug ?? false.

Behavior:

  • If debug is false (or omitted), returns immediately — no file I/O
  • Validates projectRoot is non-empty and an absolute path; silently returns if not
  • Appends to .vcp/vcp.log (creates the file if it doesn't exist)
  • Never throws — logging failures are silently caught to avoid breaking hook execution

Log format:

ISO_TIMESTAMP [event] source: decision — details

Example:

2026-02-16T10:31:05.789Z [PreToolUse] security-gate: block — CWE-798

Integration

All 4 hooks import vcpLog and loadGlobalConfig, read the debug flag from global config, and pass it to every vcpLog call. Logging only occurs when the user has set debug: true in ~/.vcp/config.json:

Hook Events Logged
security-context.ts info with character count and full injected context between --- BEGIN CONTEXT --- / --- END CONTEXT --- markers
security-gate.ts allow (no findings / empty content), block (CWE IDs), warn (suppressed CWEs)
test-quality-warning.ts warn (issue count + file path), allow (no issues)
stop-reminder.ts info (reminder shown)

Design: Why Hooks and Skills Share a Module

Aspect Hooks Skills
Execution Bun runs TypeScript directly AI interprets SKILL.md instructions
Access to modules import from relative paths Call CLI via Bash
Config access CLAUDE_PROJECT_DIR env var Read .vcp/config.json with pluginRoot
Plugin path CLAUDE_PLUGIN_ROOT (template var in hooks.json) pluginRoot from .vcp/config.json

Both environments need the same logic (config loading, manifest fetch, standard resolution, ignore filtering). The shared module provides it once, with two access patterns:

  • Hooks: Direct import
  • Skills: CLI entrypoint called via Bash

Test Coverage

vcp-context-core.test.ts — 40 tests

Category Tests What's Tested
parseIgnoreList 3 Categorization into standards/rules/cwes, empty input
resolveApplicableStandards 8 Core-only, scope matching, compliance matching, no-config fallback, ignore filtering
extractRuleSummaries 8 Basic extraction, scope grouping, no-rules handling, rule-level ignore, multi-standard
formatContext 6 Output structure, scope grouping, severity ordering, token budget truncation, core-before-non-core ordering
flattenV2Manifest 6 Multi-scope flattening, applies/scope field preservation, all entry fields, empty input, integration with resolveApplicableStandards
buildReferenceSection 3 Only fetched standards included, empty fetched map, unsafe URLs excluded even if fetched
loadConfig auto-migration 3 .vcp/config.json loading, .vcp.json → .vcp/config.json migration, missing config
Integration: security-context.ts 3 Hook exit codes, output format, no-config behavior

vcp-logger.test.ts — 9 tests

Category Tests What's Tested
File creation 1 Creates .vcp/vcp.log in project root (debug=true)
Append behavior 1 Multiple entries appended correctly (debug=true)
Format validation 1 ISO timestamp, event, source, decision, details pattern
Error handling 1 Nonexistent directory does not throw
Guard: empty root 1 Empty projectRoot silently no-ops
Guard: relative root 1 Relative path silently no-ops
Optional details 1 Omits separator when details undefined
Debug: false 1 No .vcp/vcp.log file created when debug=false
Debug: default 1 No .vcp/vcp.log file created when debug omitted (defaults to false)

global-config.test.ts — 42 tests

Category Tests What's Tested
globalConfigPath 2 Path ends in .vcp/config.json, starts with homedir
loadGlobalConfig 1 Never throws (returns null or config)
validateStandardsUrl 16 HTTPS enforcement, localhost, private IPs, link-local, cloud metadata, IPv6 ULA, IPv4-mapped IPv6, trailing-dot localhost, enterprise URLs
resolveStandardsUrl 7 Project wins, global fallback, null when both null, invalid URL rejection, edge cases
resolvePluginRoot 3 Project wins, global fallback, null when both null
mergeIgnoreArrays 3 Union, deduplication, empty arrays
applyGlobalDefaults 7 Severity override, ignore merge, scopes no-merge, compliance no-merge, null global, no mutation
ensureGlobalConfig 3 Returns existing config, null without pluginRoot, always uses DEFAULT_MANIFEST_URL (never promotes project URL)
DEFAULT_MANIFEST_URL 1 URL format validation

Clone this wiki locally