@skill-graph/cli 0.5.6
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/CHANGELOG.md +247 -0
- package/LICENSE +200 -0
- package/NOTICE +62 -0
- package/README.md +398 -0
- package/SKILL_GRAPH.md +443 -0
- package/bin/skill-graph.js +374 -0
- package/docs/ADOPTION.md +117 -0
- package/docs/CONFORMANCE.md +66 -0
- package/docs/PRIMER.md +384 -0
- package/docs/QUICKSTART-30MIN.md +333 -0
- package/docs/ROUTING-METRICS.md +120 -0
- package/docs/SKILL-MD-FORMAT-COMPATIBILITY.md +127 -0
- package/docs/SKILL_AUDIT_CHECKLIST.md +199 -0
- package/docs/SKILL_AUDIT_LOOP.md +195 -0
- package/docs/SKILL_METADATA_PROTOCOL.md +609 -0
- package/docs/_archived/marketplace-publication-priority-2026-05-18.md +239 -0
- package/docs/adr/0001-predicate-set.md +69 -0
- package/docs/adr/0002-json-ld-context.md +82 -0
- package/docs/adr/0003-ontoclean-rigidity-tags.md +65 -0
- package/docs/adr/0004-persistent-identifiers.md +74 -0
- package/docs/adr/0005-freshness-consolidation.md +70 -0
- package/docs/adr/0006-revise-predicate-rename.md +105 -0
- package/docs/adr/0007-audit-loop-cadence.md +99 -0
- package/docs/adr/0008-skill-surface-split-and-curation-policy.md +93 -0
- package/docs/category-consumers.md +168 -0
- package/docs/concept-map.md +194 -0
- package/docs/diagrams/drift-states.mmd +21 -0
- package/docs/diagrams/manifest-pipeline.mmd +25 -0
- package/docs/diagrams/routing-harness.mmd +41 -0
- package/docs/diagrams/starter-graph.mmd +53 -0
- package/docs/field-decision-guide.md +315 -0
- package/docs/field-rationale.md +211 -0
- package/docs/field-reference.generated.md +624 -0
- package/docs/field-reference.md +1426 -0
- package/docs/glossary.md +190 -0
- package/docs/head-noun-glossary.md +63 -0
- package/docs/images/audit-phases.png +0 -0
- package/docs/images/drift-states.png +0 -0
- package/docs/images/graded-mode.png +0 -0
- package/docs/images/manifest-pipeline.png +0 -0
- package/docs/images/routing-harness.png +0 -0
- package/docs/images/skill-anatomy.png +0 -0
- package/docs/images/starter-graph.png +0 -0
- package/docs/images/system-model.png +0 -0
- package/docs/integrations/github-actions.md +155 -0
- package/docs/manifest-field-mapping.md +443 -0
- package/docs/marketplace-publication-queue.generated.md +240 -0
- package/docs/marketplace-release-agent-prompt.md +82 -0
- package/docs/marketplace-skill-candidate-list.md +272 -0
- package/docs/marketplace-syndication.md +222 -0
- package/docs/migration-sample-review.md +155 -0
- package/docs/migrations/v4-to-v5.md +168 -0
- package/docs/migrations/v5-to-v6.md +221 -0
- package/docs/name-exceptions.yaml +37 -0
- package/docs/plans/marketplace-p1-public-migration-plan.md +41 -0
- package/docs/plans/multi-root-workspace.md +148 -0
- package/docs/plans/scripts-roadmap.md +107 -0
- package/docs/plans/v4-schema-bump.md +160 -0
- package/docs/plans/wave-2-extraction.md +122 -0
- package/docs/positioning-vs-marketplaces.md +175 -0
- package/docs/proposals/skill-audit-loop-positioning.md +160 -0
- package/docs/quality-doctrine.md +138 -0
- package/docs/recommended-skills.md +150 -0
- package/docs/research/skill-comprehension-eval-research.md +1830 -0
- package/docs/research/skill-retrieval-evidence.md +66 -0
- package/docs/skill-metadata-protocol.md +471 -0
- package/docs/skills-sh-maintainer-cleanup-request.md +80 -0
- package/examples/audits/a11y/findings.md +52 -0
- package/examples/audits/a11y/scorecard.md +21 -0
- package/examples/audits/a11y/verdict.md +44 -0
- package/examples/audits/debugging/findings.md +59 -0
- package/examples/audits/debugging/scorecard.md +22 -0
- package/examples/audits/debugging/verdict.md +33 -0
- package/examples/audits/documentation/findings.md +59 -0
- package/examples/audits/documentation/scorecard.md +22 -0
- package/examples/audits/documentation/verdict.md +33 -0
- package/examples/evals/a11y.json +140 -0
- package/examples/evals/api-design.json +52 -0
- package/examples/evals/code-review.json +52 -0
- package/examples/evals/data-modeling.json +52 -0
- package/examples/evals/database-migration.json +52 -0
- package/examples/evals/debugging.json +118 -0
- package/examples/evals/dependency-architecture.json +52 -0
- package/examples/evals/design-system-architecture.json +52 -0
- package/examples/evals/error-tracking.json +52 -0
- package/examples/evals/event-contract-design.json +52 -0
- package/examples/evals/form-ux-architecture.json +52 -0
- package/examples/evals/framework-fit-analysis.json +52 -0
- package/examples/evals/graph-audit.json +139 -0
- package/examples/evals/information-architecture.json +52 -0
- package/examples/evals/interaction-feedback.json +52 -0
- package/examples/evals/interaction-patterns.json +52 -0
- package/examples/evals/layout-composition.json +52 -0
- package/examples/evals/lint-overlay.json +117 -0
- package/examples/evals/microcopy.json +52 -0
- package/examples/evals/observability-modeling.json +52 -0
- package/examples/evals/pattern-recognition.json +96 -0
- package/examples/evals/performance-engineering.json +52 -0
- package/examples/evals/refactor.json +128 -0
- package/examples/evals/semiotics.json +52 -0
- package/examples/evals/skill-infrastructure.json +96 -0
- package/examples/evals/skill-router.json +140 -0
- package/examples/evals/skill-router.routing.json +113 -0
- package/examples/evals/system-interface-contracts.json +52 -0
- package/examples/evals/task-analysis.json +52 -0
- package/examples/evals/testing-strategy.json +118 -0
- package/examples/evals/type-safety.json +249 -0
- package/examples/evals/visual-design-foundations.json +52 -0
- package/examples/evals/webhook-integration.json +52 -0
- package/examples/exports/a11y.skill-md.md +80 -0
- package/examples/exports/debugging.skill-md.md +80 -0
- package/examples/exports/refactor.skill-md.md +78 -0
- package/examples/exports/testing-strategy.skill-md.md +81 -0
- package/examples/projects/markdown-static-site/README.md +115 -0
- package/examples/projects/markdown-static-site/skills/content-source-router/SKILL.md +131 -0
- package/examples/projects/markdown-static-site/skills/image-optimization-pipeline-config/SKILL.md +132 -0
- package/examples/projects/markdown-static-site/skills/link-rot-detection/SKILL.md +103 -0
- package/examples/projects/markdown-static-site/skills/markdown-post-frontmatter-validation/SKILL.md +133 -0
- package/examples/projects/markdown-static-site/skills/migrate-posts-to-v2-frontmatter/SKILL.md +140 -0
- package/examples/projects/saas-stripe-postgres/README.md +208 -0
- package/examples/projects/saas-stripe-postgres/db/migrations/0004_canonicalize_orders.sql +37 -0
- package/examples/projects/saas-stripe-postgres/db/schema.sql +112 -0
- package/examples/projects/saas-stripe-postgres/skills/migrate-orders-to-canonical-schema/SKILL.md +149 -0
- package/examples/projects/saas-stripe-postgres/skills/nextjs-server-action-validation/SKILL.md +154 -0
- package/examples/projects/saas-stripe-postgres/skills/payment-provider-router/SKILL.md +153 -0
- package/examples/projects/saas-stripe-postgres/skills/postgres-rls-pattern/SKILL.md +163 -0
- package/examples/projects/saas-stripe-postgres/skills/stripe-webhook-signature-verification/SKILL.md +137 -0
- package/examples/protocol/skill-metadata-template.md +301 -0
- package/examples/protocol/skills.manifest.sample.json +13245 -0
- package/examples/skill-metadata-template.md +317 -0
- package/examples/skills.manifest.sample.json +13519 -0
- package/examples/tests/v3-1-skos-fixture/SKILL.md +93 -0
- package/marketplace/README.md +17 -0
- package/marketplace/skills/a11y/SKILL.md +66 -0
- package/marketplace/skills/acid-fundamentals/SKILL.md +106 -0
- package/marketplace/skills/agent-engineering/SKILL.md +386 -0
- package/marketplace/skills/agent-eval-design/SKILL.md +55 -0
- package/marketplace/skills/ai-native-development/SKILL.md +294 -0
- package/marketplace/skills/api-design/SKILL.md +60 -0
- package/marketplace/skills/architecture-decision-records/SKILL.md +55 -0
- package/marketplace/skills/background-jobs/SKILL.md +265 -0
- package/marketplace/skills/bounded-context-mapping/SKILL.md +55 -0
- package/marketplace/skills/cap-theorem-tradeoffs/SKILL.md +127 -0
- package/marketplace/skills/client-server-boundary/SKILL.md +187 -0
- package/marketplace/skills/code-review/SKILL.md +120 -0
- package/marketplace/skills/color-system-design/SKILL.md +43 -0
- package/marketplace/skills/component-architecture/SKILL.md +126 -0
- package/marketplace/skills/compression/SKILL.md +112 -0
- package/marketplace/skills/conceptual-modeling/SKILL.md +181 -0
- package/marketplace/skills/connection-pooling/SKILL.md +105 -0
- package/marketplace/skills/constraint-awareness/SKILL.md +287 -0
- package/marketplace/skills/content-monitor/SKILL.md +209 -0
- package/marketplace/skills/context-engineering/SKILL.md +320 -0
- package/marketplace/skills/context-graph/SKILL.md +174 -0
- package/marketplace/skills/context-management/SKILL.md +174 -0
- package/marketplace/skills/context-window/SKILL.md +239 -0
- package/marketplace/skills/contract-testing/SKILL.md +120 -0
- package/marketplace/skills/cron-scheduling/SKILL.md +223 -0
- package/marketplace/skills/dark-mode-implementation/SKILL.md +47 -0
- package/marketplace/skills/data-modeling/SKILL.md +59 -0
- package/marketplace/skills/data-modeling-fundamentals/SKILL.md +117 -0
- package/marketplace/skills/database-migration/SKILL.md +429 -0
- package/marketplace/skills/debugging/SKILL.md +67 -0
- package/marketplace/skills/dependency-architecture/SKILL.md +58 -0
- package/marketplace/skills/design-module-composition/SKILL.md +43 -0
- package/marketplace/skills/design-system-architecture/SKILL.md +61 -0
- package/marketplace/skills/design-thinking/SKILL.md +44 -0
- package/marketplace/skills/diagnosis/SKILL.md +296 -0
- package/marketplace/skills/diff-analysis/SKILL.md +188 -0
- package/marketplace/skills/e2e-test-design/SKILL.md +113 -0
- package/marketplace/skills/entity-relationship-modeling/SKILL.md +218 -0
- package/marketplace/skills/epistemic-grounding/SKILL.md +112 -0
- package/marketplace/skills/error-boundary/SKILL.md +235 -0
- package/marketplace/skills/error-tracking/SKILL.md +261 -0
- package/marketplace/skills/eval-driven-development/SKILL.md +147 -0
- package/marketplace/skills/evaluation/SKILL.md +113 -0
- package/marketplace/skills/event-contract-design/SKILL.md +60 -0
- package/marketplace/skills/event-storming/SKILL.md +56 -0
- package/marketplace/skills/form-ux-architecture/SKILL.md +60 -0
- package/marketplace/skills/framework-fit-analysis/SKILL.md +59 -0
- package/marketplace/skills/frontend-architecture/SKILL.md +43 -0
- package/marketplace/skills/generative-ui/SKILL.md +118 -0
- package/marketplace/skills/graph-audit/SKILL.md +81 -0
- package/marketplace/skills/guardrails/SKILL.md +118 -0
- package/marketplace/skills/hooks-patterns/SKILL.md +185 -0
- package/marketplace/skills/http-semantics/SKILL.md +136 -0
- package/marketplace/skills/ideation/SKILL.md +41 -0
- package/marketplace/skills/indexing-strategy/SKILL.md +108 -0
- package/marketplace/skills/information-architecture/SKILL.md +59 -0
- package/marketplace/skills/integration-test-design/SKILL.md +111 -0
- package/marketplace/skills/intent-recognition/SKILL.md +136 -0
- package/marketplace/skills/interaction-feedback/SKILL.md +59 -0
- package/marketplace/skills/interaction-patterns/SKILL.md +59 -0
- package/marketplace/skills/journey-mapping/SKILL.md +41 -0
- package/marketplace/skills/keywords/SKILL.md +213 -0
- package/marketplace/skills/knowledge-modeling/SKILL.md +232 -0
- package/marketplace/skills/layout-composition/SKILL.md +59 -0
- package/marketplace/skills/linguistics/SKILL.md +429 -0
- package/marketplace/skills/lint-overlay/SKILL.md +76 -0
- package/marketplace/skills/mental-models/SKILL.md +126 -0
- package/marketplace/skills/merge-queue/SKILL.md +94 -0
- package/marketplace/skills/methodology/SKILL.md +317 -0
- package/marketplace/skills/microcopy/SKILL.md +232 -0
- package/marketplace/skills/middleware-patterns/SKILL.md +363 -0
- package/marketplace/skills/mobile-responsive-ux/SKILL.md +287 -0
- package/marketplace/skills/mutation-testing/SKILL.md +112 -0
- package/marketplace/skills/naming-conventions/SKILL.md +112 -0
- package/marketplace/skills/observability-modeling/SKILL.md +59 -0
- package/marketplace/skills/ontology-modeling/SKILL.md +67 -0
- package/marketplace/skills/owasp-security/SKILL.md +153 -0
- package/marketplace/skills/pattern-recognition/SKILL.md +472 -0
- package/marketplace/skills/performance-budgets/SKILL.md +185 -0
- package/marketplace/skills/performance-engineering/SKILL.md +58 -0
- package/marketplace/skills/performance-testing/SKILL.md +125 -0
- package/marketplace/skills/printify/SKILL.md +42 -0
- package/marketplace/skills/prioritization/SKILL.md +118 -0
- package/marketplace/skills/problem-framing/SKILL.md +41 -0
- package/marketplace/skills/problem-locating-solving/SKILL.md +203 -0
- package/marketplace/skills/project-knowledge-extraction/SKILL.md +54 -0
- package/marketplace/skills/prompt-craft/SKILL.md +134 -0
- package/marketplace/skills/prompt-injection-defense/SKILL.md +132 -0
- package/marketplace/skills/property-based-testing/SKILL.md +100 -0
- package/marketplace/skills/prototyping/SKILL.md +43 -0
- package/marketplace/skills/query-optimization/SKILL.md +144 -0
- package/marketplace/skills/real-time-updates/SKILL.md +324 -0
- package/marketplace/skills/ref-patterns/SKILL.md +284 -0
- package/marketplace/skills/refactor/SKILL.md +65 -0
- package/marketplace/skills/rendering-models/SKILL.md +142 -0
- package/marketplace/skills/replication-patterns/SKILL.md +110 -0
- package/marketplace/skills/research-synthesis/SKILL.md +41 -0
- package/marketplace/skills/route-handler-design/SKILL.md +347 -0
- package/marketplace/skills/schema-evolution/SKILL.md +140 -0
- package/marketplace/skills/security-fundamentals/SKILL.md +139 -0
- package/marketplace/skills/semantic-center/SKILL.md +194 -0
- package/marketplace/skills/semantic-relations/SKILL.md +250 -0
- package/marketplace/skills/semantics/SKILL.md +366 -0
- package/marketplace/skills/semiotics/SKILL.md +230 -0
- package/marketplace/skills/seo-strategy/SKILL.md +260 -0
- package/marketplace/skills/server-actions-design/SKILL.md +243 -0
- package/marketplace/skills/server-components-design/SKILL.md +190 -0
- package/marketplace/skills/sharding-strategy/SKILL.md +123 -0
- package/marketplace/skills/shopify/SKILL.md +42 -0
- package/marketplace/skills/skill-infrastructure/SKILL.md +320 -0
- package/marketplace/skills/skill-router/SKILL.md +71 -0
- package/marketplace/skills/skill-scaffold/SKILL.md +105 -0
- package/marketplace/skills/snapshot-testing/SKILL.md +120 -0
- package/marketplace/skills/spec-driven-development/SKILL.md +148 -0
- package/marketplace/skills/state-machine-modeling/SKILL.md +56 -0
- package/marketplace/skills/state-management/SKILL.md +134 -0
- package/marketplace/skills/streaming-architecture/SKILL.md +194 -0
- package/marketplace/skills/summarization/SKILL.md +156 -0
- package/marketplace/skills/suspense-patterns/SKILL.md +265 -0
- package/marketplace/skills/system-interface-contracts/SKILL.md +59 -0
- package/marketplace/skills/task-analysis/SKILL.md +201 -0
- package/marketplace/skills/taxonomy-design/SKILL.md +66 -0
- package/marketplace/skills/test-coverage-strategy/SKILL.md +108 -0
- package/marketplace/skills/test-doubles-design/SKILL.md +98 -0
- package/marketplace/skills/test-driven-development/SKILL.md +96 -0
- package/marketplace/skills/testing-strategy/SKILL.md +67 -0
- package/marketplace/skills/theme-system-design/SKILL.md +43 -0
- package/marketplace/skills/tool-call-flow/SKILL.md +229 -0
- package/marketplace/skills/tool-call-strategy/SKILL.md +292 -0
- package/marketplace/skills/transaction-isolation/SKILL.md +98 -0
- package/marketplace/skills/type-safety/SKILL.md +177 -0
- package/marketplace/skills/typography-system/SKILL.md +43 -0
- package/marketplace/skills/usability-testing/SKILL.md +43 -0
- package/marketplace/skills/user-research/SKILL.md +43 -0
- package/marketplace/skills/vercel-composition-patterns/SKILL.md +157 -0
- package/marketplace/skills/version-control/SKILL.md +233 -0
- package/marketplace/skills/visual-design-foundations/SKILL.md +59 -0
- package/marketplace/skills/visual-hierarchy/SKILL.md +43 -0
- package/marketplace/skills/webhook-integration/SKILL.md +331 -0
- package/marketplace/skills/writing-humanizer/SKILL.md +380 -0
- package/package.json +67 -0
- package/schemas/manifest.schema.json +811 -0
- package/schemas/manifest.v2.schema.json +164 -0
- package/schemas/manifest.v3.schema.json +758 -0
- package/schemas/manifest.v4.schema.json +755 -0
- package/schemas/manifest.v5.schema.json +755 -0
- package/schemas/manifest.v6.schema.json +811 -0
- package/schemas/skill.context.jsonld +279 -0
- package/schemas/skill.schema.json +919 -0
- package/schemas/skill.v2.schema.json +201 -0
- package/schemas/skill.v3.schema.json +827 -0
- package/schemas/skill.v4.schema.json +822 -0
- package/schemas/skill.v5.schema.json +830 -0
- package/schemas/skill.v6.schema.json +946 -0
- package/schemas/vocabulary/keywords.json +180 -0
- package/schemas/vocabulary/workspace_tags.json +23 -0
- package/scripts/__tests__/migrate-skill-v2-to-v3.test.js +161 -0
- package/scripts/__tests__/migrate-skill-v3-to-v4.test.js +158 -0
- package/scripts/__tests__/test-export-parser-drift.js +149 -0
- package/scripts/__tests__/test-marketplace-export.js +114 -0
- package/scripts/__tests__/test-router-paths.js +82 -0
- package/scripts/__tests__/test-stability-promotion.js +244 -0
- package/scripts/__tests__/test-v3-1-alias-contract.js +109 -0
- package/scripts/__tests__/test-v3-1-skos-runtime.js +116 -0
- package/scripts/backfill-schema-version.js +198 -0
- package/scripts/build-field-reference.js +160 -0
- package/scripts/build-retrieval-baseline.js +511 -0
- package/scripts/check-markdown-links.js +211 -0
- package/scripts/check-protocol-consistency.js +979 -0
- package/scripts/export-marketplace-skills.js +610 -0
- package/scripts/export-skill.js +374 -0
- package/scripts/generate-manifest.js +787 -0
- package/scripts/lib/alias-contract.js +83 -0
- package/scripts/lib/audit-prompt-builder.js +771 -0
- package/scripts/lib/mock-grader.js +134 -0
- package/scripts/lib/parse-frontmatter.js +429 -0
- package/scripts/lib/roots.js +119 -0
- package/scripts/lint/check-archetype-sections.js +185 -0
- package/scripts/lint/check-category-enum.js +83 -0
- package/scripts/lint/check-routing-eval.js +146 -0
- package/scripts/lint/check-routing-quality.js +211 -0
- package/scripts/lint/check-stability-promotion.js +220 -0
- package/scripts/lint/format-code-frame.js +206 -0
- package/scripts/marketplace-install.js +125 -0
- package/scripts/migrate-category-to-enum.js +169 -0
- package/scripts/migrate-skill-v2-to-v3.js +424 -0
- package/scripts/migrate-skill-v3-to-v4.js +200 -0
- package/scripts/migrate-skill-v5-to-v6.js +304 -0
- package/scripts/restructure-by-category.js +85 -0
- package/scripts/seed-publication-classification.js +282 -0
- package/scripts/skill-audit.js +893 -0
- package/scripts/skill-graph-drift.js +483 -0
- package/scripts/skill-graph-route.js +766 -0
- package/scripts/skill-graph-routing-eval.js +393 -0
- package/scripts/skill-lint.js +1317 -0
- package/scripts/skill-overlap.js +213 -0
- package/scripts/verify-skill-md-export.js +201 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Archetype-aware section validator for SKILL.md files.
|
|
4
|
+
*
|
|
5
|
+
* Each archetype has a minimum required set of H2 body sections defined in
|
|
6
|
+
* `docs/skill-metadata-protocol.md § Archetype Section Map`. This check:
|
|
7
|
+
*
|
|
8
|
+
* - Errors on any required H2 section that is missing from the body.
|
|
9
|
+
* - Warns on any H2 section that exists but whose content is empty
|
|
10
|
+
* (< 50 non-whitespace characters between consecutive H2 headers).
|
|
11
|
+
*
|
|
12
|
+
* The warning threshold (50 chars) catches placeholder sections like
|
|
13
|
+
* `## Verification\n\n(todo)\n` without tripping on intentionally terse
|
|
14
|
+
* but real sections such as a two-bullet `## Do NOT Use When` list.
|
|
15
|
+
*
|
|
16
|
+
* @module lint/check-archetype-sections
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
'use strict';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Required H2 sections per archetype, sourced from
|
|
23
|
+
* `docs/skill-metadata-protocol.md § Archetype Section Map`.
|
|
24
|
+
*
|
|
25
|
+
* @type {Record<string, string[]>}
|
|
26
|
+
*/
|
|
27
|
+
const REQUIRED_SECTIONS = {
|
|
28
|
+
capability: ['Coverage', 'Philosophy', 'Verification', 'Do NOT Use When'],
|
|
29
|
+
workflow: ['Coverage', 'Philosophy', 'Workflow', 'Verification', 'Do NOT Use When'],
|
|
30
|
+
router: ['Coverage', 'Routing Rules', 'Do NOT Use When'],
|
|
31
|
+
overlay: ['Coverage', 'Overlay Rules', 'Extends', 'Do NOT Use When'],
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Conditional section requirements. Applied on top of REQUIRED_SECTIONS based on
|
|
36
|
+
* frontmatter values. Each entry is `{ section, predicate, help }` — if the
|
|
37
|
+
* predicate returns true for the frontmatter, the section is required in
|
|
38
|
+
* addition to the archetype's base set.
|
|
39
|
+
*
|
|
40
|
+
* The `## Evals` rule closes a discoverability gap: a skill that declares
|
|
41
|
+
* `eval_artifacts: present` but has no `## Evals` body section hides the eval
|
|
42
|
+
* surface from readers (the artifact is findable only via the `examples/evals/`
|
|
43
|
+
* scan). Forcing the section guarantees the skill body links to its eval file.
|
|
44
|
+
*/
|
|
45
|
+
const CONDITIONAL_SECTIONS = [
|
|
46
|
+
{
|
|
47
|
+
section: 'Evals',
|
|
48
|
+
predicate: (fm) => fm && fm.eval_artifacts === 'present',
|
|
49
|
+
help: 'Add a "## Evals" body section that references the eval artifact under examples/evals/ so readers can discover it from the SKILL.md alone.',
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Extract all top-level H2 section headers from the markdown body (the part
|
|
55
|
+
* after the closing `---` of the frontmatter block).
|
|
56
|
+
*
|
|
57
|
+
* @param {string} sourceText - Full SKILL.md content.
|
|
58
|
+
* @returns {Array<{heading: string, line: number, contentLength: number}>}
|
|
59
|
+
* Each entry has the heading text (without `## `), the 1-based line number
|
|
60
|
+
* of the `## …` line, and the number of non-whitespace characters in the
|
|
61
|
+
* section body (i.e. between this H2 and the next one or end-of-file).
|
|
62
|
+
*/
|
|
63
|
+
function extractH2Sections(sourceText) {
|
|
64
|
+
const lines = sourceText.split(/\r?\n/);
|
|
65
|
+
|
|
66
|
+
// Find the end of the frontmatter block (second `---`).
|
|
67
|
+
let bodyStart = 0;
|
|
68
|
+
let dashCount = 0;
|
|
69
|
+
for (let i = 0; i < lines.length; i++) {
|
|
70
|
+
if (lines[i].trim() === '---') {
|
|
71
|
+
dashCount++;
|
|
72
|
+
if (dashCount === 2) { bodyStart = i + 1; break; }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Collect H2 positions in body.
|
|
77
|
+
const h2s = [];
|
|
78
|
+
for (let i = bodyStart; i < lines.length; i++) {
|
|
79
|
+
const m = lines[i].match(/^## (.+)$/);
|
|
80
|
+
if (m) {
|
|
81
|
+
h2s.push({ heading: m[1].trim(), lineIdx: i });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// For each H2, measure how many non-whitespace characters are in its body.
|
|
86
|
+
const sections = [];
|
|
87
|
+
for (let j = 0; j < h2s.length; j++) {
|
|
88
|
+
const start = h2s[j].lineIdx + 1;
|
|
89
|
+
const end = j + 1 < h2s.length ? h2s[j + 1].lineIdx : lines.length;
|
|
90
|
+
const body = lines.slice(start, end).join('\n');
|
|
91
|
+
sections.push({
|
|
92
|
+
heading: h2s[j].heading,
|
|
93
|
+
line: h2s[j].lineIdx + 1, // 1-based
|
|
94
|
+
contentLength: body.replace(/\s/g, '').length,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return sections;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Run archetype-aware section validation on one SKILL.md file.
|
|
103
|
+
*
|
|
104
|
+
* @param {object} opts
|
|
105
|
+
* @param {string} opts.filePath - Path to the file (used in messages).
|
|
106
|
+
* @param {string} opts.sourceText - Full file content.
|
|
107
|
+
* @param {object} opts.fm - Parsed frontmatter object.
|
|
108
|
+
* @param {number} [opts.emptyThreshold=50] - Minimum non-whitespace characters for
|
|
109
|
+
* a section to be considered non-empty.
|
|
110
|
+
*
|
|
111
|
+
* @returns {{
|
|
112
|
+
* errors: Array<{message: string, line: number, column: number, help: string}>,
|
|
113
|
+
* warnings: Array<{message: string, line: number, column: number, help: string}>
|
|
114
|
+
* }}
|
|
115
|
+
*/
|
|
116
|
+
function checkArchetypeSections(opts) {
|
|
117
|
+
const {
|
|
118
|
+
filePath,
|
|
119
|
+
sourceText,
|
|
120
|
+
fm,
|
|
121
|
+
emptyThreshold = 50,
|
|
122
|
+
} = opts;
|
|
123
|
+
|
|
124
|
+
const errors = [];
|
|
125
|
+
const warnings = [];
|
|
126
|
+
|
|
127
|
+
if (!fm || !fm.type) {
|
|
128
|
+
// Cannot validate without a type — schema check handles missing type.
|
|
129
|
+
return { errors, warnings };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const archetype = fm.type;
|
|
133
|
+
const required = REQUIRED_SECTIONS[archetype];
|
|
134
|
+
|
|
135
|
+
if (!required) {
|
|
136
|
+
// Unknown archetype — schema check handles this.
|
|
137
|
+
return { errors, warnings };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const sections = extractH2Sections(sourceText);
|
|
141
|
+
const presentHeadings = new Set(sections.map(s => s.heading));
|
|
142
|
+
|
|
143
|
+
// Error: missing required sections.
|
|
144
|
+
for (const req of required) {
|
|
145
|
+
if (!presentHeadings.has(req)) {
|
|
146
|
+
// Point at line 1 col 1 (frontmatter type: field would be ideal but
|
|
147
|
+
// locateYamlKey is in the parent; use line 1 as a safe fallback — the
|
|
148
|
+
// formatter still shows the file reference clearly).
|
|
149
|
+
errors.push({
|
|
150
|
+
message: `missing required section "## ${req}" for archetype "${archetype}"`,
|
|
151
|
+
line: 1,
|
|
152
|
+
column: 1,
|
|
153
|
+
help: `Add a "## ${req}" section. Required sections for ${archetype}: ${required.map(r => `"## ${r}"`).join(', ')}. See docs/skill-metadata-protocol.md § Archetype Section Map.`,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Conditional sections — required when frontmatter predicate holds.
|
|
159
|
+
for (const { section, predicate, help } of CONDITIONAL_SECTIONS) {
|
|
160
|
+
if (!predicate(fm)) continue;
|
|
161
|
+
if (presentHeadings.has(section)) continue;
|
|
162
|
+
errors.push({
|
|
163
|
+
message: `missing required section "## ${section}" (conditional: eval_artifacts is "${fm && fm.eval_artifacts}")`,
|
|
164
|
+
line: 1,
|
|
165
|
+
column: 1,
|
|
166
|
+
help,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Warn: sections present but empty.
|
|
171
|
+
for (const section of sections) {
|
|
172
|
+
if (section.contentLength < emptyThreshold) {
|
|
173
|
+
warnings.push({
|
|
174
|
+
message: `section "## ${section.heading}" exists but appears empty (${section.contentLength} non-whitespace chars, threshold ${emptyThreshold})`,
|
|
175
|
+
line: section.line,
|
|
176
|
+
column: 1,
|
|
177
|
+
help: `Fill in the "## ${section.heading}" section or remove it if it is not needed for this archetype.`,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return { errors, warnings };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
module.exports = { checkArchetypeSections, REQUIRED_SECTIONS };
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Category enum check for SKILL.md files (Check 13).
|
|
4
|
+
*
|
|
5
|
+
* The Skill Graph policy is that `category` functions as a **browse facet**,
|
|
6
|
+
* not ontology truth, and must take exactly one of six canonical values.
|
|
7
|
+
*
|
|
8
|
+
* Post-Phase-1 (v5 schema bump, skill-graph commit `f489641`, 2026-05-16):
|
|
9
|
+
* the canonical enum is closed at BOTH the schema level (`schemas/skill.schema.json`
|
|
10
|
+
* `category.enum`) AND this lint level. This file is now redundant-but-correct:
|
|
11
|
+
* the schema will reject invalid values first, and this lint provides a
|
|
12
|
+
* second-layer guarantee with a more descriptive error message and the
|
|
13
|
+
* authoritative definitions inline.
|
|
14
|
+
*
|
|
15
|
+
* Both sources of truth must stay in sync. A future protocol revision that
|
|
16
|
+
* adds (e.g.) a 7th category must update three places in the same commit:
|
|
17
|
+
* 1. `schemas/skill.schema.json` `category.enum`
|
|
18
|
+
* 2. `CATEGORY_ENUM` below
|
|
19
|
+
* 3. `docs/skill-metadata-protocol.md` § Category
|
|
20
|
+
*
|
|
21
|
+
* @module lint/check-category-enum
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
'use strict';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Canonical category enum — closed set as of v5 (2026-05-16).
|
|
28
|
+
*
|
|
29
|
+
* Mirror of `schemas/skill.schema.json` `category.enum`. Update both in the
|
|
30
|
+
* same commit when adding/removing values.
|
|
31
|
+
*
|
|
32
|
+
* Framed as a BROWSE FACET, not ontology truth. Cross-cutting truth
|
|
33
|
+
* (a skill that is both engineering and quality, for example) lives in
|
|
34
|
+
* `relations.related`, not in multiple category memberships.
|
|
35
|
+
*/
|
|
36
|
+
const CATEGORY_ENUM = Object.freeze([
|
|
37
|
+
'foundations', // Epistemics, grounding, verification, context engineering, reasoning
|
|
38
|
+
'engineering', // Building software systems
|
|
39
|
+
'design', // Visual, interaction, IA, content, motion
|
|
40
|
+
'quality', // Cross-cutting non-functional properties (a11y, perf, security, type-safety, testing)
|
|
41
|
+
'agent', // Agent-specific concepts (tool design, prompt design, agent state)
|
|
42
|
+
'product', // Prioritization, scope, MVP, PRDs, journeys
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Run the category-enum check on one SKILL.md file.
|
|
47
|
+
*
|
|
48
|
+
* @param {object} opts
|
|
49
|
+
* @param {string} opts.filePath - Path to the file (used in messages only).
|
|
50
|
+
* @param {string} opts.sourceText - Full file content.
|
|
51
|
+
* @param {object} opts.fm - Parsed frontmatter object.
|
|
52
|
+
*
|
|
53
|
+
* @returns {{
|
|
54
|
+
* errors: Array<{message: string, line: number, column: number, help: string}>,
|
|
55
|
+
* warnings: Array<{message: string, line: number, column: number, help: string}>
|
|
56
|
+
* }}
|
|
57
|
+
*/
|
|
58
|
+
function checkCategoryEnum(opts) {
|
|
59
|
+
const { sourceText, fm } = opts;
|
|
60
|
+
const errors = [];
|
|
61
|
+
const warnings = [];
|
|
62
|
+
|
|
63
|
+
if (!fm) return { errors, warnings };
|
|
64
|
+
if (typeof fm.category !== 'string') return { errors, warnings };
|
|
65
|
+
|
|
66
|
+
if (!CATEGORY_ENUM.includes(fm.category)) {
|
|
67
|
+
const lineMatch = sourceText.match(/^category\s*:.*$/m);
|
|
68
|
+
const lineNumber = lineMatch
|
|
69
|
+
? sourceText.substring(0, sourceText.indexOf(lineMatch[0])).split('\n').length
|
|
70
|
+
: 1;
|
|
71
|
+
|
|
72
|
+
errors.push({
|
|
73
|
+
message: `category "${fm.category}" is not in the canonical enum: ${CATEGORY_ENUM.join(', ')}`,
|
|
74
|
+
line: lineNumber,
|
|
75
|
+
column: 1,
|
|
76
|
+
help: `Pick one of: ${CATEGORY_ENUM.join(', ')}. If the skill is genuinely cross-cutting, primary-category it under its strongest fit and use relations.related for the others. See docs/skill-metadata-protocol.md § Category and docs/plans/skill-taxonomy-v5-and-gap-fill.md.`,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return { errors, warnings };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = { checkCategoryEnum, CATEGORY_ENUM };
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Routing-eval integrity check for SKILL.md files (lint check 12).
|
|
4
|
+
*
|
|
5
|
+
* **R3 — routing_eval: present must be executable (ERROR)**
|
|
6
|
+
* A skill that declares `routing_eval: present` is claiming its routing
|
|
7
|
+
* coverage has been evaluated. The authored fields `examples` and
|
|
8
|
+
* `anti_examples` (schema_version 3, v0.5.0) are what the evaluation runs
|
|
9
|
+
* against. This check enforces two things:
|
|
10
|
+
*
|
|
11
|
+
* 1. If `routing_eval: present`, the skill MUST declare both `examples`
|
|
12
|
+
* and `anti_examples` (non-empty). No input — no evaluation possible.
|
|
13
|
+
*
|
|
14
|
+
* 2. If `routing_eval: present`, running the harness
|
|
15
|
+
* (`scripts/skill-graph-routing-eval.js`) against this skill MUST
|
|
16
|
+
* return verdict=PASS. Every FAIL case surfaces as a lint error with
|
|
17
|
+
* the failing prompt.
|
|
18
|
+
*
|
|
19
|
+
* This is the gate that turns `routing_eval` from self-assertion into a
|
|
20
|
+
* verifiable claim. Before this check, authors could set `present` without
|
|
21
|
+
* any executable evidence; after it, `present` is lint-rejected until the
|
|
22
|
+
* harness agrees.
|
|
23
|
+
*
|
|
24
|
+
* @module lint/check-routing-eval
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
'use strict';
|
|
28
|
+
|
|
29
|
+
const fs = require('fs');
|
|
30
|
+
const path = require('path');
|
|
31
|
+
const { evaluateSkill } = require('../skill-graph-routing-eval');
|
|
32
|
+
const { packageRoot, workspaceRoot } = require('../lib/roots');
|
|
33
|
+
|
|
34
|
+
const REPO_ROOT = workspaceRoot();
|
|
35
|
+
const PACKAGE_ROOT = packageRoot();
|
|
36
|
+
const DEFAULT_MANIFEST = path.join(REPO_ROOT, 'skills.manifest.json');
|
|
37
|
+
const SAMPLE_MANIFEST = path.join(REPO_ROOT, 'examples', 'skills.manifest.sample.json');
|
|
38
|
+
const PACKAGE_SAMPLE_MANIFEST = path.join(PACKAGE_ROOT, 'examples', 'skills.manifest.sample.json');
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Run routing-eval checks on one SKILL.md file.
|
|
42
|
+
*
|
|
43
|
+
* @param {object} opts
|
|
44
|
+
* @param {string} opts.filePath - Path to the file (used in messages only).
|
|
45
|
+
* @param {string} opts.sourceText - Full file content.
|
|
46
|
+
* @param {object} opts.fm - Parsed frontmatter object.
|
|
47
|
+
* @param {object} [opts.manifest] - Optional pre-loaded manifest (performance).
|
|
48
|
+
*
|
|
49
|
+
* @returns {{
|
|
50
|
+
* errors: Array<{message: string, line: number, column: number, help: string}>,
|
|
51
|
+
* warnings: Array<{message: string, line: number, column: number, help: string}>
|
|
52
|
+
* }}
|
|
53
|
+
*/
|
|
54
|
+
function checkRoutingEval(opts) {
|
|
55
|
+
const { sourceText, fm } = opts;
|
|
56
|
+
const errors = [];
|
|
57
|
+
if (!fm) return { errors, warnings: [] };
|
|
58
|
+
if (fm.routing_eval !== 'present') return { errors, warnings: [] };
|
|
59
|
+
|
|
60
|
+
const keyLine = locateKey(sourceText, 'routing_eval') || { line: 1, column: 1 };
|
|
61
|
+
|
|
62
|
+
// Guard 1: examples + anti_examples must exist.
|
|
63
|
+
const hasExamples = Array.isArray(fm.examples) && fm.examples.length > 0;
|
|
64
|
+
const hasAnti = Array.isArray(fm.anti_examples) && fm.anti_examples.length > 0;
|
|
65
|
+
if (!hasExamples || !hasAnti) {
|
|
66
|
+
errors.push({
|
|
67
|
+
message: `routing_eval: present without populated examples + anti_examples — the harness has no prompts to evaluate`,
|
|
68
|
+
line: keyLine.line,
|
|
69
|
+
column: keyLine.column,
|
|
70
|
+
help: 'Either populate examples and anti_examples (see docs/field-reference.md § examples and § anti_examples) or set routing_eval to "absent".',
|
|
71
|
+
});
|
|
72
|
+
return { errors, warnings: [] };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Guard 2: harness must pass.
|
|
76
|
+
const manifest = opts.manifest || loadManifest();
|
|
77
|
+
if (!manifest) {
|
|
78
|
+
errors.push({
|
|
79
|
+
message: `routing_eval: present but no manifest is available for the harness — run generate-manifest.js first`,
|
|
80
|
+
line: keyLine.line,
|
|
81
|
+
column: keyLine.column,
|
|
82
|
+
help: 'Run `node scripts/generate-manifest.js --output skills.manifest.json` to produce the manifest the harness evaluates against.',
|
|
83
|
+
});
|
|
84
|
+
return { errors, warnings: [] };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const skillEntry = (manifest.skills || []).find(s => s.name === fm.name);
|
|
88
|
+
if (!skillEntry) {
|
|
89
|
+
errors.push({
|
|
90
|
+
message: `routing_eval: present but skill "${fm.name}" is not in the manifest — regenerate the manifest`,
|
|
91
|
+
line: keyLine.line,
|
|
92
|
+
column: keyLine.column,
|
|
93
|
+
help: 'The skill must appear in the manifest so the harness can resolve its activation.examples / anti_examples.',
|
|
94
|
+
});
|
|
95
|
+
return { errors, warnings: [] };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const todayISO = new Date().toISOString().slice(0, 10);
|
|
99
|
+
const report = evaluateSkill(manifest, skillEntry, todayISO);
|
|
100
|
+
|
|
101
|
+
if (report.verdict === 'FAIL') {
|
|
102
|
+
for (const c of report.cases) {
|
|
103
|
+
if (c.verdict !== 'FAIL') continue;
|
|
104
|
+
errors.push({
|
|
105
|
+
message: `routing_eval: present but [${c.kind}] "${truncate(c.prompt, 72)}" fails — ${c.reason}`,
|
|
106
|
+
line: keyLine.line,
|
|
107
|
+
column: keyLine.column,
|
|
108
|
+
help: 'Either fix the routing signal (keywords / boundary / anti_example) until the harness passes, or set routing_eval to "absent" until you do. Run `node scripts/skill-graph-routing-eval.js --skill ' + fm.name + '` for full diagnostics.',
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return { errors, warnings: [] };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function loadManifest() {
|
|
117
|
+
const p = fs.existsSync(DEFAULT_MANIFEST)
|
|
118
|
+
? DEFAULT_MANIFEST
|
|
119
|
+
: (fs.existsSync(SAMPLE_MANIFEST) ? SAMPLE_MANIFEST : PACKAGE_SAMPLE_MANIFEST);
|
|
120
|
+
if (!fs.existsSync(p)) return null;
|
|
121
|
+
try { return JSON.parse(fs.readFileSync(p, 'utf8')); }
|
|
122
|
+
catch { return null; }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function locateKey(sourceText, key) {
|
|
126
|
+
const lines = sourceText.split('\n');
|
|
127
|
+
let dashCount = 0;
|
|
128
|
+
let inside = false;
|
|
129
|
+
for (let i = 0; i < lines.length; i++) {
|
|
130
|
+
if (lines[i].trim() === '---') {
|
|
131
|
+
dashCount++;
|
|
132
|
+
if (dashCount === 1) { inside = true; continue; }
|
|
133
|
+
if (dashCount === 2) break;
|
|
134
|
+
}
|
|
135
|
+
if (!inside) continue;
|
|
136
|
+
const m = lines[i].match(new RegExp(`^(\\s*)${key}\\s*:`));
|
|
137
|
+
if (m) return { line: i + 1, column: m[1].length + 1 };
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function truncate(s, n) {
|
|
143
|
+
return s.length <= n ? s : s.slice(0, n - 1) + '\u2026';
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
module.exports = { checkRoutingEval };
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Routing quality checks for SKILL.md files (A2 narrow).
|
|
4
|
+
*
|
|
5
|
+
* Only "defensible" heuristics are implemented here — checks with low false-
|
|
6
|
+
* positive risk and clear remediation paths. Length-threshold heuristics for
|
|
7
|
+
* description or coverage are intentionally omitted (high false-positive risk
|
|
8
|
+
* on legitimately terse skills).
|
|
9
|
+
*
|
|
10
|
+
* **Check R1 — empty keywords in operational scope (ERROR)**
|
|
11
|
+
* A skill with `scope: codebase` or any `routing_bundles` entry is meant to
|
|
12
|
+
* be surfaced by a router. An empty `keywords: []` (or no keywords field)
|
|
13
|
+
* means the skill cannot be discovered by keyword-based routers. This is
|
|
14
|
+
* almost always an authoring mistake.
|
|
15
|
+
*
|
|
16
|
+
* Note: the task spec text says "operational scope" but after SH-5784 the
|
|
17
|
+
* v2 scope value is `codebase`. This check targets `scope: codebase` OR
|
|
18
|
+
* skills that have a non-empty `routing_bundles` array, matching the intent
|
|
19
|
+
* of the original proposal.
|
|
20
|
+
*
|
|
21
|
+
* **Check R2 — description verbatim in Coverage section (WARN)**
|
|
22
|
+
* The description field is a routing contract (≤3 sentences) and `## Coverage`
|
|
23
|
+
* is a scope map (a bulleted topic list). When the description text appears
|
|
24
|
+
* verbatim inside `## Coverage`, the two layers have collapsed into one and
|
|
25
|
+
* the Coverage section adds no information for a reader who already read the
|
|
26
|
+
* frontmatter. This is always a copy-paste mistake.
|
|
27
|
+
*
|
|
28
|
+
* @module lint/check-routing-quality
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
'use strict';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Run routing-quality checks on one SKILL.md file.
|
|
35
|
+
*
|
|
36
|
+
* @param {object} opts
|
|
37
|
+
* @param {string} opts.filePath - Path to the file (used in messages only).
|
|
38
|
+
* @param {string} opts.sourceText - Full file content.
|
|
39
|
+
* @param {object} opts.fm - Parsed frontmatter object.
|
|
40
|
+
*
|
|
41
|
+
* @returns {{
|
|
42
|
+
* errors: Array<{message: string, line: number, column: number, help: string}>,
|
|
43
|
+
* warnings: Array<{message: string, line: number, column: number, help: string}>
|
|
44
|
+
* }}
|
|
45
|
+
*/
|
|
46
|
+
function checkRoutingQuality(opts) {
|
|
47
|
+
const { filePath, sourceText, fm } = opts;
|
|
48
|
+
|
|
49
|
+
const errors = [];
|
|
50
|
+
const warnings = [];
|
|
51
|
+
|
|
52
|
+
if (!fm) return { errors, warnings };
|
|
53
|
+
|
|
54
|
+
// ------------------------------------------------------------------ R1
|
|
55
|
+
// Error: empty keywords for scope: codebase or routing_bundles-having skills.
|
|
56
|
+
const isCodebaseScope = fm.scope === 'codebase';
|
|
57
|
+
const hasRoutingGroups = Array.isArray(fm.routing_bundles) && fm.routing_bundles.length > 0;
|
|
58
|
+
// The custom frontmatter parser represents `keywords: []` as the string "[]"
|
|
59
|
+
// (inline YAML array syntax). Treat that as empty as well.
|
|
60
|
+
const keywordsEmpty = !fm.keywords
|
|
61
|
+
|| fm.keywords === '[]'
|
|
62
|
+
|| (Array.isArray(fm.keywords) && fm.keywords.length === 0);
|
|
63
|
+
|
|
64
|
+
if ((isCodebaseScope || hasRoutingGroups) && keywordsEmpty) {
|
|
65
|
+
// Locate the `keywords` key in the frontmatter if it exists, else point at
|
|
66
|
+
// the `scope` key.
|
|
67
|
+
const keyLine = locateKeyInFrontmatter(sourceText, 'keywords')
|
|
68
|
+
|| locateKeyInFrontmatter(sourceText, 'scope')
|
|
69
|
+
|| { line: 1, column: 1 };
|
|
70
|
+
|
|
71
|
+
const scopeReason = isCodebaseScope
|
|
72
|
+
? 'scope: codebase'
|
|
73
|
+
: `routing_bundles: [${fm.routing_bundles.join(', ')}]`;
|
|
74
|
+
|
|
75
|
+
errors.push({
|
|
76
|
+
message: `keywords: [] for a skill with ${scopeReason} — router cannot discover this skill`,
|
|
77
|
+
line: keyLine.line,
|
|
78
|
+
column: keyLine.column,
|
|
79
|
+
help: 'Add at least one keyword to the keywords list. Skills with scope: codebase or routing_bundles must be discoverable by keyword routers. See docs/field-reference.md § keywords.',
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ------------------------------------------------------------------ R2
|
|
84
|
+
// Warn: description text appears verbatim inside ## Coverage section.
|
|
85
|
+
const description = typeof fm.description === 'string' ? fm.description.trim() : '';
|
|
86
|
+
if (description.length > 20) {
|
|
87
|
+
const coverageContent = extractSectionContent(sourceText, 'Coverage');
|
|
88
|
+
if (coverageContent !== null && coverageContent.includes(description)) {
|
|
89
|
+
const sectionLine = locateH2InBody(sourceText, 'Coverage');
|
|
90
|
+
warnings.push({
|
|
91
|
+
message: 'description text appears verbatim in ## Coverage — the two layers have collapsed into one',
|
|
92
|
+
line: sectionLine ? sectionLine.line : 1,
|
|
93
|
+
column: 1,
|
|
94
|
+
help: 'The description is the routing contract (≤3 sentences). ## Coverage is a bulleted scope map. Rewrite ## Coverage as a topic list, removing the copied description sentence. See docs/skill-metadata-protocol.md § Semantic layer discipline.',
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { errors, warnings };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// Internal helpers
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Locate a YAML key inside the frontmatter block.
|
|
108
|
+
* Returns { line, column } (1-based) or null if not found.
|
|
109
|
+
*
|
|
110
|
+
* @param {string} sourceText
|
|
111
|
+
* @param {string} key
|
|
112
|
+
* @returns {{ line: number, column: number }|null}
|
|
113
|
+
*/
|
|
114
|
+
function locateKeyInFrontmatter(sourceText, key) {
|
|
115
|
+
const lines = sourceText.split('\n');
|
|
116
|
+
// Only search inside the frontmatter block (between the two `---` markers).
|
|
117
|
+
let inFrontmatter = false;
|
|
118
|
+
let dashCount = 0;
|
|
119
|
+
for (let i = 0; i < lines.length; i++) {
|
|
120
|
+
if (lines[i].trim() === '---') {
|
|
121
|
+
dashCount++;
|
|
122
|
+
if (dashCount === 1) { inFrontmatter = true; continue; }
|
|
123
|
+
if (dashCount === 2) break; // end of frontmatter
|
|
124
|
+
}
|
|
125
|
+
if (!inFrontmatter) continue;
|
|
126
|
+
const m = lines[i].match(new RegExp(`^(\\s*)${escapeRegex(key)}\\s*:`));
|
|
127
|
+
if (m) {
|
|
128
|
+
return { line: i + 1, column: m[1].length + 1 };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Extract the text content of a named H2 section from the markdown body
|
|
136
|
+
* (after the closing `---`).
|
|
137
|
+
*
|
|
138
|
+
* @param {string} sourceText
|
|
139
|
+
* @param {string} heading - Heading text without `## ` prefix.
|
|
140
|
+
* @returns {string|null} Section content, or null if section not found.
|
|
141
|
+
*/
|
|
142
|
+
function extractSectionContent(sourceText, heading) {
|
|
143
|
+
const lines = sourceText.split('\n');
|
|
144
|
+
|
|
145
|
+
// Skip past frontmatter.
|
|
146
|
+
let bodyStart = 0;
|
|
147
|
+
let dashCount = 0;
|
|
148
|
+
for (let i = 0; i < lines.length; i++) {
|
|
149
|
+
if (lines[i].trim() === '---') {
|
|
150
|
+
dashCount++;
|
|
151
|
+
if (dashCount === 2) { bodyStart = i + 1; break; }
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const target = `## ${heading}`;
|
|
156
|
+
let collecting = false;
|
|
157
|
+
let content = [];
|
|
158
|
+
|
|
159
|
+
for (let i = bodyStart; i < lines.length; i++) {
|
|
160
|
+
if (lines[i].trimEnd() === target) {
|
|
161
|
+
collecting = true;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (collecting) {
|
|
165
|
+
// Stop at the next H2.
|
|
166
|
+
if (/^## /.test(lines[i])) break;
|
|
167
|
+
content.push(lines[i]);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return collecting ? content.join('\n') : null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Locate an H2 heading in the body (after frontmatter).
|
|
176
|
+
* Returns { line } (1-based) or null.
|
|
177
|
+
*
|
|
178
|
+
* @param {string} sourceText
|
|
179
|
+
* @param {string} heading
|
|
180
|
+
* @returns {{ line: number }|null}
|
|
181
|
+
*/
|
|
182
|
+
function locateH2InBody(sourceText, heading) {
|
|
183
|
+
const lines = sourceText.split('\n');
|
|
184
|
+
let dashCount = 0;
|
|
185
|
+
let pastFrontmatter = false;
|
|
186
|
+
const target = `## ${heading}`;
|
|
187
|
+
for (let i = 0; i < lines.length; i++) {
|
|
188
|
+
if (!pastFrontmatter) {
|
|
189
|
+
if (lines[i].trim() === '---') {
|
|
190
|
+
dashCount++;
|
|
191
|
+
if (dashCount === 2) pastFrontmatter = true;
|
|
192
|
+
}
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (lines[i].trimEnd() === target) {
|
|
196
|
+
return { line: i + 1 };
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Escape special regex characters.
|
|
204
|
+
* @param {string} s
|
|
205
|
+
* @returns {string}
|
|
206
|
+
*/
|
|
207
|
+
function escapeRegex(s) {
|
|
208
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
module.exports = { checkRoutingQuality };
|