/** * Files config merge logic, link matching, and deferred path computation. * * Handles the `files` key in skill packaging config: * - Merging defaults + per-skill entries (additive, per-skill wins on dest collision) * - Matching auto-discovered links to files entries * - Computing deferred paths for validation * - Copying declared build artifacts into a skill output dir (every build path) */ import { type ValidationIssue } from '@vibe-agent-toolkit/agent-schema'; import type { SkillFileEntry } from '@vibe-agent-toolkit/resources'; export type { SkillFileEntry } from '@vibe-agent-toolkit/resources'; /** One file a GLOB `files:` entry matched and the never-package list refused. */ export interface DroppedGlobMatch { /** The glob `source:` whose net caught it. */ source: string; /** * ABSOLUTE path of the refused file, forward-slashed. * * The reported subject: this is the one path in this record a reader can * actually open. `dest` names a path that, by definition, does NOT exist — * nothing was written there — so a finding anchored at the dest cannot be * traced back to anything. Kept absolute rather than pre-relativized because * the run's anchor is the caller's to choose (`locationRoot` may not be the * project root when one run spans several projects); every emitter passes it * through {@link issueLocation} at emit time. */ absFile: string; /** * Skill-output-relative dest the file WOULD have had, spelled exactly as a * COPIED dest is (see {@link normalizeRelPath}) so consumers can compare the * two populations without re-deriving either. `checkBrokenPackagedLinks` keys * its never-package remediation on this exact set: a link is "broken because * VAT refused to package it" only when its target is one of THESE paths. * * A SET KEY, not a location: it is the coordinate system the packaged link * graph is expressed in, and mixing it into an issue's `location` — which is * always a source path to open — is what made one issue list speak two * coordinate systems at once. */ dest: string; } /** What {@link applyFilesConfig} accounted for, and what it deliberately did not. */ export interface AppliedFilesConfig { /** * Every skill-output-relative dest this config accounts for, one per FILE * (glob entries expanded). See {@link applyFilesConfig} for why the orphan * check must be told this list. */ dests: string[]; /** * Files a GLOB matched that the never-package list dropped. * * Returned rather than written to a `warn` sink because a file vanishing from * a bundle is a build FINDING, not a log line: it has to reach the structured * report (`issueCounts`), or a CI consumer reads `warnings: 0` for a build * that silently shipped less than the config asked for. */ dropped: DroppedGlobMatch[]; } /** * Turn never-package drops into reportable findings. * * Lives here rather than in the packager because this module owns the concept: * the drop and the issue describing it must not be able to disagree. * * Anchored at the SOURCE file, in ONE coordinate system for both the location * and the message. The finding used to name the would-be `dest` in both, which * meant the only path it gave you was an output path nothing was ever written to * — so the issue list mixed output coordinates (this code) with source * coordinates (every other pre-build code) and an adopter could not open the * thing that went missing. The glob pattern stays in the message because it * answers the second question ("which entry caught this?"), but it is no longer * the only identifiable thing in the finding. * * @param locationRoot The ONE base this run anchors locations at. Required, not * defaulted to the project root: a defaulted anchor is how a multi-project run * silently emits paths relative to the wrong tree. */ export declare function droppedGlobMatchesToIssues(dropped: readonly DroppedGlobMatch[], locationRoot: string): ValidationIssue[]; export declare function buildArtifactHint(source: string): string; /** * Normalize a relative path for comparison: forward slashes, no leading `./`. * The path's ROOT is the caller's business; this only fixes the spelling. * * Exported because it is the spelling every `files:` dest returned by * {@link applyFilesConfig} is normalized to, and that spelling must equal the one * `checkUnreferencedFiles` computes for a packaged file * (`toForwardSlash(safePath.relative(outputDir, file))`) — both are * skill-output-relative. A silent mismatch there reads as "not declared in * `files:` config" about a copy VAT performed itself. */ export declare function normalizeRelPath(p: string): string; /** * The skill-output-relative dest a GLOB `files:` entry gives one absolute source * path, or `undefined` when the entry's expansion would not include it. * * Answers, without running the glob, the question the packager must settle BEFORE * anything is copied: "does the config already declare where this file goes?" A * glob's expansion is late-bound to copy time ({@link copyGlobEntry}), so the path * map used to have no entry for its matches — and a match that link traversal ALSO * found was therefore dropped at its type-derived location while the glob copied * it to the declared dest. Identical bytes shipped twice, the rewritten link * pointed at traversal's copy, and the declared `dest:` was dead. One file, one * dest: the declaration wins, and this is how the path map learns it. * * The three gates below are exactly {@link copyGlobEntry}'s own, restated as a * predicate so the dest computed here is the dest the copy actually writes: * under the static base, matching the magic remainder (`dot: true`, as the copy * expands), and not refused by {@link partitionNeverPackaged}. A never-packaged * match has NO dest — re-pointing the path map at one would rewrite a link to a * file that never arrives. */ export declare function globEntryDest(entry: SkillFileEntry, projectRoot: string, absSource: string): string | undefined; /** * Merge defaults and per-skill files entries. * * Per-skill entries are additive to defaults. When a per-skill entry has the * same dest as a default, the per-skill entry wins (override by dest). * * @throws Error if duplicate dest values exist within the same level (defaults or per-skill) */ export declare function mergeFilesConfig(defaults: SkillFileEntry[] | undefined, perSkill: SkillFileEntry[] | undefined): SkillFileEntry[]; /** * The skill-output-relative dests named by EXPLICIT (non-glob) `files:` entries. * * THE definition of "the config declared this output path", shared by every lane * that has to decide whether a file in a bundle is there on purpose. Naming a * `source`/`dest` pair is an unambiguous instruction to ship that file; a GLOB * never named the file it caught, so its expansion is deliberately absent from * this list — a net is not a declaration. (`walk-link-graph.ts`'s * `refusesAgentInstructionFile` draws the same line on the link side, and * {@link partitionNeverPackaged} on the copy side; all three must agree or the * mechanisms disagree about one bundle.) * * Spelled via {@link normalizeRelPath}, which is the spelling a packaged file's * output-relative path is computed with — so membership can be tested by exact * equality, never by a prefix test. */ export declare function explicitFilesConfigDests(files: readonly SkillFileEntry[]): string[]; /** Options for {@link applyFilesConfig}. */ export interface ApplyFilesConfigOptions { /** Merged `files:` entries to copy. */ filesConfig: SkillFileEntry[]; /** Absolute project root; each `source` resolves relative to it. */ projectRoot: string; /** Absolute skill output dir; each `dest` resolves relative to it. */ skillOutputDir: string; /** * Absolute source paths already materialized by link traversal. * * Used only by NON-GLOB entries, where the packager's path map guarantees the * bundled copy already sits at `entry.dest`, so re-copying is pure duplication. * Glob entries carry no such guarantee and copy unconditionally — see * {@link copyGlobEntry}. Defaults to none (copy all). */ bundledFiles?: string[]; } /** * Verify that each (absSource, absDest) pair has byte-identical content. * * Throws with a message naming the offending dest path on any mismatch or * missing dest. Intended to be called after a copy operation to assert the * copy was faithful. Exported so it can be tested directly without running a * full applyFilesConfig round-trip. */ export declare function verifyFilesIntegrity(pairs: { absSource: string; absDest: string; }[]): void; /** * Verify that the on-disk contents of a glob entry's dest subtree EXACTLY match * the set of rel paths the copy step intended to write — no missing, no extra. * * Why a SET comparison and not just `verifyFilesIntegrity`'s byte check: * `verifyFilesIntegrity` hashes the (absSource, absDest) pairs that the SAME copy * code computed. If the rebase/glob-mapping logic maps a match to the WRONG dest, * that wrong dest rides along in the pair, so `hash(absSource) === hash(absDest)` * still passes and the bug slips through. Enumerating the dest subtree and * diffing against the expected rel set is what catches a misrouted rebase, a * stale leftover, or a dropped file. * * SAFETY ASSUMPTION (why this can't false-positive): a glob entry does NOT own * its dest directory outright — another `files:` entry may legitimately declare a * dest inside the same subtree, and that is exactly how the documented escape * hatch works (an explicit `source: extras/README.md` re-ships a file the glob * refused). So `expectedRel` is the union of what THIS entry copied and every * other entry's declared dest under the same subtree, and the caller runs this * only after ALL entries have copied — see {@link applyFilesConfig}. What is left * over is a file no entry accounts for: a genuine bug, not a co-tenant. (The build * wipes the skill output dir before copying — skill-packager.ts removes * `resolvedOutput` recursively — so there are no stale files either.) * * @param destDir Absolute path to the glob entry's dest subtree. * @param expectedRel Forward-slash rel paths (relative to `destDir`) that the * whole `files:` config accounts for inside this subtree. * @param source The entry's `source` (for error messages only). * @throws if the actual subtree omits an expected file or contains an extra one. */ export declare function verifyDestSet(destDir: string, expectedRel: string[], source: string): Promise; /** One GLOB `files:` entry that currently expands to nothing at all. */ export interface UnmatchedGlobEntry { /** The glob `source:` that matched no files. */ source: string; /** * ABSOLUTE path of the directory it expanded under (the glob's static base), * forward-slashed. Absolute for the same reason as * {@link DroppedGlobMatch.absFile}: the run's anchor is the emitter's choice. * The directory need not exist — "it isn't there yet" is the common case. */ absBase: string; } /** One GLOB `files:` entry whose every match the never-package list refused. */ export interface AllRefusedGlobEntry { /** The glob `source:` that netted only never-packaged files. */ source: string; /** ABSOLUTE path of the directory it expanded under, forward-slashed. */ absBase: string; /** * ABSOLUTE paths of every match, all of them refused. Carried in full because * this finding SUPERSEDES the per-file drops for its entry (see * {@link collectPreBuildGlobFindings}), so it is the only place those file * names are still reported. */ absRefused: string[]; } /** * What the project's GLOB `files:` entries look like BEFORE anything is built. * * The three buckets are MUTUALLY EXCLUSIVE per entry, and they are three of the * FOUR verdicts `copyGlobEntry` reaches at copy time — one entry cannot be both * "matched nothing" and "matched only refused files", and an entry counted in * `allRefused` contributes no `dropped` rows. Overlapping buckets would let one * config produce two findings naming two different causes. * * The fourth verdict has NO bucket: a `..` segment in the glob's magic remainder * makes the pattern unexpandable, `expandGlobEntry` throws, and * {@link collectPreBuildGlobFindings} skips the entry rather than mislabelling it. * So a config certain to fail the build produces no pre-build finding at all — * see the skip in that function for why nothing here can honestly carry it. */ export interface PreBuildGlobFindings { /** * Never-packaged files a glob would catch while still shipping something else. * See {@link DroppedGlobMatch}. */ dropped: DroppedGlobMatch[]; /** Globs that matched, but whose every match was refused — they ship nothing. */ allRefused: AllRefusedGlobEntry[]; /** Globs that currently match nothing — the other input the build dies on. */ unmatched: UnmatchedGlobEntry[]; } /** * What the project's GLOB `files:` entries would do, without copying anything. * * The pre-build half of {@link applyFilesConfig}: `vat skills validate` and * `vat audit` can answer "what will this config fail to ship?" before a build * exists, and they answer it by running THE expansion the copy runs * ({@link expandGlobEntry}) — a second, independently written expansion would be * free to disagree with the copy about what ships, which is the whole defect class * this module exists to close. * * Non-throwing by construction, unlike {@link copyGlobEntry}: a pre-build gate runs * before the artifact exists, so a glob over an unbuilt `dist/` matching nothing is * the expected state, not a failure. `copyGlobEntry` still raises there, where the * build HAS run and zero matches is real. * * That reasoning settles the SEVERITY of a failing expansion, not whether to * mention it — and this used to return only `dropped`, so the gate reported the * drop that is harmless by design (a glob is a net) and said nothing about either * expansion outcome that is certain to fail the very next command. `unmatched` * and `allRefused` end that silence, graded `info` and `warning` respectively (see * FILES_GLOB_MATCHED_NOTHING / FILES_GLOB_MATCHED_ONLY_NEVER_PACKAGED). * * The three buckets partition the entries as two of `copyGlobEntry`'s three throws * plus its success path do — same predicates, same order, one expansion. Its THIRD * throw (`expandGlobEntry` refusing a `..` segment in the magic remainder) has no * bucket, and the skip below explains why — but the consequence belongs here: this * function's answer to "what will this config fail to ship?" is silent for that one * malformed shape, which is the same class of silence `unmatched`/`allRefused` were * added to end. Closing it needs a code of its own (a wrong-shaped PATTERN, not an * unbuilt or unshippable artifact), or a `source` refine on `SkillFileEntrySchema` * that rejects the pattern at config load and makes the skip unreachable. * * Drops re-shipped by an explicit entry are filtered out for the same reason * {@link applyFilesConfig} filters them: reporting "it did not ship" about a file * the documented escape hatch puts in the bundle tells an author their remediation * failed when it worked. That filter is deliberately NOT applied to `allRefused`: * `copyGlobEntry` throws on an entry whose own kept-set is empty no matter what * any other entry ships, so filtering it would predict a green build that fails. */ export declare function collectPreBuildGlobFindings(filesConfig: readonly SkillFileEntry[], projectRoot: string): Promise; /** * Turn {@link collectPreBuildGlobFindings} into reportable pre-build issues. * * THE emitter for the pre-build gates, so a lane cannot pick up one half of the * findings and quietly drop the other — which is how the zero-match went * unreported for as long as it did. * * @param locationRoot The ONE base this run anchors locations at (see * {@link droppedGlobMatchesToIssues}). */ export declare function preBuildGlobFindingsToIssues(findings: PreBuildGlobFindings, locationRoot: string): ValidationIssue[]; export declare function applyFilesConfig(opts: ApplyFilesConfigOptions): Promise; //# sourceMappingURL=files-config.d.ts.map