/** * Enhanced skill validation for packaging * * Extends basic skill validation with: * - Size/complexity validation (SKILL.md lines, total lines, file count) * - Link depth analysis (prevent deep nesting) * - Navigation file detection (README.md, index.md patterns) * - Framework-based severity / allow config (validation.severity, validation.allow) * * Used by: * - vat skills validate (report errors, exit 1 on failure) * - vat skills build (block build on validation errors) * - vat skills audit --user (report issues, exit 0 always) */ import { type AllowUsageLedger, type AllowRecord, type ValidationConfig, type ValidationIssue } from '@vibe-agent-toolkit/agent-schema'; import { ResourceRegistry, type SkillExecutableEntry } from '@vibe-agent-toolkit/resources'; import { type GitTracker } from '@vibe-agent-toolkit/utils'; import type { EvidenceRecord, Observation } from '../evidence/index.js'; import { type DeclaredEvalSuite } from '../test-input.js'; /** * Packaging configuration for skill validation. * Replaces the old VatSkillMetadata parameter — accepts packaging options directly. */ export interface SkillPackagingConfig { linkFollowDepth?: number | 'full'; resourceNaming?: 'basename' | 'resource-id' | 'preserve-path'; stripPrefix?: string; excludeNavigationFiles?: boolean; excludeReferencesFromBundle?: { rules?: Array<{ patterns: string[]; template?: string; }>; defaultTemplate?: string; }; files?: Array<{ source: string; dest: string; }>; /** Framework-based validation configuration (severity overrides and allow entries). */ validation?: ValidationConfig | undefined; /** * Declared runtime targets for this skill. Used by the CLI verdict layer * to suppress non-applicable compat verdicts. The packaging validator * itself only stores the declaration; verdict computation lives in the * CLI (which can also bring in plugin / marketplace target layers). */ targets?: ReadonlyArray<'claude-chat' | 'claude-cowork' | 'claude-code'>; /** * Declared executables the skill ships (name-stable references for eval * `toolExpectations` + launch-guidance linting — issue #145 Phase T/L). The * config merge (`mergeSkillPackagingConfig`) copies this through generically; * declaring it here lets consumers (e.g. `vat skill test run`) read it typed. */ executables?: SkillExecutableEntry[]; /** * The skill's `vat skill test` config. Only `evals` is load-bearing for packaging: * it declares where the skill's TEST INPUT lives, which packaging must exclude * from the shipped bundle (see test-input.ts). The rest of the block is carried * through generically by the config merge and read by `vat skill test`. */ test?: { evals?: string | undefined; } | undefined; } /** Excluded reference detail for verbose output */ export interface ExcludedReferenceDetail { path: string; reason: 'depth-exceeded' | 'pattern-matched' | 'outside-project' | 'navigation-file' | 'agent-instruction-file' | 'skill-definition' | 'gitignored'; matchedPattern?: string | undefined; } /** * Enhanced validation result using the unified framework */ export interface PackagingValidationResult { /** Skill name */ skillName: string; /** * Gate verdict: `error` iff there is an active error. TWO-valued on purpose — * this is the build/validate gate, and a warning does not fail a build. * * It therefore says NOTHING about warnings or info. Read {@link * PackagingValidationResult.allErrors} for the distribution — via * `countBySeverity(result.allErrors)` from `@vibe-agent-toolkit/agent-schema`, * which is the same collapse every other lane uses. */ status: 'success' | 'error'; /** * THE container: every emitted issue after severity resolution, stored once. * * This includes `info`, despite the name: severity resolution keeps info * issues in the framework's `emitted` set. Issues suppressed by `allow` are * NOT here — they live in {@link PackagingValidationResult.ignoredErrors}. * * There are deliberately no `activeErrors` / `activeWarnings` sibling arrays. * They were filtered views over this same array, and because every consumer * that serializes a result spreads the whole object, each issue record — * including its paragraph-length `fix` and `reference` prose — was written to * the output document twice. Derive the partition instead: * {@link activeErrorsOf} / {@link activeWarningsOf}, or `countBySeverity` / * `calculateValidationStatus` from `@vibe-agent-toolkit/agent-schema`. */ allErrors: ValidationIssue[]; /** Issues suppressed by allow entries */ ignoredErrors: AllowRecord[]; /** * Capability observations rolled up from compat detectors. * Carried alongside emitted issues so downstream verdict computation * (CLI layer) can recover observation payloads (e.g. EXTERNAL_CLI binary) * without re-parsing the skill. */ observations: Observation[]; /** * Raw evidence records collected by compat detectors. Surfaced so that * audit `--verbose` can render the underlying matches for each capability * observation without re-parsing the skill. */ evidence: EvidenceRecord[]; /** Metadata about the skill */ metadata: { skillLines: number; totalLines: number; fileCount: number; directFileCount: number; maxLinkDepth: number; excludedReferenceCount: number; excludedReferences: ExcludedReferenceDetail[]; }; } /** Anything carrying the emitted-issue container — a result, or a partial of one. */ type WithAllErrors = Pick; /** * The active errors: emitted, resolved-severity `error`. * * Derived on read, never stored on the result — see the `allErrors` doc comment * for why. Equivalent to `result.status === 'error'` when all you need is the * gate bit; use this only when you need the issues themselves. */ export declare function activeErrorsOf(result: WithAllErrors): ValidationIssue[]; /** The active warnings: emitted, resolved-severity `warning`. Derived on read. */ export declare function activeWarningsOf(result: WithAllErrors): ValidationIssue[]; /** * Build a fresh ResourceRegistry for a single skill's projectRoot and resolve * internal links. Extracted so the skill validator can fall back to a private * registry when the caller does not supply a shared one. * * Crawls markdown AND HTML (`.html`/`.htm`) so the live audit/validate path * sees the same link graph the built path does (issue #129 AC2). The registry * parses HTML via parse5 and surfaces its `local_file` links, so the walker * traverses HTML references and catches HTML broken links at source time — not * just at build time. (Previously the crawl was markdown-only, so source HTML * was invisible to audit/validate.) * * Exported so external callers (e.g. the inventory layer) can build a registry * once and pass it down rather than re-crawling per skill. */ export declare function crawlAndResolveRegistry(projectRoot: string): Promise; /** * Drop every memoized registry. * * Required by any in-process caller that starts an INDEPENDENT run against a * tree it may have changed since the last one — the CLI entrypoint, and * integration tests sharing a vitest worker. Without it a second run reuses the * first run's parse of files that have since moved, and reports a stale answer * as a fresh one. `resetAuditCaches` calls this alongside its own caches. */ export declare function resetPackagingRegistryCache(): void; /** * Shared context for batched skill validation runs. * * Populated once by the caller (e.g. `vat skills validate`) and threaded into * every per-skill validation so common setup is paid for exactly once: * - `registry`: a pre-crawled + `.resolveLinks()`-completed ResourceRegistry * covering the project root. Eliminates the per-skill markdown reparse. * - `gitTracker`: a pre-populated {@link GitTracker} (from * `GitTracker.initialize({ includeUntracked: true })`). Turns gitignore * checks during the link-graph walk into O(1) set lookups instead of * `git check-ignore` spawns. * * Both fields are optional — when omitted, the validator falls back to the * legacy per-skill behavior so one-off callers keep working. */ export interface SkillValidationSharedContext { /** Pre-built registry that covers the skill's project root. */ registry?: ResourceRegistry; /** Pre-populated tracker for the repo that contains the skill. */ gitTracker?: GitTracker; /** * Root every emitted `ValidationIssue.location` is expressed relative to. * * This is the ANCHOR base and it is a DIFFERENT concern from the project * root below: the project root is a validation-POLICY boundary (what counts * as "outside the project", where `files:` sources resolve from, what the * registry crawls), while the anchor base only answers "relative to what is * this location written?". Conflating the two is what let `vat audit` — which * spans many governing configs in a single run — emit one report in many * coordinate systems, with two distinct files sharing one `location`. * * A batching caller MUST pass its invocation scan root. Omitted, it falls * back to the project root, which is correct exactly when a run covers one * project (`vat skills validate`, `vat skills build`, `vat skill review`). */ locationRoot?: string; /** * The RUN's allow-entry usage ledger. * * `validation.allow` is declared once per package but validated once per * skill, so "this entry matched nothing" is a question only the whole run can * answer — a batching caller MUST supply one ledger for the batch and drain it * with `allowUnusedIssues()` after the last skill. Without it, an entry scoped * to one skill's files is reported unused by every OTHER skill in the package * (measured: 78 ALLOW_UNUSED warnings from 3 legitimate entries). * * Omitting it is a positive claim that THIS call is the whole run — correct for * the single-skill callers (`vat skill review`, `vat audit`, whose shared * context is built per skill), where the per-skill and run-level answers * coincide. `vat skills build` supplies one: its pre-build source check is the * ONLY lane in that run that can match an entry scoped to a source filename * (packaging renames the file to `SKILL.md`), so withholding its matches * reported live entries as dead on every skill in the package. * * KNOWN GAP, worth fixing if you are already in this area: the plugin-local * skill loop in `packages/cli/src/commands/claude/plugin/build.ts` still omits * a ledger while looping, so it makes that positive claim falsely. It measures * zero on VAT only because VAT's plugins are assembled by copy-in. See the * comment at that call site for why it was left and what fixing it needs. */ allowLedger?: AllowUsageLedger; /** * EVERY skill the project declares, with its effective packaging config. * * Read only for `test.evals`, and the rule it feeds is PROJECT-WIDE: a file any * skill declares as its eval suite is test input, and this lane must predict a * bundle without it — the same bundle `vat skills build` produces. Without this, * the lane counts a sibling skill's answer key as an ordinary bundled file, and * `vat skills validate` and `vat skills build` disagree about what ships. * * Assemble it ONCE per invocation from the lane's own skill discovery and pass the * same array to every skill; do not rebuild it per skill (that is a whole-project * config walk inside a per-skill loop). Omitting it is a positive claim that the * caller has no project to enumerate — true for a config-free single-skill audit, * false for anything that loops over discovered skills. */ projectSkills?: readonly DeclaredEvalSuite[]; } /** * Validate a skill for packaging * * Performs comprehensive validation including: * - Size/complexity checks * - Link depth analysis * - Navigation file detection * - Framework-based severity / allow config * * @param skillPath - Path to SKILL.md * @param packagingConfig - Optional packaging configuration (depth, excludes, validation) * @param context - Whether the skill is being validated from source or built output * @param shared - Optional shared context (registry + gitTracker) for batched runs * @returns Validation result with active errors, warnings, and allowed issues */ export declare function validateSkillForPackaging(skillPath: string, packagingConfig?: SkillPackagingConfig, context?: 'source' | 'built', shared?: SkillValidationSharedContext): Promise; /** * Detect SKILL_NAME_MISMATCHES_DIR issue from a frontmatter `name` and a * parent directory name. Returns null when no mismatch should be reported. * * Exported for direct unit testing — the packaging validator wires it up * with values derived from the skill path. */ export declare function detectNameMismatchIssue(frontmatterName: unknown, parentDir: string, skillLocation: string): ValidationIssue | null; export {}; //# sourceMappingURL=packaging-validator.d.ts.map