@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,766 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* skill-graph route — the reference consumer for Skill Metadata Protocol metadata.
|
|
4
|
+
*
|
|
5
|
+
* Reads a compiled manifest (`skills.manifest.json` or
|
|
6
|
+
* `examples/skills.manifest.sample.json`) and picks skills for a natural-language
|
|
7
|
+
* query using every graph field that differentiates Skill Graph from plain
|
|
8
|
+
* SKILL.md:
|
|
9
|
+
*
|
|
10
|
+
* - activation.keywords / activation.triggers — scoring signal
|
|
11
|
+
* - activation.paths — optional `--path` boost
|
|
12
|
+
* - workspace_tags + workspace.projects — project-scope filter
|
|
13
|
+
* - relations.depends_on — transitive co-load
|
|
14
|
+
* - relations.boundary — anti-ownership exclusion
|
|
15
|
+
* - relations.verify_with — secondary co-load
|
|
16
|
+
* - health.eval_state — quality gate
|
|
17
|
+
* - health.drift_check + health.lifecycle — staleness annotation
|
|
18
|
+
*
|
|
19
|
+
* The output explains WHY each skill was selected or excluded. That is the
|
|
20
|
+
* point: the reference consumer exists so `boundary` and `depends_on` and
|
|
21
|
+
* `eval_state` are visible in a routing decision, not just declared in a
|
|
22
|
+
* frontmatter nobody reads.
|
|
23
|
+
*
|
|
24
|
+
* Usage:
|
|
25
|
+
* node scripts/skill-graph-route.js "accessibility keyboard navigation"
|
|
26
|
+
* node scripts/skill-graph-route.js "refactor" --project <your-project>
|
|
27
|
+
* node scripts/skill-graph-route.js "ssr hydration" --max 5
|
|
28
|
+
* node scripts/skill-graph-route.js "types" --min-eval-state passing
|
|
29
|
+
* node scripts/skill-graph-route.js "css" --path src/components/Header.tsx
|
|
30
|
+
* node scripts/skill-graph-route.js --manifest examples/skills.manifest.sample.json "css"
|
|
31
|
+
* node scripts/skill-graph-route.js --json "css"
|
|
32
|
+
*
|
|
33
|
+
* Self-contained. Only uses Node built-ins — no external dependencies.
|
|
34
|
+
* Exit 0 on success, 1 on manifest load failure or no query.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
'use strict';
|
|
38
|
+
|
|
39
|
+
const fs = require('fs');
|
|
40
|
+
const path = require('path');
|
|
41
|
+
const { packageRoot, workspaceRoot } = require('./lib/roots');
|
|
42
|
+
|
|
43
|
+
const REPO_ROOT = workspaceRoot();
|
|
44
|
+
const PACKAGE_ROOT = packageRoot();
|
|
45
|
+
const DEFAULT_MANIFEST = path.join(REPO_ROOT, 'skills.manifest.json');
|
|
46
|
+
const SAMPLE_MANIFEST = path.join(REPO_ROOT, 'examples', 'skills.manifest.sample.json');
|
|
47
|
+
const PACKAGE_SAMPLE_MANIFEST = path.join(PACKAGE_ROOT, 'examples', 'skills.manifest.sample.json');
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Tokenization & scoring
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* English function-word stopwords. Dropped from both query and keyword
|
|
55
|
+
* tokenization so that matches are carried by content tokens, not by
|
|
56
|
+
* pronouns / articles / auxiliaries / WH-words that appear in almost every
|
|
57
|
+
* natural-language prompt.
|
|
58
|
+
*
|
|
59
|
+
* Without this set, a query like "fix this" exact-matches any keyword phrase
|
|
60
|
+
* containing "this", driving library-wide false positives. See
|
|
61
|
+
* `docs/plans/routing-harness-followup.md` § M1.
|
|
62
|
+
*/
|
|
63
|
+
/**
|
|
64
|
+
* Scope tiebreaker ranks (lower wins). Doctrine: a skill bound to a specific
|
|
65
|
+
* codebase is always more specific than a reference skill, which is always
|
|
66
|
+
* more specific than a portable skill. Used in `routeSkills()` sort. Unknown
|
|
67
|
+
* scopes fall back to `_default` so a manifest with a new scope value sorts
|
|
68
|
+
* last rather than throwing.
|
|
69
|
+
*/
|
|
70
|
+
const SCOPE_RANK = { codebase: 0, reference: 1, portable: 2, _default: 99 };
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Type tiebreaker ranks (lower wins). Doctrine: workflow > capability >
|
|
74
|
+
* router > overlay. Matches `skills/skill-router/SKILL.md § Type tiebreaker`.
|
|
75
|
+
*/
|
|
76
|
+
const TYPE_RANK = { workflow: 0, capability: 1, router: 2, overlay: 3, _default: 99 };
|
|
77
|
+
|
|
78
|
+
const STOPWORDS = new Set([
|
|
79
|
+
'a', 'an', 'the', 'this', 'that', 'these', 'those',
|
|
80
|
+
'to', 'of', 'in', 'on', 'at', 'by', 'for', 'with', 'from', 'as', 'if',
|
|
81
|
+
'is', 'are', 'was', 'were', 'be', 'been', 'being',
|
|
82
|
+
'it', 'its', 'my', 'our', 'your', 'their', 'his', 'her',
|
|
83
|
+
'do', 'does', 'did', 'can', 'could', 'may', 'might', 'should', 'would',
|
|
84
|
+
'have', 'has', 'had',
|
|
85
|
+
'how', 'when', 'where', 'what', 'why', 'who', 'which',
|
|
86
|
+
'and', 'or', 'not', 'but', 'so', 'then',
|
|
87
|
+
]);
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Split a query or keyword into lowercase content tokens.
|
|
91
|
+
*
|
|
92
|
+
* A token must be length ≥ 2 and NOT a stopword. The stopword filter applies
|
|
93
|
+
* equally on both sides of the comparison (query and keyword), so dropping a
|
|
94
|
+
* token from one side implicitly drops it from the other. The keyword side
|
|
95
|
+
* additionally enforces length ≥ 3 in `scoreSkill()` to prevent short-token
|
|
96
|
+
* false positives surviving stopword removal (e.g. "up", "it" in phrases
|
|
97
|
+
* like "set up the …").
|
|
98
|
+
*/
|
|
99
|
+
function tokenize(text) {
|
|
100
|
+
return String(text || '')
|
|
101
|
+
.toLowerCase()
|
|
102
|
+
.split(/[^a-z0-9]+/)
|
|
103
|
+
.filter(t => t.length >= 2 && !STOPWORDS.has(t));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Score a skill's activation against a query. Higher is better.
|
|
108
|
+
*
|
|
109
|
+
* Signals, in decreasing weight:
|
|
110
|
+
* - trigger exact match: 5
|
|
111
|
+
* - keyword exact-token match: 3 (each query token credited at most ONCE
|
|
112
|
+
* per skill, regardless of how many keyword phrases contain it —
|
|
113
|
+
* prevents keyword-bag stuffing, see M2 in the follow-up plan)
|
|
114
|
+
* - keyword substring match: 1 (per distinct query token, also deduped
|
|
115
|
+
* against tokens already credited as exact matches)
|
|
116
|
+
* - path match on --path arg: 2
|
|
117
|
+
*
|
|
118
|
+
* Returns { score, matchedBecause } where matchedBecause is a short tag list
|
|
119
|
+
* used to explain the routing decision back to the user.
|
|
120
|
+
*/
|
|
121
|
+
function scoreSkill(skill, queryTokens, pathArg) {
|
|
122
|
+
let score = 0;
|
|
123
|
+
const reasons = [];
|
|
124
|
+
|
|
125
|
+
const activation = skill.activation || {};
|
|
126
|
+
const triggers = activation.triggers || [];
|
|
127
|
+
const keywords = activation.keywords || [];
|
|
128
|
+
const paths = activation.paths || [];
|
|
129
|
+
|
|
130
|
+
// Triggers: exact match on the entire query OR on any word boundary.
|
|
131
|
+
const queryJoined = queryTokens.join(' ');
|
|
132
|
+
for (const trigger of triggers) {
|
|
133
|
+
const t = String(trigger).toLowerCase();
|
|
134
|
+
if (t === queryJoined || queryTokens.includes(t)) {
|
|
135
|
+
score += 5;
|
|
136
|
+
reasons.push(`trigger:${t}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Keywords scored in TWO passes, each with its own per-token dedup set:
|
|
141
|
+
//
|
|
142
|
+
// Pass 1 (exact): each query token earns +3 AT MOST ONCE per skill,
|
|
143
|
+
// regardless of how many keyword phrases contain it.
|
|
144
|
+
// Prevents keyword-bag stuffing (M2 in the follow-up
|
|
145
|
+
// plan) — e.g. a skill with six phrases all containing
|
|
146
|
+
// "skill" can no longer beat a skill with two phrases
|
|
147
|
+
// but more precise content.
|
|
148
|
+
//
|
|
149
|
+
// Pass 2 (substring): each query token earns +1 AT MOST ONCE per skill,
|
|
150
|
+
// and ONLY if it did not already earn an exact match
|
|
151
|
+
// in pass 1. Running substring in a second pass —
|
|
152
|
+
// rather than falling back per-keyword inside pass 1
|
|
153
|
+
// — is the critical fix for the "cleanup" vs
|
|
154
|
+
// "clean this up" interaction. A substring credit on
|
|
155
|
+
// "clean" from the phrase "cleanup" must not poison a
|
|
156
|
+
// later exact match on "clean" from the phrase
|
|
157
|
+
// "clean this up". Separate sets keep the two dedups
|
|
158
|
+
// independent.
|
|
159
|
+
const exactMatchedTokens = new Set();
|
|
160
|
+
for (const keyword of keywords) {
|
|
161
|
+
const kwTokens = tokenize(keyword).filter(t => t.length >= 3);
|
|
162
|
+
for (const kw of kwTokens) {
|
|
163
|
+
if (queryTokens.includes(kw) && !exactMatchedTokens.has(kw)) {
|
|
164
|
+
exactMatchedTokens.add(kw);
|
|
165
|
+
score += 3;
|
|
166
|
+
reasons.push(`keyword:${keyword}`);
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const substringMatchedTokens = new Set();
|
|
173
|
+
for (const keyword of keywords) {
|
|
174
|
+
const full = String(keyword).toLowerCase();
|
|
175
|
+
for (const q of queryTokens) {
|
|
176
|
+
if (
|
|
177
|
+
q.length >= 3 &&
|
|
178
|
+
!exactMatchedTokens.has(q) &&
|
|
179
|
+
!substringMatchedTokens.has(q) &&
|
|
180
|
+
full.includes(q)
|
|
181
|
+
) {
|
|
182
|
+
substringMatchedTokens.add(q);
|
|
183
|
+
score += 1;
|
|
184
|
+
reasons.push(`~keyword:${keyword}`);
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Path match: if the caller passed --path, boost skills whose positive path
|
|
191
|
+
// list matches after gitignore-style negations have been applied.
|
|
192
|
+
if (pathArg) {
|
|
193
|
+
const pathMatch = matchPathList(pathArg, paths);
|
|
194
|
+
if (pathMatch.matched) {
|
|
195
|
+
score += 2;
|
|
196
|
+
reasons.push(`path:${pathMatch.pattern}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return { score, reasons };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Dependency-free glob support for activation.paths.
|
|
205
|
+
* Paths are matched against posix-style separators.
|
|
206
|
+
*
|
|
207
|
+
* `matchesGlob()` answers whether one concrete glob matches one path.
|
|
208
|
+
* `matchPathList()` applies authored list semantics, where `!pattern` only
|
|
209
|
+
* subtracts from prior positive includes.
|
|
210
|
+
*/
|
|
211
|
+
function expandBraces(pattern) {
|
|
212
|
+
const match = String(pattern).match(/\{([^{}]+)\}/);
|
|
213
|
+
if (!match) return [String(pattern)];
|
|
214
|
+
const before = pattern.slice(0, match.index);
|
|
215
|
+
const after = pattern.slice(match.index + match[0].length);
|
|
216
|
+
return match[1].split(',').flatMap(part => expandBraces(before + part + after));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function globToRegExp(pattern) {
|
|
220
|
+
const pat = String(pattern).replace(/\\/g, '/');
|
|
221
|
+
let out = '^';
|
|
222
|
+
for (let i = 0; i < pat.length; i++) {
|
|
223
|
+
const ch = pat[i];
|
|
224
|
+
if (ch === '*') {
|
|
225
|
+
if (pat[i + 1] === '*') {
|
|
226
|
+
if (pat[i + 2] === '/') {
|
|
227
|
+
out += '(?:.*/)?';
|
|
228
|
+
i += 2;
|
|
229
|
+
} else {
|
|
230
|
+
out += '.*';
|
|
231
|
+
i += 1;
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
out += '[^/]*';
|
|
235
|
+
}
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (ch === '?') {
|
|
239
|
+
out += '[^/]';
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
out += ch.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
243
|
+
}
|
|
244
|
+
return new RegExp(out + '$');
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function matchesGlob(filePath, pattern) {
|
|
248
|
+
let pat = pattern;
|
|
249
|
+
if (typeof pat !== 'string' || pat.length === 0) return false;
|
|
250
|
+
if (pat.startsWith('!')) pat = pat.slice(1);
|
|
251
|
+
const normalizedPath = String(filePath).replace(/\\/g, '/');
|
|
252
|
+
return expandBraces(pat).some(expanded => globToRegExp(expanded).test(normalizedPath));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function matchPathList(filePath, patterns) {
|
|
256
|
+
let matched = false;
|
|
257
|
+
let matchedPattern = null;
|
|
258
|
+
let excludedBy = null;
|
|
259
|
+
if (!Array.isArray(patterns)) return { matched, pattern: null, excludedBy: null };
|
|
260
|
+
|
|
261
|
+
for (const pattern of patterns) {
|
|
262
|
+
if (typeof pattern !== 'string' || pattern.length === 0) continue;
|
|
263
|
+
const negated = pattern.startsWith('!');
|
|
264
|
+
if (!matchesGlob(filePath, pattern)) continue;
|
|
265
|
+
if (negated) {
|
|
266
|
+
if (matched) {
|
|
267
|
+
matched = false;
|
|
268
|
+
excludedBy = pattern;
|
|
269
|
+
}
|
|
270
|
+
} else {
|
|
271
|
+
matched = true;
|
|
272
|
+
matchedPattern = pattern;
|
|
273
|
+
excludedBy = null;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return { matched, pattern: matched ? matchedPattern : null, excludedBy };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ---------------------------------------------------------------------------
|
|
281
|
+
// Relation helpers
|
|
282
|
+
// ---------------------------------------------------------------------------
|
|
283
|
+
|
|
284
|
+
/** Extract a skill name from a v2 bare-string or v3 {skill, ...} relation item. */
|
|
285
|
+
function relItemName(item) {
|
|
286
|
+
if (typeof item === 'string') return item;
|
|
287
|
+
if (item && typeof item === 'object' && typeof item.skill === 'string') return item.skill;
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Extract a human-readable reason from a v3 boundary item, if present. */
|
|
292
|
+
function boundaryReason(item) {
|
|
293
|
+
if (item && typeof item === 'object' && typeof item.reason === 'string') return item.reason;
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// ---------------------------------------------------------------------------
|
|
298
|
+
// Project-tag matching
|
|
299
|
+
// ---------------------------------------------------------------------------
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Decide whether a skill applies to a given project handle, using the
|
|
303
|
+
* workspace config's semantic-tag mapping to expand the project into a set
|
|
304
|
+
* of matchable tags.
|
|
305
|
+
*
|
|
306
|
+
* A skill matches when:
|
|
307
|
+
* - it has no workspace_tags (ambient / cross-project), OR
|
|
308
|
+
* - any tag in workspace_tags matches the literal project handle, OR
|
|
309
|
+
* - any tag in workspace_tags matches one of the project's semantic_tags.
|
|
310
|
+
*/
|
|
311
|
+
function skillAppliesToProject(skill, project, workspace) {
|
|
312
|
+
const tags = skill.workspace_tags || [];
|
|
313
|
+
if (tags.length === 0) return { applies: true, reason: 'ambient' };
|
|
314
|
+
if (!project) return { applies: true, reason: 'no project filter active' };
|
|
315
|
+
|
|
316
|
+
if (tags.includes(project)) return { applies: true, reason: `literal:${project}` };
|
|
317
|
+
|
|
318
|
+
const semanticTags = (workspace && workspace.projects && workspace.projects[project] && workspace.projects[project].semantic_tags) || [];
|
|
319
|
+
for (const tag of tags) {
|
|
320
|
+
if (semanticTags.includes(tag)) return { applies: true, reason: `semantic:${tag}` };
|
|
321
|
+
}
|
|
322
|
+
return { applies: false, reason: `workspace_tags [${tags.join(', ')}] exclude project "${project}"` };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ---------------------------------------------------------------------------
|
|
326
|
+
// Staleness detection
|
|
327
|
+
// ---------------------------------------------------------------------------
|
|
328
|
+
|
|
329
|
+
function daysBetween(isoA, isoB) {
|
|
330
|
+
const a = new Date(isoA).getTime();
|
|
331
|
+
const b = new Date(isoB).getTime();
|
|
332
|
+
if (isNaN(a) || isNaN(b)) return null;
|
|
333
|
+
return Math.floor((b - a) / 86400000);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Compute staleness for a skill: { stale: boolean, days: number|null }.
|
|
338
|
+
*
|
|
339
|
+
* A skill is stale when lifecycle.stale_after_days is declared AND the days
|
|
340
|
+
* elapsed since drift_check.last_verified exceed that limit.
|
|
341
|
+
*/
|
|
342
|
+
function computeStaleness(skill, today) {
|
|
343
|
+
const health = skill.health || {};
|
|
344
|
+
const lifecycle = (health.lifecycle) || {};
|
|
345
|
+
const driftCheck = health.drift_check;
|
|
346
|
+
if (!lifecycle.stale_after_days || !driftCheck || !driftCheck.last_verified) {
|
|
347
|
+
return { stale: false, days: null };
|
|
348
|
+
}
|
|
349
|
+
const days = daysBetween(driftCheck.last_verified, today);
|
|
350
|
+
if (days === null) return { stale: false, days: null };
|
|
351
|
+
return { stale: days > lifecycle.stale_after_days, days };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// ---------------------------------------------------------------------------
|
|
355
|
+
// Routing pipeline
|
|
356
|
+
// ---------------------------------------------------------------------------
|
|
357
|
+
|
|
358
|
+
function routeSkills(manifest, options) {
|
|
359
|
+
const {
|
|
360
|
+
query,
|
|
361
|
+
project,
|
|
362
|
+
maxResults,
|
|
363
|
+
minEvalState,
|
|
364
|
+
pathArg,
|
|
365
|
+
todayISO,
|
|
366
|
+
} = options;
|
|
367
|
+
|
|
368
|
+
const skills = Array.isArray(manifest.skills) ? manifest.skills : [];
|
|
369
|
+
const workspace = manifest.workspace || null;
|
|
370
|
+
const queryTokens = tokenize(query);
|
|
371
|
+
|
|
372
|
+
const byName = new Map();
|
|
373
|
+
for (const s of skills) byName.set(s.name, s);
|
|
374
|
+
|
|
375
|
+
// -------------------------------------------------------------------------
|
|
376
|
+
// Stage 1: score every skill and filter by project.
|
|
377
|
+
// -------------------------------------------------------------------------
|
|
378
|
+
const scored = [];
|
|
379
|
+
const excludedByProject = [];
|
|
380
|
+
|
|
381
|
+
for (const skill of skills) {
|
|
382
|
+
const projectCheck = skillAppliesToProject(skill, project, workspace);
|
|
383
|
+
if (!projectCheck.applies) {
|
|
384
|
+
excludedByProject.push({ skill, reason: projectCheck.reason });
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
const { score, reasons } = scoreSkill(skill, queryTokens, pathArg);
|
|
388
|
+
if (score > 0) {
|
|
389
|
+
scored.push({ skill, score, reasons, role: 'match', projectMatch: projectCheck.reason });
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (scored.length === 0) {
|
|
394
|
+
return {
|
|
395
|
+
selected: [],
|
|
396
|
+
coLoaded: [],
|
|
397
|
+
excluded: [],
|
|
398
|
+
excludedByProject,
|
|
399
|
+
notes: ['no skills matched the query'],
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Tiebreakers implement the doctrine documented in
|
|
404
|
+
// `skills/skill-router/SKILL.md § Scope tiebreaker` and `§ Type tiebreaker`:
|
|
405
|
+
// 1. Highest score wins.
|
|
406
|
+
// 2. On a score tie, narrower scope wins: codebase > reference > portable.
|
|
407
|
+
// 3. On a scope tie, more specific type wins: workflow > capability > router > overlay.
|
|
408
|
+
// 4. On a complete tie, alphabetical by name (stable, deterministic output).
|
|
409
|
+
scored.sort((a, b) =>
|
|
410
|
+
(b.score - a.score) ||
|
|
411
|
+
(SCOPE_RANK[a.skill.scope] ?? SCOPE_RANK._default) - (SCOPE_RANK[b.skill.scope] ?? SCOPE_RANK._default) ||
|
|
412
|
+
(TYPE_RANK[a.skill.type] ?? TYPE_RANK._default) - (TYPE_RANK[b.skill.type] ?? TYPE_RANK._default) ||
|
|
413
|
+
a.skill.name.localeCompare(b.skill.name)
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
// -------------------------------------------------------------------------
|
|
417
|
+
// Stage 2: top-N matches seed the selection set.
|
|
418
|
+
// -------------------------------------------------------------------------
|
|
419
|
+
const topMatches = scored.slice(0, maxResults);
|
|
420
|
+
const selectedNames = new Set(topMatches.map(e => e.skill.name));
|
|
421
|
+
|
|
422
|
+
// -------------------------------------------------------------------------
|
|
423
|
+
// Stage 3: expand via depends_on transitive closure.
|
|
424
|
+
// -------------------------------------------------------------------------
|
|
425
|
+
const coLoaded = [];
|
|
426
|
+
const queue = topMatches.map(e => e.skill.name);
|
|
427
|
+
const visited = new Set(queue);
|
|
428
|
+
|
|
429
|
+
while (queue.length > 0) {
|
|
430
|
+
const current = queue.shift();
|
|
431
|
+
const skill = byName.get(current);
|
|
432
|
+
if (!skill || !skill.relations || !Array.isArray(skill.relations.depends_on)) continue;
|
|
433
|
+
for (const dep of skill.relations.depends_on) {
|
|
434
|
+
const depName = relItemName(dep);
|
|
435
|
+
if (!depName || visited.has(depName)) continue;
|
|
436
|
+
visited.add(depName);
|
|
437
|
+
const depSkill = byName.get(depName);
|
|
438
|
+
if (depSkill) {
|
|
439
|
+
const projectCheck = skillAppliesToProject(depSkill, project, workspace);
|
|
440
|
+
if (projectCheck.applies) {
|
|
441
|
+
coLoaded.push({
|
|
442
|
+
skill: depSkill,
|
|
443
|
+
reason: `depends_on closure from ${current}`,
|
|
444
|
+
role: 'depends_on',
|
|
445
|
+
});
|
|
446
|
+
selectedNames.add(depName);
|
|
447
|
+
queue.push(depName);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// -------------------------------------------------------------------------
|
|
454
|
+
// Stage 4: verify_with co-loading (one hop only — no transitive).
|
|
455
|
+
// -------------------------------------------------------------------------
|
|
456
|
+
for (const { skill } of topMatches) {
|
|
457
|
+
if (!skill.relations || !Array.isArray(skill.relations.verify_with)) continue;
|
|
458
|
+
for (const v of skill.relations.verify_with) {
|
|
459
|
+
const vName = typeof v === 'string' ? v : null;
|
|
460
|
+
if (!vName || selectedNames.has(vName)) continue;
|
|
461
|
+
const vSkill = byName.get(vName);
|
|
462
|
+
if (vSkill) {
|
|
463
|
+
const projectCheck = skillAppliesToProject(vSkill, project, workspace);
|
|
464
|
+
if (projectCheck.applies) {
|
|
465
|
+
coLoaded.push({
|
|
466
|
+
skill: vSkill,
|
|
467
|
+
reason: `verify_with partner of ${skill.name}`,
|
|
468
|
+
role: 'verify_with',
|
|
469
|
+
});
|
|
470
|
+
selectedNames.add(vName);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// -------------------------------------------------------------------------
|
|
477
|
+
// Stage 4b: broader (SKOS generalisation) parent recall boost.
|
|
478
|
+
//
|
|
479
|
+
// Per ADR 0001 Decision #3, `relations.broader` declares cross-skill
|
|
480
|
+
// generalisation — the target is a more general skill the author wants
|
|
481
|
+
// co-loaded when the specific (child) skill matches. SKOS semantics:
|
|
482
|
+
// skos:broader(child, parent) means "parent is broader than child".
|
|
483
|
+
//
|
|
484
|
+
// Recall behaviour: when a topMatch declares `broader: [parent]`, the parent
|
|
485
|
+
// is co-loaded as a generalisation companion — the agent gets the broader
|
|
486
|
+
// context alongside the specific match. One hop only (no transitive
|
|
487
|
+
// ancestor walk), to mirror Stage 4's verify_with shape and avoid
|
|
488
|
+
// accidentally pulling in entire taxonomy chains.
|
|
489
|
+
//
|
|
490
|
+
// Inverse direction (`narrower`) is NOT co-loaded because if the parent
|
|
491
|
+
// matched, the children are NOT necessarily relevant — only an explicit
|
|
492
|
+
// child match should pull the parent in. (Authors who want parent →
|
|
493
|
+
// child co-loading should use `verify_with` or `depends_on`.)
|
|
494
|
+
// -------------------------------------------------------------------------
|
|
495
|
+
for (const { skill } of topMatches) {
|
|
496
|
+
if (!skill.relations || !Array.isArray(skill.relations.broader)) continue;
|
|
497
|
+
for (const b of skill.relations.broader) {
|
|
498
|
+
const bName = typeof b === 'string' ? b : null;
|
|
499
|
+
if (!bName || selectedNames.has(bName)) continue;
|
|
500
|
+
const bSkill = byName.get(bName);
|
|
501
|
+
if (bSkill) {
|
|
502
|
+
const projectCheck = skillAppliesToProject(bSkill, project, workspace);
|
|
503
|
+
if (projectCheck.applies) {
|
|
504
|
+
coLoaded.push({
|
|
505
|
+
skill: bSkill,
|
|
506
|
+
reason: `broader generalisation of ${skill.name}`,
|
|
507
|
+
role: 'broader',
|
|
508
|
+
});
|
|
509
|
+
selectedNames.add(bName);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// -------------------------------------------------------------------------
|
|
516
|
+
// Stage 5: score-aware boundary exclusion.
|
|
517
|
+
//
|
|
518
|
+
// A skill listed in another SELECTED skill's boundary[] is removed from
|
|
519
|
+
// the selection, subject to two guards:
|
|
520
|
+
//
|
|
521
|
+
// (1) Only skills that scored in stage 1 — i.e. topMatches — may act as
|
|
522
|
+
// declaring skills. Co-loaded skills (brought in via depends_on or
|
|
523
|
+
// verify_with, with no independent query score) MUST NOT boundary-
|
|
524
|
+
// exclude anyone. Otherwise a topMatch that pulls in a verify_with
|
|
525
|
+
// partner can be excluded BY THAT PARTNER if the partner's boundary
|
|
526
|
+
// names the topMatch — a cyclic invalidation of the topMatch's own
|
|
527
|
+
// win. The partner's boundary is authored as a request-time guard
|
|
528
|
+
// for when the partner itself is the primary match, not as a veto
|
|
529
|
+
// on whatever brought it along.
|
|
530
|
+
//
|
|
531
|
+
// (2) Even among topMatches, exclusion only fires if the declarer
|
|
532
|
+
// actually outscored the target. A weaker match cannot veto a
|
|
533
|
+
// stronger, more direct match on its own vocabulary (M3 in the
|
|
534
|
+
// follow-up plan). Score ties fall through to exclusion, so
|
|
535
|
+
// authored boundaries still break ties deterministically.
|
|
536
|
+
// -------------------------------------------------------------------------
|
|
537
|
+
const boundaryExcluded = [];
|
|
538
|
+
for (const declaring of topMatches) {
|
|
539
|
+
const skill = declaring.skill;
|
|
540
|
+
if (!skill.relations || !Array.isArray(skill.relations.boundary)) continue;
|
|
541
|
+
for (const b of skill.relations.boundary) {
|
|
542
|
+
const bName = relItemName(b);
|
|
543
|
+
const reason = boundaryReason(b);
|
|
544
|
+
if (!bName) continue;
|
|
545
|
+
if (!selectedNames.has(bName)) continue;
|
|
546
|
+
|
|
547
|
+
const bScored = scored.find(e => e.skill.name === bName);
|
|
548
|
+
if (bScored && bScored.score > declaring.score) {
|
|
549
|
+
// Target outscored the declarer on the query; keep it in selection.
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const bSkill = byName.get(bName);
|
|
554
|
+
if (bSkill) {
|
|
555
|
+
boundaryExcluded.push({
|
|
556
|
+
skill: bSkill,
|
|
557
|
+
reason: reason
|
|
558
|
+
? `in boundary[] of ${skill.name}: ${reason}`
|
|
559
|
+
: `in boundary[] of ${skill.name}`,
|
|
560
|
+
role: 'boundary_excluded',
|
|
561
|
+
});
|
|
562
|
+
selectedNames.delete(bName);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// -------------------------------------------------------------------------
|
|
568
|
+
// Stage 6: quality gate (eval_state).
|
|
569
|
+
// -------------------------------------------------------------------------
|
|
570
|
+
const gateRank = { unverified: 0, passing: 1, monitored: 2 };
|
|
571
|
+
const minGate = gateRank[minEvalState] ?? 0;
|
|
572
|
+
const qualityExcluded = [];
|
|
573
|
+
|
|
574
|
+
function passesGate(skill) {
|
|
575
|
+
const state = (skill.health && skill.health.eval_state) || 'unverified';
|
|
576
|
+
return (gateRank[state] ?? 0) >= minGate;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const filterGate = (list) => list.filter(entry => {
|
|
580
|
+
if (passesGate(entry.skill)) return true;
|
|
581
|
+
qualityExcluded.push({
|
|
582
|
+
skill: entry.skill,
|
|
583
|
+
reason: `eval_state=${(entry.skill.health && entry.skill.health.eval_state) || 'unverified'} below --min-eval-state=${minEvalState}`,
|
|
584
|
+
role: 'quality_excluded',
|
|
585
|
+
});
|
|
586
|
+
return false;
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
const selectedFiltered = filterGate(topMatches.filter(e => selectedNames.has(e.skill.name)));
|
|
590
|
+
const coLoadedFiltered = filterGate(coLoaded.filter(e => selectedNames.has(e.skill.name)));
|
|
591
|
+
|
|
592
|
+
// -------------------------------------------------------------------------
|
|
593
|
+
// Stage 7: staleness annotation (does not exclude).
|
|
594
|
+
// -------------------------------------------------------------------------
|
|
595
|
+
function annotate(entry) {
|
|
596
|
+
const s = computeStaleness(entry.skill, todayISO);
|
|
597
|
+
return { ...entry, staleness: s };
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
return {
|
|
601
|
+
selected: selectedFiltered.map(annotate),
|
|
602
|
+
coLoaded: coLoadedFiltered.map(annotate),
|
|
603
|
+
excluded: boundaryExcluded.concat(qualityExcluded).map(annotate),
|
|
604
|
+
excludedByProject,
|
|
605
|
+
notes: [],
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// ---------------------------------------------------------------------------
|
|
610
|
+
// Rendering
|
|
611
|
+
// ---------------------------------------------------------------------------
|
|
612
|
+
|
|
613
|
+
function pad(str, width) {
|
|
614
|
+
const s = String(str);
|
|
615
|
+
if (s.length >= width) return s.slice(0, width - 1) + '…';
|
|
616
|
+
return s + ' '.repeat(width - s.length);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function renderText(result, query) {
|
|
620
|
+
const lines = [];
|
|
621
|
+
lines.push(`Query: "${query}"`);
|
|
622
|
+
lines.push('');
|
|
623
|
+
|
|
624
|
+
if (result.notes.length > 0) {
|
|
625
|
+
for (const note of result.notes) lines.push(` (${note})`);
|
|
626
|
+
lines.push('');
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const sec = (title, list, formatter) => {
|
|
630
|
+
if (list.length === 0) return;
|
|
631
|
+
lines.push(title);
|
|
632
|
+
lines.push(' ' + pad('Skill', 24) + pad('Score', 7) + pad('State', 12) + 'Reason');
|
|
633
|
+
lines.push(' ' + '─'.repeat(72));
|
|
634
|
+
for (const entry of list) lines.push(' ' + formatter(entry));
|
|
635
|
+
lines.push('');
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
sec('SELECTED', result.selected, e =>
|
|
639
|
+
pad(e.skill.name, 24) +
|
|
640
|
+
pad(String(e.score != null ? e.score : ''), 7) +
|
|
641
|
+
pad((e.skill.health && e.skill.health.eval_state) || '-', 12) +
|
|
642
|
+
(e.reasons ? e.reasons.join(', ') : '') +
|
|
643
|
+
(e.staleness && e.staleness.stale ? ` ⚠ stale (${e.staleness.days}d since last verify)` : '')
|
|
644
|
+
);
|
|
645
|
+
|
|
646
|
+
sec('CO-LOADED', result.coLoaded, e =>
|
|
647
|
+
pad(e.skill.name, 24) +
|
|
648
|
+
pad('—', 7) +
|
|
649
|
+
pad((e.skill.health && e.skill.health.eval_state) || '-', 12) +
|
|
650
|
+
e.reason +
|
|
651
|
+
(e.staleness && e.staleness.stale ? ` ⚠ stale (${e.staleness.days}d)` : '')
|
|
652
|
+
);
|
|
653
|
+
|
|
654
|
+
sec('EXCLUDED', result.excluded, e =>
|
|
655
|
+
pad(e.skill.name, 24) +
|
|
656
|
+
pad('—', 7) +
|
|
657
|
+
pad((e.skill.health && e.skill.health.eval_state) || '-', 12) +
|
|
658
|
+
e.reason
|
|
659
|
+
);
|
|
660
|
+
|
|
661
|
+
if (result.excludedByProject.length > 0) {
|
|
662
|
+
lines.push(`EXCLUDED BY PROJECT FILTER (${result.excludedByProject.length} skill(s) — not shown)`);
|
|
663
|
+
lines.push('');
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const sCount = result.selected.length;
|
|
667
|
+
const cCount = result.coLoaded.length;
|
|
668
|
+
const xCount = result.excluded.length;
|
|
669
|
+
const staleCount = [...result.selected, ...result.coLoaded].filter(e => e.staleness && e.staleness.stale).length;
|
|
670
|
+
lines.push(`${sCount} selected, ${cCount} co-loaded, ${xCount} excluded. ${staleCount} stale.`);
|
|
671
|
+
return lines.join('\n');
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function renderJson(result, query) {
|
|
675
|
+
// Trim the skill objects to a useful subset for programmatic consumers.
|
|
676
|
+
const trim = (e) => ({
|
|
677
|
+
name: e.skill.name,
|
|
678
|
+
id: e.skill.id,
|
|
679
|
+
path: e.skill.path,
|
|
680
|
+
score: e.score,
|
|
681
|
+
role: e.role,
|
|
682
|
+
eval_state: (e.skill.health && e.skill.health.eval_state) || null,
|
|
683
|
+
reasons: e.reasons,
|
|
684
|
+
reason: e.reason,
|
|
685
|
+
staleness: e.staleness,
|
|
686
|
+
});
|
|
687
|
+
return JSON.stringify({
|
|
688
|
+
query,
|
|
689
|
+
selected: result.selected.map(trim),
|
|
690
|
+
co_loaded: result.coLoaded.map(trim),
|
|
691
|
+
excluded: result.excluded.map(trim),
|
|
692
|
+
excluded_by_project: result.excludedByProject.map(e => ({ name: e.skill.name, reason: e.reason })),
|
|
693
|
+
}, null, 2);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// ---------------------------------------------------------------------------
|
|
697
|
+
// CLI
|
|
698
|
+
// ---------------------------------------------------------------------------
|
|
699
|
+
|
|
700
|
+
function argValue(args, flag) {
|
|
701
|
+
const i = args.indexOf(flag);
|
|
702
|
+
return i !== -1 && args[i + 1] ? args[i + 1] : null;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function main() {
|
|
706
|
+
const args = process.argv.slice(2);
|
|
707
|
+
const outputJson = args.includes('--json');
|
|
708
|
+
const manifestArg = argValue(args, '--manifest');
|
|
709
|
+
const project = argValue(args, '--project') || process.env.SKILL_GRAPH_PROJECT || null;
|
|
710
|
+
const maxResults = parseInt(argValue(args, '--max') || '10', 10);
|
|
711
|
+
const minEvalState = argValue(args, '--min-eval-state') || 'unverified';
|
|
712
|
+
const pathArg = argValue(args, '--path');
|
|
713
|
+
|
|
714
|
+
// Everything that is not a flag and not a flag argument is treated as the query.
|
|
715
|
+
const nonFlag = [];
|
|
716
|
+
for (let i = 0; i < args.length; i++) {
|
|
717
|
+
const a = args[i];
|
|
718
|
+
if (a.startsWith('--')) {
|
|
719
|
+
if (['--manifest', '--project', '--max', '--min-eval-state', '--path'].includes(a)) i++;
|
|
720
|
+
continue;
|
|
721
|
+
}
|
|
722
|
+
nonFlag.push(a);
|
|
723
|
+
}
|
|
724
|
+
const query = nonFlag.join(' ').trim();
|
|
725
|
+
|
|
726
|
+
if (!query) {
|
|
727
|
+
console.error('Usage: skill-graph-route.js <query> [--project P] [--max N] [--min-eval-state unverified|passing|monitored] [--path FILE] [--manifest PATH] [--json]');
|
|
728
|
+
process.exit(1);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
const manifestPath = manifestArg
|
|
732
|
+
? path.resolve(manifestArg)
|
|
733
|
+
: (fs.existsSync(DEFAULT_MANIFEST)
|
|
734
|
+
? DEFAULT_MANIFEST
|
|
735
|
+
: (fs.existsSync(SAMPLE_MANIFEST) ? SAMPLE_MANIFEST : PACKAGE_SAMPLE_MANIFEST));
|
|
736
|
+
|
|
737
|
+
if (!fs.existsSync(manifestPath)) {
|
|
738
|
+
console.error(`ERROR manifest not found: ${manifestPath}`);
|
|
739
|
+
console.error('Run `node scripts/generate-manifest.js --output skills.manifest.json` first, or pass --manifest <path>.');
|
|
740
|
+
process.exit(1);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
let manifest;
|
|
744
|
+
try {
|
|
745
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
746
|
+
} catch (e) {
|
|
747
|
+
console.error(`ERROR cannot parse manifest: ${e.message}`);
|
|
748
|
+
process.exit(1);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
const todayISO = new Date().toISOString().slice(0, 10);
|
|
752
|
+
const result = routeSkills(manifest, { query, project, maxResults, minEvalState, pathArg, todayISO });
|
|
753
|
+
|
|
754
|
+
if (outputJson) {
|
|
755
|
+
process.stdout.write(renderJson(result, query) + '\n');
|
|
756
|
+
} else {
|
|
757
|
+
process.stdout.write(renderText(result, query) + '\n');
|
|
758
|
+
}
|
|
759
|
+
process.exit(0);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// Allow require() from scripts/skill-graph-routing-eval.js so the harness
|
|
763
|
+
// can reuse the scoring + boundary-exclusion pipeline without shelling out.
|
|
764
|
+
module.exports = { routeSkills, tokenize, matchesGlob, matchPathList, computeStaleness };
|
|
765
|
+
|
|
766
|
+
if (require.main === module) main();
|