Skip to content

Intent-aware skill resource rules: audit/build parity, single-source matrix, and a scenario harness #129

@jdutton

Description

@jdutton

Summary

Umbrella tracking issue for #126 and #127. This is the single design home for all skill-resource link/orphan validation work, delivered as three sequential slices rather than one PR (the three slices touch the same link-validation surface, so they share a lineage, not parallel branches):

Sequencing rationale. The order is logically flexible (the engine does not strictly depend on #126/#127), but slices 1–2 ship first because they fix live adopter pain with small, low-risk PRs, while the engine is the largest piece and still carries open design questions (see "Open design questions" at the bottom). Don't put the least-settled, highest-risk slice in front of two ready fixes.

#126 and #127 are reopened and recast as slices 1–2 (numbers preserved — design history kept, no churn). They are not "closed into" this issue.

VAT decides "is this skill resource OK?" (orphan? broken link? outside the skill boundary?) across three code paths that have diverged, and none of them encodes the intent behind a file's inclusion. The result: vat audit and vat build disagree, source HTML is invisible to audit, the sanctioned files: mechanism doesn't actually silence the error it's meant to resolve, and several fix messages can nudge authors toward workarounds (copying files into dist, suppressing) instead of the right action.

This issue proposes refactoring skill-resource checks into pure, intent-aware verdict functions driven by an explicit context, unified behind one engine, documented by a single-source matrix (registry == docs == runtime), and proven by a DRY scenario-test harness with automatic aliasing detection.

Verified against HEAD 7f81588d (PR #116 merge).

The three paths today

  1. Builtpost-build-checks.ts (checkUnreferencedFiles over walkDir(dist) = only physically-copied files; HTML-aware). → PACKAGED_UNREFERENCED_FILE, PACKAGED_BROKEN_LINK.
  2. Live source/auditvalidateSkillForPackaging (packaging-validator.ts), run by vat audit / vat skills validate on configured skills. Crawls **/*.md only (:189); detects markdown broken links (LINK_MISSING_TARGET); emits no orphan code; never parses HTML.
  3. Wild fallbackskill-validator.ts detectUnreferencedFiles, only when no config and --warn-unreferenced-files. **/*.md only; SKILL_UNREFERENCED_FILE (info).

Genuine gaps

  • Orphan detection missing on the live path. A skill green under vat audit can carry orphans the build later flags.
  • Source HTML blind on the audit/validate paths (md-only crawl).
  • The build only sees what it copied (skill-packager.ts rms dist ~:400, then copies link-reachable + files: entries). A merely-unlinked source file is never copied, so path 1 never sees it either.
  • Deferred-artifact handling is built but unwired. computeDeferredPaths (files-config.ts:134) and walk-link-graph's deferredPaths (:98,:291) exist and are unit-tested, but neither skill-packager nor validateSkillForPackaging passes deferredPaths. So a SKILL.md link to a files:-declared-but-not-yet-built artifact still fires LINK_TO_GITIGNORED_FILE (gitignored dest) / LINK_BROKEN_FILE (no ignore match). This is where the dist-copy/suppress workaround is born — the sanctioned escape doesn't yet silence the error. (Detailed in "Deferred build artifacts (absorbed from Slice 2 of #129: wire deferredPaths + close the files: gitignore-copy leak #127)" below.)

Design

Base layer vs skill overlay

The base resources layer answers "does this link resolve?". The agent-skills layer overlays the contract that supplies intent: a single entry point (SKILL.md) read by an agent under a budget; self-containment (must ship and work for installers); a build transform (dist wiped+regenerated, links rewritten, artifacts deferred); an LLM consumer (mention-vs-link matters); a per-part role.

Intent is edge-scoped and per-skill

A resource may be included by multiple skills / a plugin. Intent attaches to the inclusion edge (the copy) (source→dest, in context C) as a single value, never on the source (which would force a lossy set that can't pair role↔context). Orphan-ness is per skill; a non-skill inbound link does not rescue a skill-internal orphan. The cross-cutting set is a derived aggregation.

FileCopyRole — call-site-derived, on the edge

Author config stays {source, dest}. The engine stamps a closed enum on each copy operation, derived from the call site (skill packaging vs marketplace). SkillFileEntrySchema is .strict() and shared with the marketplace plugin entry (project-config.ts:136,161,203), so the role must not be added there — it lives on the runtime copy record only.

  • Phase 1: 'skill-bundled' | 'plugin-artifact' (the layer distinction — never apply skill expectations to a plugin artifact).
  • Phase 2 (later): within-skill 'skill-doc' | 'skill-executable' | 'skill-asset' …, derived then maybe declared.

Rules as pure verdict functions

verdict(ctx: RuleContext) → Issue | null, with description/fix/defaultSeverity sourced from CODE_REGISTRY. A thin extraction front end derives RuleContext from real inputs; verdict functions never touch the filesystem.

RuleContext = {
  scope: 'base-resource' | 'skill' | 'plugin'
  fileKind: 'doc' | 'executable' | 'asset' | 'data' | 'schema' | 'nav' | 'directory' | 'unknown'
  reachableFromSkillMd: boolean
  referencedHow: 'link' | 'mention' | 'none'
  copyRole?: FileCopyRole      // single value, on the edge
  inFilesConfig: boolean
  existsAtSource: boolean
  looksBuildProduced: boolean
  insideSkillDir: boolean
}

Vocabulary note (directory links). RuleContext.fileKind: 'directory' is the post-stat realization of #126's source-level local_directory link shape: the parser assigns local_directory without I/O for refs ending in /, and once any local ref resolves to a directory the engine treats it as fileKind: 'directory'. Whether a directory target is an error is decided entirely by the edge intent#126's "typed single-file slot" is exactly the edge whose copyRole/contract demands a single file (e.g. a packaging files: source entry); a navigational edge with no single-file contract treats a directory as a valid, existence-checked target. The two issues describe one model: local_directory / "typed single-file slot" ≡ fileKind: 'directory' / edge-scoped intent.

The matrix (spine of spec, docs, tests)

A single-source-of-truth rule catalog. CODE_REGISTRY.entry() holds {defaultSeverity, description, fix, reference} (no message field — runtime message is templated from description + per-issue context). The doc catalog's columns are asserted equal to the registry's description + fix + defaultSeverity; runtime message is treated as dynamic. Builds on the existing docs-completeness test (agent-skills/test/docs/validation-codes.test.ts), tightening it from anchor-presence to description/fix equality. The long "why / when fine / how to ignore" lives once behind reference.

Two views: (1) rule catalog — code | fires-when | means | fix-headline | severity | ships-now-vs-deferred; (2) disambiguation map — symptom (broken⇄orphan) × intent (build-artifact / runtime-asset / forgotten-doc / typo / stale / leaves-bundle) → the catalog row. View 2 names the broken⇄orphan oscillation and shows the files: edge (once deferredPaths is wired) as the resolving state.

Behavior

  • Orphan branches by fileKind: executable → must be linked (real signal); asset → declare in files: (we cannot infer runtime-loading without code parsing — declaration is the resolution); doc → link or remove. The uncatchable case (asset loaded by a script but neither linked nor declared) is documented as a known limit, not faked.
  • Broken-link headline = "copies into the bundle at build"; deferral detail lives behind reference.
  • Anti-workaround invariant: every error/warning fix names a sanctioned action before any "ignore/allow" — enforced by a registry meta-test.

Scenario test harness (DRY, exhaustive, alias-detecting)

A core unit harness drives the real verdict functions with constructed RuleContext (no filesystem):

  • Table of { intent, ctx: Partial<RuleContext>, expect }, one-line deltas, one it.each runner.
  • Per row asserts code === expect and description/fix/defaultSeverity === registry.
  • Aliasing detector: group by serialized RuleContext; if one signature maps to >1 distinct expect, fail and name both intents → resolve via compound message or a new discriminating context field.
  • Plus extraction integration tests proving extract(realDir) → RuleContext is faithful, including normalization parity (percent-encoding / case / leading-slash) between paths 1 and 2, which normalize differently today.

Engine unification

One orphan/link engine consumed by the built path and the live validateSkillForPackaging (not the rarely-run detectUnreferencedFiles); widen the **/*.md-only crawl (packaging-validator.ts:189) to .html/.htm + non-doc orphans; remove duplicated logic (duplication-check green, baseline untouched).


Deferred build artifacts (absorbed from #127)

Originally filed as #127: "vat skills validate: computeDeferredPaths() is exported but never consumed — links to files:-declared build artifacts fail with LINK_TO_GITIGNORED_FILE." This is #129's AC #3, stated here as one reconciled requirement. The codes below are the authoritative current behavior — the body above formerly said LINK_MISSING_TARGET for this case, which was stale.

Problem

When a skill bundles a build-generated artifact via the files: packaging config and links to it from SKILL.md, vat skills validate reports a hard error (LINK_TO_GITIGNORED_FILE, or LINK_BROKEN_FILE when no ignore rule matches the path) at source-validation time — even though the files: entry declares exactly that artifact and the build will materialize it before distribution.

The helper that exists to prevent this, computeDeferredPaths(), is implemented and exported but is consumed by nothing in the validate or build link-resolution paths. It is effectively dead code, so the "deferred path" concept it encodes never takes effect.

Environment: vibe-agent-toolkit 0.1.39-rc.1 (@vibe-agent-toolkit/cli, /agent-skills, /resources).

Repro (minimal, fictional)

A skill trail-conditions ships a generated lookup file produced by a sibling build step. The artifact is a build output (gitignored) and is copied into the skill at package time via files:.

vibe-agent-toolkit.config.yaml:

skills:
  include: ["skills/**/SKILL.md"]
  config:
    trail-conditions:
      files:
        - source: build/generated/trail-index.json   # emitted by `npm run gen`; gitignored build output
          dest: data/trail-index.json                # materialized into the skill at build time

skills/trail-conditions/SKILL.md:

See the [trail index](data/trail-index.json) for the station-to-segment map.

build/generated/ is listed in .gitignore, and data/trail-index.json does not exist in the source tree — it is only produced when vat skills build runs the files: copy.

Run:

vat skills validate

Actual

LINK_TO_GITIGNORED_FILE — Markdown link targets a gitignored file;
risks leaking ignored data into the bundle. (link: data/trail-index.json)

(If no ignore rule matches the resolved dest, the same link fails instead as LINK_BROKEN_FILE — File not found.)

The only way to make the skill green today is a per-path validation.allow entry for every such link — which defeats the purpose of declaring the artifact in files: and silences a check that should simply understand the artifact is deferred.

Expected

A link whose target is declared in the skill's files: config — matching either the dest or the build-artifact source — should be treated as a deferred path ("will exist after build") and skip the LINK_TO_GITIGNORED_FILE / LINK_BROKEN_FILE checks. This is exactly what computeDeferredPaths() was written to express.

Root cause

computeDeferredPaths(files) is defined and exported in @vibe-agent-toolkit/agent-skills (files-config.ts:134 / dist/files-config.js), and its own doc comment states it returns "the set of paths that should be treated as 'deferred' during source-time validation … source may be a build artifact that doesn't exist at validation time; dest is the target location that won't exist until build time."

But a workspace-wide search finds no call sites — it appears only at its definition and the index re-export (index.ts:38). walk-link-graph's deferredPaths option (:98, used :291) is likewise never supplied. The skills validate flow (@vibe-agent-toolkit/cli, commands/skills/validate.js) builds the validation context and runs the link validator (@vibe-agent-toolkit/resources, link-validator.js) without ever computing or passing deferred paths. So the validator resolves the target against the working tree, finds it absent/ignored, and emits LINK_TO_GITIGNORED_FILE / LINK_BROKEN_FILE.

Suggested fix

  1. In the skills validate flow, call computeDeferredPaths(mergedFilesConfig) per skill and thread the result into the link-validation options.
  2. In link-validator.js, when a resolved local target is in the deferred set, suppress LINK_TO_GITIGNORED_FILE and LINK_BROKEN_FILE for that link (optionally downgrade to an info/notice so it is still visible).
  3. Apply the same in vat skills build so build-time link rewriting and link validation agree.

Related, lower priority (separate consideration)

Plugin-local skills distributed via verbatim tree-copy (vat claude plugin build) currently have no files: surface of their own: plugin-level files[].dest rejects skills/… ("that surface is owned by the skill-stream"), and tree-copied skills never pass through packageSkill. So a skill that ships build-injected artifacts via the self-contained plugin layout has no first-class way to declare those artifacts for validation at all. Fixing (1)–(3) helps skills built through the packaging path; please also consider whether tree-copied plugin-local skills should be able to declare files: deferred paths so their injected artifacts can be validated and link-rewritten the same way.


Directory links (absorbed from #126)

Originally filed as #126: "Directory links should be valid targets; narrow LINK_TARGETS_DIRECTORY to typed single-file slots." A fully-specced, independently-shippable redesign of the directory-link rule, preserved in full below. Vocabulary is unified with this issue's model — see the Vocabulary note in Design above: #126's local_directoryRuleContext.fileKind: 'directory', and #126's "typed single-file slot" ≡ the edge whose copyRole/contract demands a single file.

Problem

VAT's link validation treats any local link that resolves to a directory as a hard error. In adoption this produces false positives for legitimate, GitHub-renderable links and pushes authors into awkward workarounds — repointing [docs/](docs/) to dodge the error, or writing [docs/](docs/README.md) to manually perform the index resolution GitHub does automatically.

This is wrong in principle: a directory is a legitimate link target. In a filesystem a directory is a real, existing referent (in Unix, literally a file); on GitHub a directory link renders as a navigable tree. "Docs live in docs/" in a ToC is a valid statement, and if docs/ is renamed/deleted the link should fail — but that's already caught by ordinary existence checking, not a directory rule.

The rule isn't wrong in spirit — it's mis-scoped. A directory is only a problem for a reference that is contractually a single file (a typed slot opened or copied as one file). It leaked into the general link path, which only checks that links resolve and has no business judging directory-ness.

Background — when & why this landed

Documented rationale (docs/validation-codes.md): "agents and renderers cannot load a directory as content." True only for a reference that is contractually a single file; false for a navigational reference.

Desired-state principle

A link resolving to a directory is a valid target. It is an error only for a reference that is contractually a single file (a typed slot) and resolves to a directory.

The discriminator is reference type, not which command runs:

Reference type Examples Directory target
Untyped / navigational Markdown [..](..), HTML href/src — prose links in docs, READMEs, SKILL.md bodies Valid. Existence-checked only; a resolved directory passes (no error, no warning), in every command (vat resources validate and the skill-bundling link walk).
Typed single-file slot A packaging files: source entry, a schema/template path, any config field declared to be exactly one file Resolving to a directory → LINK_TARGETS_DIRECTORY (error) — the contract demanded a file.

Corollaries:

  • Directory-ness is never itself the defect; the defect is a single-file contract receiving a directory.
  • The trailing slash (docs/ vs docs) is a diagnostic hint only — never a correctness gate. It informs classification/messages; it never changes pass/fail. Authors are not required to add a slash to make a directory link valid.
  • Directory determination is filesystem-only. External / server-resolved URLs are judged solely by HTTP status (or linkAuth), never by a directory rule — a folder-shaped URL (https://x.com/docs/) has no client-determinable meaning.
  • What ships in a bundle is a packaging-config concern, separate from link validation. A navigational link to assets/ is valid because assets/ exists; whether its contents are packaged is governed by packaging globs / files, not the prose link. Git doesn't track empty directories, so an "empty directory" doesn't exist in a clean checkout/bundle and is caught by the ordinary target-missing path.

Divergences to fix (one comprehensive change)

  • D1 — Wrong code (bug). packages/resources/src/link-validator.ts (~L249) emits LINK_BROKEN_FILE with message "Link target is a directory" — mislabeled (dedicated LINK_TARGETS_DIRECTORY exists) and reads to adopters like "the file doesn't exist."
  • D2 — Over-broad error (core). Untyped/navigational links must not error on a resolved directory at all. Remove the directory-error branch from the resources link-validator.
  • D3 — LINK_TARGETS_DIRECTORY over-applied. The skill-bundling link walk (walk-link-graphwalker-to-issues, where 'directory-target' → 'LINK_TARGETS_DIRECTORY' at walker-to-issues.ts:12) currently maps a directory-target prose link to LINK_TARGETS_DIRECTORY. These prose links are navigational — stop flagging them. The code must fire only for typed single-file slots (e.g. a packaging files: source entry) resolving to a directory.
  • D4 — Missing local_directory classification (enhancement). classifyLink (packages/resources/src/link-parser.ts) has no directory shape. Add local_directory for local refs ending in / (e.g. docs/), assigned without I/O, for clearer diagnostics and pre-stat intent. Not load-bearing: never decides pass/fail. docs (no slash, no extension) stays shape-ambiguous (local_file), resolved by stat, and once resolved to a directory is treated identically to docs/ (valid). (This is the source-level shape that becomes RuleContext.fileKind: 'directory' after stat — see the Vocabulary note above.)
  • D5 — HTML inheritance boundary (feat: first-class local HTML resources (#112) #116). HTML links route through the same validateLocalFileLink path. HTML must inherit the corrected rule and must not receive a filesystem-style directory error on href="dir/"; suggestion text must not leak filesystem assumptions into HTML diagnostics.
  • D6 — Web/external boundary. Make explicit (code + tests) that the directory rule is filesystem-only and never applied to external links.
  • D7 — Index resolution out of scope (decision record). GitHub-style docs/docs/README.md/index.html resolution is not implemented. Unnecessary once a directory is a valid target; documented to prevent re-litigation.

Design / rules

  1. Resources validator: delete the isDirectory → error branch (link-validator.ts:249). A local link resolving to an existing path — file or directory — passes. Missing target → existing broken-link code (unchanged). (D1, D2)
  2. Bundling link walk: stop emitting LINK_TARGETS_DIRECTORY for navigational directory links; a resolved directory is valid (existence-checked). (D3)
  3. LINK_TARGETS_DIRECTORY: retained as error, emitted only where a typed single-file reference (e.g. a packaging files: source entry) resolves to a directory. (D3)
  4. Parser: add local_directory to LinkType; classifyLink returns it for local refs whose path component ends in /. Messages/intent only; never gates validation. (D4)
  5. HTML: shares rules 1–4; diagnostics avoid filesystem-only phrasing. (D5)
  6. External: directory determination guarded to local refs only; asserted by test. (D6)
  7. Docs: update docs/validation-codes.md for LINK_TARGETS_DIRECTORY (narrowed typed-single-file-slot meaning; navigational links accept directories); record the index-resolution non-goal. (D7)

Affected code (survey — confirm at implementation)

  • packages/resources/src/link-validator.ts — remove directory-error branch (:249); HTML phrasing.
  • packages/resources/src/link-parser.tsclassifyLink + local_directory.
  • packages/resources/src/types.tsLinkType union.
  • packages/agent-schema/src/validation-codes.ts + docs/validation-codes.md — narrowed LINK_TARGETS_DIRECTORY description.
  • packages/agent-skills/src/validators/walker-to-issues.ts / walk-link-graph.ts — remove the navigational directory-target → LINK_TARGETS_DIRECTORY mapping (walker-to-issues.ts:12); confirm typed single-file slot (files:) validation is the only emitter.
  • Tests: packages/resources/test/integration/link-validator.integration.test.ts (the two existing directory tests assert LINK_BROKEN_FILE — update to assert valid), plus new tests below.

Testing

  • Resources validator: [docs/](docs/) and [docs](docs) → existing directory → valid (no issue). Renamed/deleted directory → broken-link (existing code).
  • classifyLink: docs/local_directory; docs, docs/x.md, https://x.com/docs/ → unchanged.
  • Bundling link walk: navigational directory link → valid.
  • Typed single-file slot: a files: source entry resolving to a directory → LINK_TARGETS_DIRECTORY (error).
  • HTML: href="dir/" → directory → valid; diagnostics free of filesystem-only phrasing.
  • External: a folder-shaped external URL never triggers a directory determination.

Rollout note (#126)

One comprehensive change. The D1 mislabel is the smallest standalone-shippable piece but is folded in. Keep this out of PR #116 so HTML doesn't ship the strict behavior before the rule is corrected; #116 should adopt the corrected rule. (PR #116 has since merged at HEAD 7f81588d; the corrected rule should land on top of it.)


Acceptance criteria

  1. Rules are pure verdict(RuleContext) functions; one shared engine; duplicated orphan/link logic removed; duplication-check green.
  2. Live vat audit/vat skills validate (configured) gain orphan + HTML-link detection at parity with build. (Markdown broken-link detection already exists via LINK_MISSING_TARGET.)
  3. deferredPaths wired into both the packager (skill-packager.ts) and the live validator (validateSkillForPackaging) so files:-declared artifacts stop firing LINK_TO_GITIGNORED_FILE / LINK_BROKEN_FILE. (Full requirement: see "Deferred build artifacts (absorbed from Slice 2 of #129: wire deferredPaths + close the files: gitignore-copy leak #127)", ACs 10a–10d below.)
  4. FileCopyRole stamped on each copy at the call site (coarse values); never on the shared SkillFileEntry/source; plugin artifacts exempt from skill expectations.
  5. Single-source-of-truth: doc rows == registry description/fix/defaultSeverity, asserted by test; runtime message treated as dynamic; lecture only behind reference.
  6. Scenario harness over constructed contexts; exhaustive matrix coverage; aliasing detector active; anti-workaround + doc-equality meta-invariants enforced.
  7. Extraction integration tests prove real dirs → correct RuleContext, including normalization parity.
  8. Orphan branches by fileKind; broken-link headline routes to files:-copies-into-bundle; known limits documented, not faked.
  9. No severity escalation AND no demotion without corpus evidence (docs/validation-rule-design.md); no new code shipped above info/warning without it; each catalog row marked ships-now vs deferred; only shipping codes registered.

Additional acceptance criteria — Deferred build artifacts (from #127)

10a. The skills validate flow calls computeDeferredPaths(mergedFilesConfig) per skill and threads the result into the link-validation options (so the helper stops being dead code).
10b. link-validator suppresses LINK_TO_GITIGNORED_FILE and LINK_BROKEN_FILE for any resolved local target in the deferred set (matching either the files: dest or the build-artifact source); optionally downgraded to an info/notice so it stays visible.
10c. vat skills build applies the same deferred-path handling so build-time link rewriting and link validation agree.
10d. Decision recorded on whether tree-copied plugin-local skills (vat claude plugin build, which today reject a skills/… files[].dest and bypass packageSkill) should be able to declare files: deferred paths so their injected artifacts can be validated/link-rewritten the same way. (Lower priority; in-scope as a tracked decision, not necessarily implemented in the first slice.)

Additional acceptance criteria — Directory links (from #126)

11a. Resources validator: the isDirectory → error branch (link-validator.ts:249) is removed; a navigational/untyped local link resolving to an existing directory passes with no issue in both vat resources validate and the skill-bundling link walk. (D1, D2)
11b. LINK_TARGETS_DIRECTORY is retained as error and fires only for a typed single-file slot (e.g. a packaging files: source entry) resolving to a directory; the navigational 'directory-target' → 'LINK_TARGETS_DIRECTORY' mapping in walker-to-issues.ts is removed. (D3)
11c. classifyLink gains local_directory (local refs ending in /), assigned without I/O, never gating pass/fail; docs stays local_file and, once resolved to a directory, is treated identically to docs/. (D4)
11d. HTML links inherit the corrected rule (no filesystem-style directory error on href="dir/"; no filesystem-only phrasing in diagnostics). (D5)
11e. Directory determination is guarded to local refs only and never applied to external links, asserted by test. (D6)
11f. docs/validation-codes.md updated for the narrowed LINK_TARGETS_DIRECTORY meaning and records the GitHub-style index-resolution non-goal. (D7)
11g. The two existing directory tests in link-validator.integration.test.ts (currently asserting LINK_BROKEN_FILE) are updated to assert valid, plus the new tests enumerated under Testing above.

Out of scope

  • Author-declared explicit roles (Phase 2).
  • Parsing script code to verify runtime asset loads.
  • Detecting externally-copied dist files (wiped on rebuild — documented limit).
  • GitHub-style directory index resolution (docs/docs/README.md/index.html) — Slice 1 of #129: directory links are valid targets (narrow LINK_TARGETS_DIRECTORY to typed single-file slots) #126 D7 decision record; unnecessary once a directory is a valid target.
  • Requiring/nudging trailing slashes (the slash is hint-only).
  • Any change to external-link / linkAuth status classification.
  • Bundle inclusion policy (what files ship) — governed by packaging config, unchanged here.

Notes

Open design questions (resolve before the engine slice / slice 3)

Surfaced by the multi-perspective adversarial review of this issue. The ships-now slices (#126, #127) are unaffected; these gate only the engine refactor:

  1. Extraction parity, not the verdict, is the hard part. The three paths iterate different domains (build = dist file inventory + link graph; live = **/*.md registry crawl with depth/exclude/gitignore filtering). "One engine over RuleContext" relocates that divergence into two path-specific extract() → RuleContext front-ends. Show both extract functions side-by-side and prove field-parity before calling it "one engine."
  2. RuleContext is lossy across paths. existsAtSource is meaningless post-build (dist is rm'd + recopied); copyRole has no edge on the live path (nothing is copied at validate time); referencedHow:'mention' on the build path matches dist-relative, post-rewrite paths the source path can't reproduce. The "normalization parity" risk is understated — the real divergence is file identity (source-relative vs dist-relative-rewritten), not just percent-encoding/case. For each field, state which path(s) populate it faithfully.
  3. The aliasing detector validates the table, not the model. Grouping hand-written Partial<RuleContext> rows catches table typos, not intent-collisions (two real intents with different fixes collapsing to one signature — the case this issue admits is "uncatchable"). Invert it: run over extract-produced contexts from a labeled-intent fixture corpus and fail on signature collision between intents with different fixes.
  4. Phase-1 FileCopyRolescope. 'skill-bundled' | 'plugin-artifact' is the same distinction as RuleContext.scope: 'skill' | 'plugin'. The closed-enum-on-the-edge machinery buys nothing in Phase 1 (it's justified only by Phase-2 roles, which are out-of-scope + evidence-gated). Consider deferring the enum entirely and using scope today. Separately, the load-bearing field fileKind (which the orphan-branch behavior rides on) is never specified — define its derivation and its evidence gate (AC feat: Implement publishing system with version management and wrapper script #9) before relying on "executable → must be linked."
  5. Orphan parity is a new error surface. Build's orphan code PACKAGED_UNREFERENCED_FILE is error; "parity" on the live path either inherits error (breaks currently-green adopter CI on upgrade) or emits a new info/warning code (build/live disagree on severity). Name the exact live orphan code + severity, and count findings against the internal adopters via VAT_ROOT_DIR before shipping.
  6. Spec hygiene for slice 3: pin "one engine consumed by both paths" with a real AC (AC feat(resources): Implement ResourceRegistry with link validation and dogfooding #1 only requires duplication-check green); decide path 3 (detectUnreferencedFiles)'s fate (delete/fold/keep); define the canonical normalization "parity" converges on (AC feat: Claude Skills audit and import tools #7); enumerate the ships-now vs deferred code lists in one place (AC feat: Implement publishing system with version management and wrapper script #9); reconcile AC feat: Implement publishing system with version management and wrapper script #9 (no demotion without evidence) with the Slice 1 of #129: directory links are valid targets (narrow LINK_TARGETS_DIRECTORY to typed single-file slots) #126 directory change and AC feat: Implement vat doctor diagnostic command #8's dependency on the deferred SKILL_UNLINKED_EXECUTABLE code.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requesthelp wantedExtra attention is needed

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions