@vibe-agent-toolkit/agent-skills 0.1.42 → 0.2.0-rc.10
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/README.md +5 -5
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.js +7 -17
- package/dist/builder.js.map +1 -1
- package/dist/content-type-routing.d.ts +32 -0
- package/dist/content-type-routing.d.ts.map +1 -1
- package/dist/content-type-routing.js +23 -0
- package/dist/content-type-routing.js.map +1 -1
- package/dist/files-config.d.ts +34 -1
- package/dist/files-config.d.ts.map +1 -1
- package/dist/files-config.js +277 -39
- package/dist/files-config.js.map +1 -1
- package/dist/fs-attribution.d.ts +45 -0
- package/dist/fs-attribution.d.ts.map +1 -0
- package/dist/fs-attribution.js +57 -0
- package/dist/fs-attribution.js.map +1 -0
- package/dist/import.d.ts.map +1 -1
- package/dist/import.js +0 -4
- package/dist/import.js.map +1 -1
- package/dist/index.d.ts +13 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +25 -7
- package/dist/index.js.map +1 -1
- package/dist/inventory/detectors/declared-but-missing.d.ts +1 -1
- package/dist/inventory/detectors/declared-but-missing.d.ts.map +1 -1
- package/dist/inventory/detectors/marketplace-source-missing.d.ts +1 -1
- package/dist/inventory/detectors/marketplace-source-missing.d.ts.map +1 -1
- package/dist/inventory/detectors/present-but-undeclared.d.ts +1 -1
- package/dist/inventory/detectors/present-but-undeclared.d.ts.map +1 -1
- package/dist/inventory/detectors/reference-target-missing.d.ts +1 -1
- package/dist/inventory/detectors/reference-target-missing.d.ts.map +1 -1
- package/dist/inventory/index.d.ts +1 -1
- package/dist/inventory/index.d.ts.map +1 -1
- package/dist/inventory/index.js +1 -1
- package/dist/inventory/index.js.map +1 -1
- package/dist/inventory/serialize.d.ts +4 -4
- package/dist/inventory/serialize.d.ts.map +1 -1
- package/dist/inventory/serialize.js +6 -6
- package/dist/inventory/serialize.js.map +1 -1
- package/dist/inventory/types.d.ts +8 -0
- package/dist/inventory/types.d.ts.map +1 -1
- package/dist/inventory/types.js.map +1 -1
- package/dist/link-graph-members.d.ts +18 -0
- package/dist/link-graph-members.d.ts.map +1 -0
- package/dist/link-graph-members.js +18 -0
- package/dist/link-graph-members.js.map +1 -0
- package/dist/parsers/frontmatter-parser.d.ts.map +1 -1
- package/dist/parsers/frontmatter-parser.js +30 -6
- package/dist/parsers/frontmatter-parser.js.map +1 -1
- package/dist/plugin-distribution-layout.d.ts +70 -6
- package/dist/plugin-distribution-layout.d.ts.map +1 -1
- package/dist/plugin-distribution-layout.js +84 -11
- package/dist/plugin-distribution-layout.js.map +1 -1
- package/dist/post-build-checks.d.ts +38 -1
- package/dist/post-build-checks.d.ts.map +1 -1
- package/dist/post-build-checks.js +68 -17
- package/dist/post-build-checks.js.map +1 -1
- package/dist/projection/skill-extent.d.ts +247 -0
- package/dist/projection/skill-extent.d.ts.map +1 -0
- package/dist/projection/skill-extent.js +560 -0
- package/dist/projection/skill-extent.js.map +1 -0
- package/dist/schemas/agent-skill-frontmatter.js +1 -1
- package/dist/schemas/agent-skill-frontmatter.js.map +1 -1
- package/dist/schemas/allowed-tools.d.ts +31 -0
- package/dist/schemas/allowed-tools.d.ts.map +1 -0
- package/dist/schemas/allowed-tools.js +88 -0
- package/dist/schemas/allowed-tools.js.map +1 -0
- package/dist/schemas/index.d.ts +1 -0
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/schemas/index.js +1 -0
- package/dist/schemas/index.js.map +1 -1
- package/dist/schemas/installed-plugins-registry.d.ts.map +1 -1
- package/dist/schemas/installed-plugins-registry.js +7 -1
- package/dist/schemas/installed-plugins-registry.js.map +1 -1
- package/dist/schemas/marketplace-manifest.d.ts.map +1 -1
- package/dist/schemas/marketplace-manifest.js +17 -6
- package/dist/schemas/marketplace-manifest.js.map +1 -1
- package/dist/skill-identity.d.ts +18 -3
- package/dist/skill-identity.d.ts.map +1 -1
- package/dist/skill-identity.js +25 -6
- package/dist/skill-identity.js.map +1 -1
- package/dist/skill-packager.d.ts +144 -20
- package/dist/skill-packager.d.ts.map +1 -1
- package/dist/skill-packager.js +447 -105
- package/dist/skill-packager.js.map +1 -1
- package/dist/skill-source/content-hash.d.ts.map +1 -1
- package/dist/skill-source/content-hash.js +21 -10
- package/dist/skill-source/content-hash.js.map +1 -1
- package/dist/skill-source/fetch-cache.d.ts.map +1 -1
- package/dist/skill-source/fetch-cache.js +0 -5
- package/dist/skill-source/fetch-cache.js.map +1 -1
- package/dist/skill-source/git-clone.d.ts +1 -1
- package/dist/skill-source/git-clone.d.ts.map +1 -1
- package/dist/skill-source/git-clone.js +10 -10
- package/dist/skill-source/git-clone.js.map +1 -1
- package/dist/skill-source/sources/url-source.d.ts.map +1 -1
- package/dist/skill-source/sources/url-source.js +2 -2
- package/dist/skill-source/sources/url-source.js.map +1 -1
- package/dist/skill-source/stage.d.ts.map +1 -1
- package/dist/skill-source/stage.js +0 -6
- package/dist/skill-source/stage.js.map +1 -1
- package/dist/skill-test/baseline-delta.d.ts +1575 -0
- package/dist/skill-test/baseline-delta.d.ts.map +1 -0
- package/dist/skill-test/baseline-delta.js +272 -0
- package/dist/skill-test/baseline-delta.js.map +1 -0
- package/dist/skill-test/baseline-integrity.d.ts +944 -0
- package/dist/skill-test/baseline-integrity.d.ts.map +1 -0
- package/dist/skill-test/baseline-integrity.js +3090 -0
- package/dist/skill-test/baseline-integrity.js.map +1 -0
- package/dist/skill-test/build-hook.d.ts +3 -2
- package/dist/skill-test/build-hook.d.ts.map +1 -1
- package/dist/skill-test/build-hook.js +4 -5
- package/dist/skill-test/build-hook.js.map +1 -1
- package/dist/skill-test/configure-writer.js +1 -1
- package/dist/skill-test/configure-writer.js.map +1 -1
- package/dist/skill-test/declared-env.d.ts +5 -4
- package/dist/skill-test/declared-env.d.ts.map +1 -1
- package/dist/skill-test/declared-env.js +8 -9
- package/dist/skill-test/declared-env.js.map +1 -1
- package/dist/skill-test/eval-executor.d.ts +21 -5
- package/dist/skill-test/eval-executor.d.ts.map +1 -1
- package/dist/skill-test/eval-executor.js +6 -6
- package/dist/skill-test/eval-executor.js.map +1 -1
- package/dist/skill-test/eval-fragment.d.ts +106 -28
- package/dist/skill-test/eval-fragment.d.ts.map +1 -1
- package/dist/skill-test/eval-fragment.js +78 -16
- package/dist/skill-test/eval-fragment.js.map +1 -1
- package/dist/skill-test/eval-grader.d.ts +65 -7
- package/dist/skill-test/eval-grader.d.ts.map +1 -1
- package/dist/skill-test/eval-grader.js +263 -64
- package/dist/skill-test/eval-grader.js.map +1 -1
- package/dist/skill-test/eval-inputs.d.ts +110 -8
- package/dist/skill-test/eval-inputs.d.ts.map +1 -1
- package/dist/skill-test/eval-inputs.js +177 -22
- package/dist/skill-test/eval-inputs.js.map +1 -1
- package/dist/skill-test/eval-lint.d.ts +17 -4
- package/dist/skill-test/eval-lint.d.ts.map +1 -1
- package/dist/skill-test/eval-lint.js +22 -9
- package/dist/skill-test/eval-lint.js.map +1 -1
- package/dist/skill-test/eval-suite-isolation.d.ts.map +1 -1
- package/dist/skill-test/eval-suite-isolation.js +8 -5
- package/dist/skill-test/eval-suite-isolation.js.map +1 -1
- package/dist/skill-test/evals-template.d.ts +1 -1
- package/dist/skill-test/evals-template.d.ts.map +1 -1
- package/dist/skill-test/evals-template.js +1 -3
- package/dist/skill-test/evals-template.js.map +1 -1
- package/dist/skill-test/executor-prompt.d.ts +15 -7
- package/dist/skill-test/executor-prompt.d.ts.map +1 -1
- package/dist/skill-test/executor-prompt.js +10 -7
- package/dist/skill-test/executor-prompt.js.map +1 -1
- package/dist/skill-test/failure-reason.d.ts +113 -0
- package/dist/skill-test/failure-reason.d.ts.map +1 -0
- package/dist/skill-test/failure-reason.js +124 -0
- package/dist/skill-test/failure-reason.js.map +1 -0
- package/dist/skill-test/fragment-merge.d.ts +3 -20
- package/dist/skill-test/fragment-merge.d.ts.map +1 -1
- package/dist/skill-test/fragment-merge.js +67 -8
- package/dist/skill-test/fragment-merge.js.map +1 -1
- package/dist/skill-test/grader-prompt.d.ts +29 -9
- package/dist/skill-test/grader-prompt.d.ts.map +1 -1
- package/dist/skill-test/grader-prompt.js +264 -68
- package/dist/skill-test/grader-prompt.js.map +1 -1
- package/dist/skill-test/grader-text.d.ts +145 -0
- package/dist/skill-test/grader-text.d.ts.map +1 -0
- package/dist/skill-test/grader-text.js +410 -0
- package/dist/skill-test/grader-text.js.map +1 -0
- package/dist/skill-test/grading-adapter.d.ts +38 -2
- package/dist/skill-test/grading-adapter.d.ts.map +1 -1
- package/dist/skill-test/grading-adapter.js +57 -9
- package/dist/skill-test/grading-adapter.js.map +1 -1
- package/dist/skill-test/grading-schema.d.ts +160 -5
- package/dist/skill-test/grading-schema.d.ts.map +1 -1
- package/dist/skill-test/grading-schema.js +66 -7
- package/dist/skill-test/grading-schema.js.map +1 -1
- package/dist/skill-test/harness-location.d.ts +3 -2
- package/dist/skill-test/harness-location.d.ts.map +1 -1
- package/dist/skill-test/harness-location.js +5 -16
- package/dist/skill-test/harness-location.js.map +1 -1
- package/dist/skill-test/index.d.ts +2 -2
- package/dist/skill-test/index.d.ts.map +1 -1
- package/dist/skill-test/index.js +2 -2
- package/dist/skill-test/index.js.map +1 -1
- package/dist/skill-test/lock.d.ts +58 -1
- package/dist/skill-test/lock.d.ts.map +1 -1
- package/dist/skill-test/lock.js +78 -6
- package/dist/skill-test/lock.js.map +1 -1
- package/dist/skill-test/pipeline.d.ts +19 -1
- package/dist/skill-test/pipeline.d.ts.map +1 -1
- package/dist/skill-test/pipeline.js +21 -10
- package/dist/skill-test/pipeline.js.map +1 -1
- package/dist/skill-test/preflight.d.ts +1 -1
- package/dist/skill-test/preflight.d.ts.map +1 -1
- package/dist/skill-test/preflight.js +59 -16
- package/dist/skill-test/preflight.js.map +1 -1
- package/dist/skill-test/prompt-invariants.d.ts +4 -2
- package/dist/skill-test/prompt-invariants.d.ts.map +1 -1
- package/dist/skill-test/prompt-invariants.js +5 -4
- package/dist/skill-test/prompt-invariants.js.map +1 -1
- package/dist/skill-test/run-harness.d.ts +665 -85
- package/dist/skill-test/run-harness.d.ts.map +1 -1
- package/dist/skill-test/run-harness.js +1522 -237
- package/dist/skill-test/run-harness.js.map +1 -1
- package/dist/skill-test/staging.d.ts +20 -8
- package/dist/skill-test/staging.d.ts.map +1 -1
- package/dist/skill-test/staging.js +25 -18
- package/dist/skill-test/staging.js.map +1 -1
- package/dist/skill-test/tier-plan.d.ts +1 -1
- package/dist/skill-test/tier-plan.js +1 -1
- package/dist/skill-test/tool-eval-schema.d.ts +6 -6
- package/dist/skill-test/tool-eval-schema.js +2 -2
- package/dist/skill-test/vendor-manifest.d.ts +2 -1
- package/dist/skill-test/vendor-manifest.d.ts.map +1 -1
- package/dist/skill-test/vendor-manifest.js +10 -10
- package/dist/skill-test/vendor-manifest.js.map +1 -1
- package/dist/test-input.d.ts +59 -8
- package/dist/test-input.d.ts.map +1 -1
- package/dist/test-input.js +64 -19
- package/dist/test-input.js.map +1 -1
- package/dist/validators/agent-instruction-presence.d.ts +1 -1
- package/dist/validators/agent-instruction-presence.d.ts.map +1 -1
- package/dist/validators/agent-instruction-presence.js +45 -2
- package/dist/validators/agent-instruction-presence.js.map +1 -1
- package/dist/validators/bundled-resource-link-detection.d.ts +6 -4
- package/dist/validators/bundled-resource-link-detection.d.ts.map +1 -1
- package/dist/validators/bundled-resource-link-detection.js +39 -39
- package/dist/validators/bundled-resource-link-detection.js.map +1 -1
- package/dist/validators/compat-detectors.d.ts +18 -1
- package/dist/validators/compat-detectors.d.ts.map +1 -1
- package/dist/validators/compat-detectors.js +47 -20
- package/dist/validators/compat-detectors.js.map +1 -1
- package/dist/validators/cross-skill-dependency-detection.d.ts +1 -1
- package/dist/validators/cross-skill-dependency-detection.d.ts.map +1 -1
- package/dist/validators/cross-skill-dependency-detection.js +1 -1
- package/dist/validators/cross-skill-dependency-detection.js.map +1 -1
- package/dist/validators/description-style-detection.d.ts +1 -1
- package/dist/validators/description-style-detection.d.ts.map +1 -1
- package/dist/validators/description-style-detection.js +1 -1
- package/dist/validators/description-style-detection.js.map +1 -1
- package/dist/validators/format-detection.d.ts +3 -0
- package/dist/validators/format-detection.d.ts.map +1 -1
- package/dist/validators/format-detection.js +41 -26
- package/dist/validators/format-detection.js.map +1 -1
- package/dist/validators/frontmatter-validation.d.ts +1 -1
- package/dist/validators/frontmatter-validation.d.ts.map +1 -1
- package/dist/validators/frontmatter-validation.js +2 -2
- package/dist/validators/frontmatter-validation.js.map +1 -1
- package/dist/validators/imperative-body-detection.d.ts +1 -1
- package/dist/validators/imperative-body-detection.d.ts.map +1 -1
- package/dist/validators/imperative-body-detection.js +1 -1
- package/dist/validators/imperative-body-detection.js.map +1 -1
- package/dist/validators/index.d.ts +1 -1
- package/dist/validators/index.d.ts.map +1 -1
- package/dist/validators/kebab-case-detection.d.ts +1 -1
- package/dist/validators/kebab-case-detection.d.ts.map +1 -1
- package/dist/validators/kebab-case-detection.js +1 -1
- package/dist/validators/kebab-case-detection.js.map +1 -1
- package/dist/validators/marketplace-validator.d.ts.map +1 -1
- package/dist/validators/marketplace-validator.js +7 -2
- package/dist/validators/marketplace-validator.js.map +1 -1
- package/dist/validators/mcp-tool-qualification.d.ts +158 -0
- package/dist/validators/mcp-tool-qualification.d.ts.map +1 -0
- package/dist/validators/mcp-tool-qualification.js +467 -0
- package/dist/validators/mcp-tool-qualification.js.map +1 -0
- package/dist/validators/packaged-size-limit.d.ts +296 -0
- package/dist/validators/packaged-size-limit.d.ts.map +1 -0
- package/dist/validators/packaged-size-limit.js +492 -0
- package/dist/validators/packaged-size-limit.js.map +1 -0
- package/dist/validators/packaging-validator.d.ts +149 -10
- package/dist/validators/packaging-validator.d.ts.map +1 -1
- package/dist/validators/packaging-validator.js +371 -55
- package/dist/validators/packaging-validator.js.map +1 -1
- package/dist/validators/plugin-hosted-shape.d.ts +4 -2
- package/dist/validators/plugin-hosted-shape.d.ts.map +1 -1
- package/dist/validators/plugin-hosted-shape.js +6 -7
- package/dist/validators/plugin-hosted-shape.js.map +1 -1
- package/dist/validators/plugin-recommended-fields.d.ts +1 -1
- package/dist/validators/plugin-recommended-fields.d.ts.map +1 -1
- package/dist/validators/plugin-recommended-fields.js +1 -1
- package/dist/validators/plugin-recommended-fields.js.map +1 -1
- package/dist/validators/referenced-path-missing.d.ts +211 -0
- package/dist/validators/referenced-path-missing.d.ts.map +1 -0
- package/dist/validators/referenced-path-missing.js +378 -0
- package/dist/validators/referenced-path-missing.js.map +1 -0
- package/dist/validators/registry-validator.d.ts.map +1 -1
- package/dist/validators/registry-validator.js +1 -2
- package/dist/validators/registry-validator.js.map +1 -1
- package/dist/validators/rule-engine/index.d.ts +1 -1
- package/dist/validators/rule-engine/rule-context.d.ts +20 -3
- package/dist/validators/rule-engine/rule-context.d.ts.map +1 -1
- package/dist/validators/rule-engine/rule-context.js +3 -1
- package/dist/validators/rule-engine/rule-context.js.map +1 -1
- package/dist/validators/rule-engine/rule-engine.d.ts +2 -2
- package/dist/validators/rule-engine/rule-engine.d.ts.map +1 -1
- package/dist/validators/rule-engine/rule-engine.js +52 -13
- package/dist/validators/rule-engine/rule-engine.js.map +1 -1
- package/dist/validators/skill-validator.d.ts.map +1 -1
- package/dist/validators/skill-validator.js +91 -54
- package/dist/validators/skill-validator.js.map +1 -1
- package/dist/validators/source-only-codes.js +1 -1
- package/dist/validators/source-only-codes.js.map +1 -1
- package/dist/validators/types.d.ts +21 -1
- package/dist/validators/types.d.ts.map +1 -1
- package/dist/validators/unified-validator.d.ts.map +1 -1
- package/dist/validators/unified-validator.js +4 -5
- package/dist/validators/unified-validator.js.map +1 -1
- package/dist/validators/validation-rules.d.ts +4 -4
- package/dist/validators/validation-rules.d.ts.map +1 -1
- package/dist/validators/validation-rules.js +9 -38
- package/dist/validators/validation-rules.js.map +1 -1
- package/dist/validators/walker-to-issues.d.ts +9 -3
- package/dist/validators/walker-to-issues.d.ts.map +1 -1
- package/dist/validators/walker-to-issues.js +64 -19
- package/dist/validators/walker-to-issues.js.map +1 -1
- package/dist/walk-link-graph.d.ts +64 -5
- package/dist/walk-link-graph.d.ts.map +1 -1
- package/dist/walk-link-graph.js +376 -92
- package/dist/walk-link-graph.js.map +1 -1
- package/package.json +14 -22
- package/dist/skill-test/exit-codes.d.ts +0 -97
- package/dist/skill-test/exit-codes.d.ts.map +0 -1
- package/dist/skill-test/exit-codes.js +0 -141
- package/dist/skill-test/exit-codes.js.map +0 -1
package/dist/walk-link-graph.js
CHANGED
|
@@ -8,11 +8,34 @@
|
|
|
8
8
|
* Non-markdown assets (images, JSON, etc.) won't be in the registry and still
|
|
9
9
|
* require `existsSync()` checks — this is acceptable since the goal is eliminating
|
|
10
10
|
* redundant I/O for markdown files that are already parsed.
|
|
11
|
+
*
|
|
12
|
+
* ## Membership is not traversability
|
|
13
|
+
*
|
|
14
|
+
* A registry MEMBER is a file VAT has parsed: its links are known, so the
|
|
15
|
+
* packager's rewriter can reach inside it and repoint them at bundled copies. A
|
|
16
|
+
* ROUTABLE file is one the walker walks THROUGH — it enqueues that file's link
|
|
17
|
+
* targets as further bundle members. Routing is markdown-only
|
|
18
|
+
* ({@link isRoutable}); HTML is "a leaf we can read, not a door we walk
|
|
19
|
+
* through".
|
|
20
|
+
*
|
|
21
|
+
* These used to be the same thing, by accident: any link target that
|
|
22
|
+
* `getResourceById` found was queued, so a registry's include globs silently
|
|
23
|
+
* WERE the routability policy. The two production registries disagreed as a
|
|
24
|
+
* result. `crawlAndResolveRegistry` includes HTML, so `vat audit` and
|
|
25
|
+
* post-build validation walked THROUGH a bundled page and counted its targets
|
|
26
|
+
* as bundled; the packager's `createProjectRegistry` is markdown-only, so at
|
|
27
|
+
* walk time the same page was an opaque asset and its targets were dropped.
|
|
28
|
+
* Same tree, two answers to "is HTML a door" — and audit's answer described a
|
|
29
|
+
* bundle the build never produced.
|
|
30
|
+
*
|
|
31
|
+
* Links out of a non-routable member are not silently dropped: each one whose
|
|
32
|
+
* target the walk never bundled by some other route is reported as a
|
|
33
|
+
* `non-routable-source` exclusion (`LINK_FROM_NON_ROUTABLE_FILE`).
|
|
11
34
|
*/
|
|
12
|
-
import { existsSync, statSync } from 'node:fs';
|
|
13
35
|
import { basename, dirname } from 'node:path';
|
|
14
|
-
import { isLocalFileLink, resolveLocalHref } from '@vibe-agent-toolkit/resources';
|
|
15
|
-
import {
|
|
36
|
+
import { isLocalFileLink, parserKindForPath, resolveLocalHref, } from '@vibe-agent-toolkit/resources';
|
|
37
|
+
import { CRAWL_PASS_INSIDE, CRAWL_WALKER_GITIGNORE_ID, CRAWL_WALKER_ID, crawlTimingStart, FsLookupCache, recordCrawlPass, relativeEscapesRoot, toForwardSlash, safePath, } from '@vibe-agent-toolkit/utils';
|
|
38
|
+
import { isGitIgnored } from '@vibe-agent-toolkit/utils/git';
|
|
16
39
|
import picomatch from 'picomatch';
|
|
17
40
|
import { isAgentInstructionBasename, isNavigationBasename } from './validators/validation-rules.js';
|
|
18
41
|
// ============================================================================
|
|
@@ -48,36 +71,55 @@ function resolveHrefToPath(hrefWithoutAnchor, sourceFilePath, projectRoot) {
|
|
|
48
71
|
}
|
|
49
72
|
/** Check if a link targets a file outside the project boundary */
|
|
50
73
|
function isOutsideProject(targetPath, projectRoot) {
|
|
51
|
-
return safePath.relative(projectRoot, targetPath)
|
|
74
|
+
return relativeEscapesRoot(safePath.relative(projectRoot, targetPath));
|
|
52
75
|
}
|
|
53
76
|
// Navigation / agent-instruction classification is the shared, case-insensitive
|
|
54
77
|
// matcher from validation-rules (`isNavigationBasename` / `isAgentInstructionBasename`),
|
|
55
78
|
// used directly at the call sites below. On APFS/NTFS a `Claude.md` is loaded as
|
|
56
79
|
// instructions exactly as a `CLAUDE.md` is, so the walker must refuse both spellings.
|
|
57
|
-
|
|
58
|
-
function makeExclusion(targetPath, sourcePath, targetExists, reason, link, matchedRule) {
|
|
80
|
+
function makeFollowedLink(targetPath, sourcePath, targetExists, link) {
|
|
59
81
|
return {
|
|
60
82
|
path: targetPath,
|
|
61
83
|
sourcePath,
|
|
62
84
|
...(link.line !== undefined && { sourceLine: link.line }),
|
|
63
85
|
targetExists,
|
|
86
|
+
bundled: true,
|
|
87
|
+
linkText: link.text,
|
|
88
|
+
linkHref: link.href,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/** Create an exclusion record */
|
|
92
|
+
function makeExclusion(targetPath, sourcePath, targetExists, reason, link, matchedRule) {
|
|
93
|
+
return {
|
|
94
|
+
...makeFollowedLink(targetPath, sourcePath, targetExists, link),
|
|
64
95
|
bundled: false,
|
|
65
96
|
excludeReason: reason,
|
|
66
97
|
...(matchedRule ? { matchedRule } : {}),
|
|
67
|
-
linkText: link.text,
|
|
68
|
-
linkHref: link.href,
|
|
69
98
|
};
|
|
70
99
|
}
|
|
71
100
|
/**
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
101
|
+
* Build the pass-1′ row for one link target.
|
|
102
|
+
*
|
|
103
|
+
* The two syscalls come from {@link FsLookupCache.probe}, which memoizes them
|
|
104
|
+
* across links, so repeated calls for the same path are free of I/O.
|
|
105
|
+
*/
|
|
106
|
+
function fillPathFacts(targetPath, options, state) {
|
|
107
|
+
const { exists, isDirectory } = state.pathProbe.probe(targetPath);
|
|
108
|
+
return {
|
|
109
|
+
exists,
|
|
110
|
+
isDirectory,
|
|
111
|
+
insideProject: !isOutsideProject(targetPath, options.projectRoot),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Check if a target path is a not-yet-materialized deferred build artifact.
|
|
116
|
+
* Must run BEFORE any directory check to avoid classifying a missing path.
|
|
75
117
|
*
|
|
76
118
|
* This check is gated on the target NOT existing on disk: it classifies the
|
|
77
119
|
* "hasn't been built yet" half of the deferred-artifact lifecycle. An
|
|
78
120
|
* existing real file at a covered files: dest (or source) is NOT deferred by
|
|
79
121
|
* THIS check — it falls through to the directory-target check and beyond.
|
|
80
|
-
* The gitignore branch further down
|
|
122
|
+
* The gitignore branch further down ({@link classifyGitignored}) carries its
|
|
81
123
|
* OWN, unconditional `deferredArtifacts` exemption (existing or not) for the
|
|
82
124
|
* "already built, and gitignored as expected" half — see the comment there.
|
|
83
125
|
* So an existing-but-gitignored covered path still ends up classified as
|
|
@@ -85,17 +127,12 @@ function makeExclusion(targetPath, sourcePath, targetExists, reason, link, match
|
|
|
85
127
|
* existing-and-NOT-gitignored covered path (or an uncovered path) reaches
|
|
86
128
|
* normal handling. {@link DeferredArtifacts.covers} does the exact-OR-
|
|
87
129
|
* directory-prefix membership test (pure, no filesystem access); the
|
|
88
|
-
* existence answer
|
|
89
|
-
* for the whole classifier.
|
|
130
|
+
* existence answer comes from the target's pass-1′ row.
|
|
90
131
|
*
|
|
91
132
|
* @returns true if the path was classified as deferred
|
|
92
133
|
*/
|
|
93
|
-
function checkDeferred(targetPath, targetExists, deferredArtifacts
|
|
94
|
-
|
|
95
|
-
deferredAssetSet.add(toForwardSlash(targetPath));
|
|
96
|
-
return true;
|
|
97
|
-
}
|
|
98
|
-
return false;
|
|
134
|
+
function checkDeferred(targetPath, targetExists, deferredArtifacts) {
|
|
135
|
+
return deferredArtifacts.covers(targetPath) && !targetExists;
|
|
99
136
|
}
|
|
100
137
|
/**
|
|
101
138
|
* Does the walker refuse to bundle this target because it is a repo-internal
|
|
@@ -141,8 +178,8 @@ function refusesAgentInstructionFile(targetPath, options) {
|
|
|
141
178
|
return !declaredSources.has(toForwardSlash(safePath.relative(options.projectRoot, targetPath)));
|
|
142
179
|
}
|
|
143
180
|
/**
|
|
144
|
-
*
|
|
145
|
-
* artifact — the
|
|
181
|
+
* Classify a target already known to be gitignored as either a leak or an
|
|
182
|
+
* exempted `files:` build artifact — the walk excludes it either way; only
|
|
146
183
|
* the record differs.
|
|
147
184
|
*
|
|
148
185
|
* A files:-declared DEST is exempt from the leak rule EVEN WHEN it already
|
|
@@ -161,18 +198,13 @@ function refusesAgentInstructionFile(targetPath, options) {
|
|
|
161
198
|
* coverage must NOT exempt it. An uncovered path, or a path covered only via
|
|
162
199
|
* `sourcePaths`, still reports the leak normally.
|
|
163
200
|
*/
|
|
164
|
-
function
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
169
|
-
// `targetExists: true` is not an assumption here — the caller only reaches
|
|
170
|
-
// this function for a target it has already stat'ed and found present.
|
|
171
|
-
excludedReferences.push(makeExclusion(targetPath, sourcePath, true, 'gitignored', link));
|
|
201
|
+
function classifyGitignoredTarget(targetPath, deferredArtifacts) {
|
|
202
|
+
return deferredArtifacts?.coversDest(targetPath)
|
|
203
|
+
? { kind: 'deferred' }
|
|
204
|
+
: { kind: 'excluded', reason: 'gitignored' };
|
|
172
205
|
}
|
|
173
206
|
/**
|
|
174
|
-
* The
|
|
175
|
-
* classifier within its cognitive-complexity budget.
|
|
207
|
+
* The `files:`-dest exemption for a target already known to be gitignored.
|
|
176
208
|
*
|
|
177
209
|
* GATED ON EXISTENCE, and the gate is the point. A path that is not there
|
|
178
210
|
* cannot leak anything, and neither ignore oracle can be trusted to say so:
|
|
@@ -187,69 +219,125 @@ function recordGitignoredTarget(targetPath, sourcePath, link, deferredArtifacts,
|
|
|
187
219
|
* Prefer the pre-populated GitTracker when the caller plumbed one through: it
|
|
188
220
|
* answers in O(1) against the active set, no `git check-ignore` spawn.
|
|
189
221
|
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
222
|
+
* **Memoized per walk**, so a target named by many documents costs one answer
|
|
223
|
+
* rather than one per reference. The memo cannot change the answer: both
|
|
224
|
+
* oracles are pure functions of the tree, and the memo's lifetime is one walk.
|
|
225
|
+
*
|
|
226
|
+
* ⛔ **Measured on VAT's own tree, and the result is a NEGATIVE —
|
|
227
|
+
* do not sell this as a performance win.** Counting asks vs. memo misses in the
|
|
228
|
+
* built lane: `vat audit .` = 3 asks / 2 distinct, `vat skills build` = 0 / 0,
|
|
229
|
+
* `vat inventory .` = 0 / 0. **One** oracle call avoided across all three.
|
|
230
|
+
*
|
|
231
|
+
* The reason is structural, not incidental: this branch is LAST in the cascade,
|
|
232
|
+
* so a target only reaches it after surviving all seven earlier discriminators
|
|
233
|
+
* *and* existing on disk. Nearly every link target is classified before then —
|
|
234
|
+
* bundled markdown, a directory, a navigation file, a pattern match. The memo's
|
|
235
|
+
* real justification is therefore **shape, not speed**: it is what lets the
|
|
236
|
+
* oracle read sit outside {@link classifyExclusion} so that function can be a
|
|
237
|
+
* pure predicate over the row. Any future claim of a spawn-count win needs its
|
|
238
|
+
* own measurement on a corpus where this branch is actually hot.
|
|
239
|
+
*
|
|
240
|
+
* Two further bounds, so nobody reads more into it than it does:
|
|
241
|
+
*
|
|
242
|
+
* - **The collapse is within-ONE-walk fan-in only.** `collectLinkedFiles` calls
|
|
243
|
+
* {@link walkLinkGraph} once per skill, so a target shared across skills is
|
|
244
|
+
* still asked about once per skill. That per-walk lifetime is exactly what
|
|
245
|
+
* makes the memo safe against `packageSkill` writing `dist/` between skills,
|
|
246
|
+
* so it is a deliberate ceiling, not a missed optimization.
|
|
247
|
+
* - **Not every repeat was a subprocess.** {@link isGitIgnored} settles "is
|
|
248
|
+
* there a repository here?" from the filesystem and returns early when there
|
|
249
|
+
* is none, so on a tree with no `.git` ancestor the repeats were ancestor
|
|
250
|
+
* walks rather than `git check-ignore` spawns. Inside a real repository they
|
|
251
|
+
* were spawns.
|
|
252
|
+
*
|
|
253
|
+
* @returns whether the target is gitignored
|
|
192
254
|
*/
|
|
193
|
-
function
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
255
|
+
function readGitignored(targetPath, options, state) {
|
|
256
|
+
const cached = state.gitignoreFacts.get(targetPath);
|
|
257
|
+
if (cached !== undefined)
|
|
258
|
+
return cached;
|
|
259
|
+
// Charged on the MISS path only, so `calls` counts oracle READS rather than
|
|
260
|
+
// asks — a memo hit costs nothing worth a bracket, and counting it would make
|
|
261
|
+
// the row's ms/call meaningless. This is the branch that can spawn
|
|
262
|
+
// `git check-ignore`, which is why it is separated from the walk's own row at
|
|
263
|
+
// all: the walk's cost and the oracle's cost move for different reasons.
|
|
264
|
+
const startedAt = crawlTimingStart();
|
|
197
265
|
const isIgnored = options.gitTracker === undefined
|
|
198
266
|
? isGitIgnored(targetPath, options.projectRoot)
|
|
199
267
|
: options.gitTracker.isIgnoredByActiveSet(targetPath);
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
recordGitignoredTarget(targetPath, sourcePath, link, options.deferredArtifacts, excludedReferences, deferredAssetSet);
|
|
204
|
-
return true;
|
|
268
|
+
recordCrawlPass(CRAWL_WALKER_GITIGNORE_ID, 'crawl', CRAWL_PASS_INSIDE, startedAt);
|
|
269
|
+
state.gitignoreFacts.set(targetPath, isIgnored);
|
|
270
|
+
return isIgnored;
|
|
205
271
|
}
|
|
206
272
|
/**
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
* pattern match).
|
|
273
|
+
* The `exists` × `isDirectory` corner of the cascade — the only place the row's
|
|
274
|
+
* three-state `isDirectory` is read.
|
|
210
275
|
*
|
|
211
|
-
*
|
|
276
|
+
* `null` means *no answer about the kind*: the path is present to `existsSync`
|
|
277
|
+
* and unstattable anyway (a permissions problem, or a change racing the two
|
|
278
|
+
* calls). The classifier can say nothing about WHAT the target is — but it can
|
|
279
|
+
* say that the target is there and could not be read, and that is the report.
|
|
280
|
+
*
|
|
281
|
+
* ✅ **This used to skip silently, and that was D6 in the spec's deferred-fixes
|
|
282
|
+
* ledger — now fixed.** `{ kind: 'skipped' }` recorded nothing at all, so an
|
|
283
|
+
* unreadable link target vanished from the report entirely: no exclusion, no
|
|
284
|
+
* bundling, no issue. It survived the pass-1′ refactor only because that
|
|
285
|
+
* refactor was held to byte-identical output (it is what the pre-refactor
|
|
286
|
+
* `catch { return true }` did). Meanwhile the resources lane turned the
|
|
287
|
+
* IDENTICAL class of read failure into a `RESOURCE_UNREADABLE` finding, so one
|
|
288
|
+
* unreadable file got two answers depending on which lane found it. It is now
|
|
289
|
+
* an `unreadable-target` exclusion, which the verdict engine reports as
|
|
290
|
+
* `LINK_TARGET_UNREADABLE` — the resources code's skill-packaging sibling.
|
|
291
|
+
*
|
|
292
|
+
* @returns a verdict when the path's kind decides the outcome, else `undefined`
|
|
212
293
|
*/
|
|
213
|
-
function
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
294
|
+
function classifyPathKind(facts) {
|
|
295
|
+
if (!facts.exists)
|
|
296
|
+
return undefined;
|
|
297
|
+
if (facts.isDirectory === true)
|
|
298
|
+
return { kind: 'excluded', reason: 'directory-target' };
|
|
299
|
+
if (facts.isDirectory === null)
|
|
300
|
+
return { kind: 'excluded', reason: 'unreadable-target' };
|
|
301
|
+
return undefined;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* The first-match-wins exclusion cascade, as a **pure function of the pass-1′
|
|
305
|
+
* row**. No filesystem access, no walk-state mutation, no recording.
|
|
306
|
+
*
|
|
307
|
+
* ⚠️ **The order IS the behaviour.** This is a cascade, not a set of
|
|
308
|
+
* independent predicates: a directory that is also pattern-matched is reported
|
|
309
|
+
* as `directory-target`, and rewriting it as `reasons.find(...)` over
|
|
310
|
+
* independently-evaluated conditions would silently repick which reason wins.
|
|
311
|
+
* Every branch below sits exactly where it sat when the classifier did its own
|
|
312
|
+
* I/O inline; only the source of the facts changed.
|
|
313
|
+
*
|
|
314
|
+
* The one branch that is not a column is the last one — see
|
|
315
|
+
* {@link readGitignored} for why the gitignore oracle is demand-read at the
|
|
316
|
+
* point the cascade reaches it rather than filled for every row.
|
|
317
|
+
*
|
|
318
|
+
* @returns the verdict, or `undefined` when nothing excludes the target
|
|
319
|
+
*/
|
|
320
|
+
function classifyExclusion(targetPath, options, excludeMatchers, facts) {
|
|
219
321
|
// The deferred check is the FIRST discriminator: a not-yet-materialized target
|
|
220
322
|
// must be classified before any check that would read it off disk.
|
|
221
|
-
if (options.deferredArtifacts && checkDeferred(targetPath,
|
|
222
|
-
return
|
|
323
|
+
if (options.deferredArtifacts && checkDeferred(targetPath, facts.exists, options.deferredArtifacts)) {
|
|
324
|
+
return { kind: 'deferred' };
|
|
223
325
|
}
|
|
224
326
|
// Check if target is a directory
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
if (statSync(targetPath).isDirectory()) {
|
|
229
|
-
excludedReferences.push(makeExclusion(targetPath, sourcePath, targetExists, 'directory-target', link));
|
|
230
|
-
return true;
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
catch {
|
|
234
|
-
// statSync failure = skip
|
|
235
|
-
return true;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
327
|
+
const kindVerdict = classifyPathKind(facts);
|
|
328
|
+
if (kindVerdict)
|
|
329
|
+
return kindVerdict;
|
|
238
330
|
// Check project boundary
|
|
239
|
-
if (
|
|
240
|
-
|
|
241
|
-
return true;
|
|
242
|
-
}
|
|
331
|
+
if (!facts.insideProject)
|
|
332
|
+
return { kind: 'excluded', reason: 'outside-project' };
|
|
243
333
|
// Check navigation file exclusion
|
|
244
334
|
if (options.excludeNavigationFiles && isNavigationBasename(basename(targetPath))) {
|
|
245
|
-
|
|
246
|
-
return true;
|
|
335
|
+
return { kind: 'excluded', reason: 'navigation-file' };
|
|
247
336
|
}
|
|
248
337
|
// Repo-internal agent-instruction files — see {@link refusesAgentInstructionFile}
|
|
249
338
|
// for why they cannot travel, and for the explicit-`files:` escape hatch.
|
|
250
339
|
if (refusesAgentInstructionFile(targetPath, options)) {
|
|
251
|
-
|
|
252
|
-
return true;
|
|
340
|
+
return { kind: 'excluded', reason: 'agent-instruction-file' };
|
|
253
341
|
}
|
|
254
342
|
// Check for cross-skill SKILL.md links — a SKILL.md is a skill definition marker,
|
|
255
343
|
// not a resource. Bundling another skill's SKILL.md creates duplicate skill definitions
|
|
@@ -260,21 +348,78 @@ function checkExclusions(targetPath, sourcePath, link, options, excludeMatchers,
|
|
|
260
348
|
// prevents re-traversal, and the duplicate-definition risk does not apply to the
|
|
261
349
|
// current skill itself.
|
|
262
350
|
if (basename(targetPath) === 'SKILL.md') {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
return true;
|
|
351
|
+
return safePath.resolve(targetPath) === safePath.resolve(options.skillRootPath)
|
|
352
|
+
? { kind: 'skipped' }
|
|
353
|
+
: { kind: 'excluded', reason: 'skill-definition' };
|
|
267
354
|
}
|
|
268
355
|
// Check exclude patterns (relative to projectRoot)
|
|
269
356
|
const relativePath = toForwardSlash(safePath.relative(options.projectRoot, targetPath));
|
|
270
357
|
const matchedExclude = excludeMatchers.find((m) => m.isMatch(relativePath));
|
|
271
358
|
if (matchedExclude) {
|
|
272
|
-
|
|
273
|
-
return true;
|
|
359
|
+
return { kind: 'excluded', reason: 'pattern-matched', matchedRule: matchedExclude.rule };
|
|
274
360
|
}
|
|
275
|
-
//
|
|
276
|
-
//
|
|
277
|
-
return
|
|
361
|
+
// Falls through to the cascade's final branch, which is the one that needs an
|
|
362
|
+
// oracle rather than a column — see {@link classifyGitignored}.
|
|
363
|
+
return undefined;
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* The cascade's LAST branch, kept out of {@link classifyExclusion} so that
|
|
367
|
+
* function can stay a pure function of the row.
|
|
368
|
+
*
|
|
369
|
+
* Its position at the end is not an implementation detail — it is why the
|
|
370
|
+
* demand-read is safe. Every earlier discriminator has already declined, so the
|
|
371
|
+
* set of paths this asks the oracle about is exactly the set the shipped
|
|
372
|
+
* classifier asked about, link for link.
|
|
373
|
+
*
|
|
374
|
+
* Existence-gated, and the gate is the point: a path that is not there cannot
|
|
375
|
+
* leak anything, and neither oracle can be trusted to say so — see
|
|
376
|
+
* {@link readGitignored}.
|
|
377
|
+
*/
|
|
378
|
+
function classifyGitignored(targetPath, options, facts, state) {
|
|
379
|
+
if (!facts.exists)
|
|
380
|
+
return undefined;
|
|
381
|
+
if (!readGitignored(targetPath, options, state))
|
|
382
|
+
return undefined;
|
|
383
|
+
return classifyGitignoredTarget(targetPath, options.deferredArtifacts);
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Apply a verdict to the walk state — the only place the classifier's decision
|
|
387
|
+
* becomes a mutation.
|
|
388
|
+
*
|
|
389
|
+
* @returns true if the caller should stop processing this link
|
|
390
|
+
*/
|
|
391
|
+
function applyExclusionVerdict(verdict, targetPath, sourcePath, link, facts, state) {
|
|
392
|
+
switch (verdict.kind) {
|
|
393
|
+
case 'deferred': {
|
|
394
|
+
state.deferredAssetSet.add(toForwardSlash(targetPath));
|
|
395
|
+
return true;
|
|
396
|
+
}
|
|
397
|
+
case 'excluded': {
|
|
398
|
+
state.excludedReferences.push(makeExclusion(targetPath, sourcePath, facts.exists, verdict.reason, link, verdict.matchedRule));
|
|
399
|
+
return true;
|
|
400
|
+
}
|
|
401
|
+
case 'skipped': {
|
|
402
|
+
return true;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Check if a target path should be excluded for structural reasons
|
|
408
|
+
* (deferred build artifact, directory, outside project, navigation file, or
|
|
409
|
+
* pattern match).
|
|
410
|
+
*
|
|
411
|
+
* Pass 1′ fills the row, pass 4 judges it, and the recording is a third step —
|
|
412
|
+
* the three used to be one interleaved function that stat'ed paths mid-cascade.
|
|
413
|
+
*
|
|
414
|
+
* @returns true if the link was excluded or deferred (caller should skip to next link)
|
|
415
|
+
*/
|
|
416
|
+
function checkExclusions(targetPath, sourcePath, link, options, excludeMatchers, state) {
|
|
417
|
+
const facts = fillPathFacts(targetPath, options, state);
|
|
418
|
+
const verdict = classifyExclusion(targetPath, options, excludeMatchers, facts)
|
|
419
|
+
?? classifyGitignored(targetPath, options, facts, state);
|
|
420
|
+
if (verdict === undefined)
|
|
421
|
+
return false;
|
|
422
|
+
return applyExclusionVerdict(verdict, targetPath, sourcePath, link, facts, state);
|
|
278
423
|
}
|
|
279
424
|
/**
|
|
280
425
|
* Process a single local_file link during the graph walk.
|
|
@@ -292,7 +437,7 @@ function processLink(link, currentResource, currentDepth, registry, options, exc
|
|
|
292
437
|
// Resolve the target path from the link href
|
|
293
438
|
const targetPath = resolveHrefToPath(hrefWithoutAnchor, currentResource.filePath, options.projectRoot);
|
|
294
439
|
// Check structural exclusions (deferred, directory, boundary, navigation, pattern, gitignore)
|
|
295
|
-
if (checkExclusions(targetPath, currentResource.filePath, link, options, excludeMatchers, state
|
|
440
|
+
if (checkExclusions(targetPath, currentResource.filePath, link, options, excludeMatchers, state)) {
|
|
296
441
|
return;
|
|
297
442
|
}
|
|
298
443
|
// Try to find the target in the registry (markdown files)
|
|
@@ -301,22 +446,96 @@ function processLink(link, currentResource, currentDepth, registry, options, exc
|
|
|
301
446
|
: registry.getResource(targetPath);
|
|
302
447
|
if (targetResource) {
|
|
303
448
|
processRegistryResource(targetResource, targetPath, currentResource.filePath, link, currentDepth, options, state);
|
|
304
|
-
//
|
|
449
|
+
// Straight to the probe, not through `fillPathFacts`: this site wants one
|
|
450
|
+
// column, and building a whole row to read it would compute an
|
|
451
|
+
// `insideProject` the caller immediately discards. The probe memo is what
|
|
452
|
+
// makes it free — this used to be a second `existsSync` of the identical
|
|
453
|
+
// path in the same tick.
|
|
305
454
|
}
|
|
306
|
-
else if (
|
|
307
|
-
// Not in registry — non-markdown asset that exists on disk
|
|
455
|
+
else if (state.pathProbe.probe(targetPath).exists) {
|
|
456
|
+
// Not in registry — non-markdown asset that exists on disk.
|
|
457
|
+
//
|
|
458
|
+
// ⚠️ `currentDepth` is deliberately NOT consulted here, and its absence is
|
|
459
|
+
// the behaviour, not an oversight. An asset is a leaf: bundling it enqueues
|
|
460
|
+
// nothing, so there is no depth for a limit to bound. A `maxDepth` of 1
|
|
461
|
+
// therefore still ships every image, JSON and PDF hanging off a
|
|
462
|
+
// depth-3 document, provided some routed markdown reached that document.
|
|
463
|
+
// Stated in {@link walkLinkGraph}'s own semantics list ("Non-markdown
|
|
464
|
+
// assets bypass depth limits") and mirrored for non-routable REGISTRY
|
|
465
|
+
// members in {@link processRegistryResource} — do not "fix" one site
|
|
466
|
+
// without the other; they are one rule with two arms.
|
|
308
467
|
state.bundledAssetSet.add(toForwardSlash(targetPath));
|
|
309
468
|
}
|
|
310
469
|
else {
|
|
311
470
|
// File doesn't exist and not in registry, and not deferred (handled above).
|
|
312
471
|
// Record as missing-target so downstream emits LINK_MISSING_TARGET.
|
|
313
472
|
state.excludedReferences.push(makeExclusion(targetPath, currentResource.filePath, false, 'missing-target', link));
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
if (relativeEscapesRoot(safePath.relative(dirname(options.skillRootPath), targetPath))) {
|
|
476
|
+
state.followedOutsideSkillDir.push(makeFollowedLink(targetPath, currentResource.filePath, true, link));
|
|
314
477
|
}
|
|
315
478
|
}
|
|
316
479
|
/**
|
|
317
|
-
*
|
|
480
|
+
* Is this file one the walker walks THROUGH, enqueuing its link targets as
|
|
481
|
+
* further bundle members?
|
|
482
|
+
*
|
|
483
|
+
* Routing is markdown-only. HTML may be a registry member — parsed, links
|
|
484
|
+
* known, reachable by the rewriter — without being a door: the decision is
|
|
485
|
+
* "a leaf we can read, not a door we walk through", and it is spec-backed
|
|
486
|
+
* (Anthropic's skill guidance is markdown throughout, and VAT's own stance docs
|
|
487
|
+
* mention HTML nowhere).
|
|
488
|
+
*
|
|
489
|
+
* Keyed on {@link parserKindForPath} rather than a local extension test, so
|
|
490
|
+
* VAT keeps ONE parser discriminator: a format that gains a parser gains a
|
|
491
|
+
* routability answer in the same edit.
|
|
492
|
+
*/
|
|
493
|
+
function isRoutable(filePath) {
|
|
494
|
+
return parserKindForPath(filePath) === 'markdown';
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Record every local-file link out of a bundled non-routable member as a
|
|
498
|
+
* candidate `non-routable-source` exclusion.
|
|
499
|
+
*
|
|
500
|
+
* Without this the targets simply vanish: `SKILL.md → guide.html →
|
|
501
|
+
* diagram.svg` bundles the HTML, never looks inside it, and ships a bundled
|
|
502
|
+
* page whose `<img src>` points at a file that is not there. A silent drop
|
|
503
|
+
* becomes a reported one.
|
|
504
|
+
*/
|
|
505
|
+
function recordUnfollowedLinks(member, options, state) {
|
|
506
|
+
for (const link of member.links) {
|
|
507
|
+
if (!isLocalFileLink(link.type))
|
|
508
|
+
continue;
|
|
509
|
+
const hrefWithoutAnchor = link.href.split('#')[0] ?? link.href;
|
|
510
|
+
if (hrefWithoutAnchor === '')
|
|
511
|
+
continue;
|
|
512
|
+
const targetPath = resolveHrefToPath(hrefWithoutAnchor, member.filePath, options.projectRoot);
|
|
513
|
+
const targetExists = state.pathProbe.probe(targetPath).exists;
|
|
514
|
+
state.unfollowedFromNonRoutable.push(makeExclusion(targetPath, member.filePath, targetExists, 'non-routable-source', link));
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Process a registry resource link: check routability, depth, cycle, and either
|
|
519
|
+
* bundle or exclude.
|
|
318
520
|
*/
|
|
319
521
|
function processRegistryResource(targetResource, targetPath, sourcePath, link, currentDepth, options, state) {
|
|
522
|
+
// A non-routable member is a leaf, and leaves are bundled on the same terms
|
|
523
|
+
// as plain assets — which bypass `maxDepth` entirely. Checking routability
|
|
524
|
+
// BEFORE depth is what makes adding HTML to a registry a pure gain: the file
|
|
525
|
+
// still travels exactly as it did when it was an unparsed asset, it is merely
|
|
526
|
+
// parsed now so the rewriter can reach its links. Applying the depth limit
|
|
527
|
+
// here instead would silently DROP HTML that a markdown-only registry shipped.
|
|
528
|
+
if (!isRoutable(targetResource.filePath)) {
|
|
529
|
+
if (state.visitedResourceIds.has(targetResource.id)) {
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
state.visitedResourceIds.add(targetResource.id);
|
|
533
|
+
state.bundledResourceMap.set(targetResource.id, targetResource);
|
|
534
|
+
// `maxBundledDepth` describes how far the walk ROUTED. A leaf does not
|
|
535
|
+
// extend that, any more than a bundled PNG does.
|
|
536
|
+
recordUnfollowedLinks(targetResource, options, state);
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
320
539
|
// Check depth limit. A registry resource came from a disk crawl, so it
|
|
321
540
|
// exists by construction — `targetExists: true` is a fact here, not a guess.
|
|
322
541
|
if (currentDepth >= options.maxDepth) {
|
|
@@ -352,9 +571,36 @@ function processRegistryResource(targetResource, targetPath, sourcePath, link, c
|
|
|
352
571
|
* @returns Graph walk result with bundled resources, assets, and exclusions
|
|
353
572
|
*/
|
|
354
573
|
export function walkLinkGraph(skillResourceId, registry, options) {
|
|
574
|
+
// This walker is not a projection contributor, so the merge driver never sees
|
|
575
|
+
// it and it has no stratum of its own. It records under the synthetic id
|
|
576
|
+
// `walk-link-graph:walk` in the `crawl` stratum — declared in
|
|
577
|
+
// `crawl-timing.ts`, deliberately not an ad-hoc string here, because a
|
|
578
|
+
// synthetic id that arrives by accident is indistinguishable in the dump from
|
|
579
|
+
// a real contributor's.
|
|
580
|
+
//
|
|
581
|
+
// `try`/`finally` rather than a record before each `return`: the early exit
|
|
582
|
+
// for an unknown skill id is a real (cheap) call and must still be counted,
|
|
583
|
+
// and a throw must not silently drop a bracket.
|
|
584
|
+
const startedAt = crawlTimingStart();
|
|
585
|
+
try {
|
|
586
|
+
return walkLinkGraphBody(skillResourceId, registry, options);
|
|
587
|
+
}
|
|
588
|
+
finally {
|
|
589
|
+
recordCrawlPass(CRAWL_WALKER_ID, 'crawl', CRAWL_PASS_INSIDE, startedAt);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* The walk itself, with no timing concern in it.
|
|
594
|
+
*
|
|
595
|
+
* @param skillResourceId - The resource ID of the skill's SKILL.md in the registry
|
|
596
|
+
* @param registry - A walkable registry with pre-parsed resources
|
|
597
|
+
* @param options - Walk options (depth, excludes, etc.)
|
|
598
|
+
* @returns Graph walk result with bundled resources, assets, and exclusions
|
|
599
|
+
*/
|
|
600
|
+
function walkLinkGraphBody(skillResourceId, registry, options) {
|
|
355
601
|
const skillResource = registry.getResourceById(skillResourceId);
|
|
356
602
|
if (!skillResource) {
|
|
357
|
-
return { bundledResources: [], bundledAssets: [], excludedReferences: [], maxBundledDepth: 0, deferredAssets: [] };
|
|
603
|
+
return { bundledResources: [], bundledAssets: [], excludedReferences: [], outsideSkillDirLinks: [], maxBundledDepth: 0, deferredAssets: [] };
|
|
358
604
|
}
|
|
359
605
|
// Compile exclude patterns once
|
|
360
606
|
const excludeMatchers = options.excludeRules.map((rule) => ({
|
|
@@ -374,6 +620,10 @@ export function walkLinkGraph(skillResourceId, registry, options) {
|
|
|
374
620
|
excludedReferences: [],
|
|
375
621
|
maxBundledDepth: 0,
|
|
376
622
|
queue: [[skillResource, 0]],
|
|
623
|
+
unfollowedFromNonRoutable: [],
|
|
624
|
+
followedOutsideSkillDir: [],
|
|
625
|
+
pathProbe: options.pathProbe ?? new FsLookupCache(),
|
|
626
|
+
gitignoreFacts: new Map(),
|
|
377
627
|
};
|
|
378
628
|
while (state.queue.length > 0) {
|
|
379
629
|
const entry = state.queue.shift();
|
|
@@ -387,10 +637,44 @@ export function walkLinkGraph(skillResourceId, registry, options) {
|
|
|
387
637
|
}
|
|
388
638
|
}
|
|
389
639
|
}
|
|
640
|
+
// What actually SHIPPED, resolved once the walk is over. Deferred artifacts
|
|
641
|
+
// count as accounted-for: the target will exist once the build materializes
|
|
642
|
+
// it, which is the same reason they are not `missing-target`.
|
|
643
|
+
const bundledPaths = new Set([
|
|
644
|
+
...state.bundledAssetSet,
|
|
645
|
+
...state.deferredAssetSet,
|
|
646
|
+
...[...state.bundledResourceMap.values()].map(r => toForwardSlash(r.filePath)),
|
|
647
|
+
]);
|
|
648
|
+
const targetShipped = (r) => bundledPaths.has(toForwardSlash(r.path));
|
|
649
|
+
// 🔑 Two reasons ask ONE question — "did the target ship?" — and both can only
|
|
650
|
+
// answer it here, against the FINAL bundle, or the answer depends on queue
|
|
651
|
+
// order.
|
|
652
|
+
//
|
|
653
|
+
// `depth-exceeded` is recorded eagerly, at the moment an edge is refused, and
|
|
654
|
+
// that is correct: the refusal is a fact about the EDGE. It is not a fact
|
|
655
|
+
// about the TARGET, and `LINK_DROPPED_BY_DEPTH` claims the latter — "this link
|
|
656
|
+
// was not bundled". On any diamond the two diverge: the same reference cited
|
|
657
|
+
// from SKILL.md and again from a page one hop deeper ships via the shallow
|
|
658
|
+
// route and is refused on the deep one, and the finding used to fire anyway.
|
|
659
|
+
// Measured on `packages/vat-example-cat-agents`, whose
|
|
660
|
+
// `cat-breed-selection.md` is in the built bundle at depth 1 and was reported
|
|
661
|
+
// dropped for its depth-2 occurrence via `workflow-orchestration.md`. The
|
|
662
|
+
// remedy the message implies — raise `linkFollowDepth` — is a no-op for a link
|
|
663
|
+
// that already works.
|
|
664
|
+
//
|
|
665
|
+
// Filtered in place rather than held back and appended, so the surviving
|
|
666
|
+
// exclusions keep the order the walk found them in.
|
|
667
|
+
const excludedReferences = state.excludedReferences.filter((r) => r.excludeReason !== 'depth-exceeded' || !targetShipped(r));
|
|
668
|
+
for (const unfollowed of state.unfollowedFromNonRoutable) {
|
|
669
|
+
if (!targetShipped(unfollowed)) {
|
|
670
|
+
excludedReferences.push(unfollowed);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
390
673
|
return {
|
|
391
674
|
bundledResources: [...state.bundledResourceMap.values()],
|
|
392
675
|
bundledAssets: [...state.bundledAssetSet],
|
|
393
|
-
excludedReferences
|
|
676
|
+
excludedReferences,
|
|
677
|
+
outsideSkillDirLinks: state.followedOutsideSkillDir.filter(targetShipped),
|
|
394
678
|
maxBundledDepth: state.maxBundledDepth,
|
|
395
679
|
deferredAssets: [...state.deferredAssetSet],
|
|
396
680
|
};
|