@mseep/core 3.0.0
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 +285 -0
- package/LICENSE +21 -0
- package/README.ja.md +14 -0
- package/README.ko.md +14 -0
- package/README.md +227 -0
- package/README.pt-BR.md +14 -0
- package/README.skills.md +50 -0
- package/README.uk.md +14 -0
- package/README.zh-CN.md +14 -0
- package/bin/booklib-mcp.js +458 -0
- package/bin/booklib.js +2394 -0
- package/bin/skills.cjs +1292 -0
- package/community/registry.json +1616 -0
- package/hooks/hooks.json +52 -0
- package/hooks/posttooluse-capture.mjs +67 -0
- package/hooks/posttooluse-contradict.mjs +76 -0
- package/hooks/posttooluse-imports.mjs +67 -0
- package/hooks/pretooluse-inject.mjs +82 -0
- package/hooks/suggest.js +153 -0
- package/lib/agent-detector.js +96 -0
- package/lib/config-loader.js +39 -0
- package/lib/conflict-resolver.js +148 -0
- package/lib/connectors/context7.js +167 -0
- package/lib/connectors/github.js +223 -0
- package/lib/connectors/local.js +120 -0
- package/lib/connectors/notion.js +436 -0
- package/lib/connectors/web.js +134 -0
- package/lib/context-builder.js +574 -0
- package/lib/discovery-engine.js +298 -0
- package/lib/doctor/hook-installer.js +83 -0
- package/lib/doctor/usage-tracker.js +87 -0
- package/lib/engine/auditor.js +103 -0
- package/lib/engine/auto-linker.js +177 -0
- package/lib/engine/bm25-index.js +178 -0
- package/lib/engine/capture.js +120 -0
- package/lib/engine/context-map.js +641 -0
- package/lib/engine/corrections.js +194 -0
- package/lib/engine/decision-checker.js +203 -0
- package/lib/engine/doctor.js +207 -0
- package/lib/engine/embedding-provider.js +72 -0
- package/lib/engine/gap-detector.js +138 -0
- package/lib/engine/gap-resolver.js +135 -0
- package/lib/engine/graph-injector.js +137 -0
- package/lib/engine/graph-search.js +183 -0
- package/lib/engine/graph.js +170 -0
- package/lib/engine/handoff.js +411 -0
- package/lib/engine/import-checker.js +249 -0
- package/lib/engine/import-parser.js +145 -0
- package/lib/engine/indexer.js +334 -0
- package/lib/engine/lookup-priority.js +15 -0
- package/lib/engine/parser.js +257 -0
- package/lib/engine/principle-extractor.js +116 -0
- package/lib/engine/project-analyzer.js +353 -0
- package/lib/engine/query-expander.js +42 -0
- package/lib/engine/reasoning-modes.js +353 -0
- package/lib/engine/registries.js +524 -0
- package/lib/engine/reranker.js +45 -0
- package/lib/engine/rrf.js +59 -0
- package/lib/engine/scanner.js +151 -0
- package/lib/engine/searcher.js +223 -0
- package/lib/engine/session-coordinator.js +291 -0
- package/lib/engine/session-manager.js +375 -0
- package/lib/engine/source-detector.js +240 -0
- package/lib/engine/source-manager.js +142 -0
- package/lib/engine/structured-response.js +47 -0
- package/lib/engine/synthesis-templates.js +364 -0
- package/lib/installer.js +70 -0
- package/lib/instinct-block.js +21 -0
- package/lib/mcp-config-writer.js +107 -0
- package/lib/paths.js +62 -0
- package/lib/project-initializer.js +856 -0
- package/lib/registry/skills.js +102 -0
- package/lib/registry-searcher.js +107 -0
- package/lib/rules/rules-manager.js +169 -0
- package/lib/skill-fetcher.js +333 -0
- package/lib/well-known-builder.js +74 -0
- package/lib/wizard/index.js +1389 -0
- package/lib/wizard/integration-detector.js +41 -0
- package/lib/wizard/project-detector.js +146 -0
- package/lib/wizard/prompt.js +221 -0
- package/lib/wizard/registry-embeddings.js +107 -0
- package/lib/wizard/skill-recommender.js +69 -0
- package/package.json +70 -0
- package/skills/animation-at-work/SKILL.md +270 -0
- package/skills/animation-at-work/assets/example_asset.txt +1 -0
- package/skills/animation-at-work/evals/evals.json +44 -0
- package/skills/animation-at-work/evals/results.json +13 -0
- package/skills/animation-at-work/examples/after.md +64 -0
- package/skills/animation-at-work/examples/before.md +35 -0
- package/skills/animation-at-work/references/api_reference.md +369 -0
- package/skills/animation-at-work/references/review-checklist.md +79 -0
- package/skills/animation-at-work/scripts/audit_animations.py +295 -0
- package/skills/animation-at-work/scripts/example.py +1 -0
- package/skills/booklib-mcp-guide/SKILL.md +129 -0
- package/skills/booklib-mcp-guide/evals/evals.json +37 -0
- package/skills/booklib-mcp-guide/examples/after.md +34 -0
- package/skills/booklib-mcp-guide/examples/before.md +27 -0
- package/skills/booklib-mcp-guide/references/tool-catalog.md +9 -0
- package/skills/clean-code-reviewer/SKILL.md +444 -0
- package/skills/clean-code-reviewer/audit.json +35 -0
- package/skills/clean-code-reviewer/evals/evals.json +185 -0
- package/skills/clean-code-reviewer/evals/results.json +13 -0
- package/skills/clean-code-reviewer/examples/after.md +48 -0
- package/skills/clean-code-reviewer/examples/before.md +33 -0
- package/skills/clean-code-reviewer/references/api_reference.md +158 -0
- package/skills/clean-code-reviewer/references/practices-catalog.md +282 -0
- package/skills/clean-code-reviewer/references/review-checklist.md +254 -0
- package/skills/clean-code-reviewer/scripts/pre-review.py +206 -0
- package/skills/data-intensive-patterns/SKILL.md +267 -0
- package/skills/data-intensive-patterns/assets/example_asset.txt +1 -0
- package/skills/data-intensive-patterns/evals/evals.json +54 -0
- package/skills/data-intensive-patterns/evals/results.json +13 -0
- package/skills/data-intensive-patterns/examples/after.md +61 -0
- package/skills/data-intensive-patterns/examples/before.md +38 -0
- package/skills/data-intensive-patterns/references/api_reference.md +34 -0
- package/skills/data-intensive-patterns/references/patterns-catalog.md +551 -0
- package/skills/data-intensive-patterns/references/review-checklist.md +193 -0
- package/skills/data-intensive-patterns/scripts/adr.py +213 -0
- package/skills/data-intensive-patterns/scripts/example.py +1 -0
- package/skills/data-pipelines/SKILL.md +259 -0
- package/skills/data-pipelines/assets/example_asset.txt +1 -0
- package/skills/data-pipelines/evals/evals.json +45 -0
- package/skills/data-pipelines/evals/results.json +13 -0
- package/skills/data-pipelines/examples/after.md +97 -0
- package/skills/data-pipelines/examples/before.md +37 -0
- package/skills/data-pipelines/references/api_reference.md +301 -0
- package/skills/data-pipelines/references/review-checklist.md +181 -0
- package/skills/data-pipelines/scripts/example.py +1 -0
- package/skills/data-pipelines/scripts/new_pipeline.py +444 -0
- package/skills/design-patterns/SKILL.md +271 -0
- package/skills/design-patterns/assets/example_asset.txt +1 -0
- package/skills/design-patterns/evals/evals.json +46 -0
- package/skills/design-patterns/evals/results.json +13 -0
- package/skills/design-patterns/examples/after.md +52 -0
- package/skills/design-patterns/examples/before.md +29 -0
- package/skills/design-patterns/references/api_reference.md +1 -0
- package/skills/design-patterns/references/patterns-catalog.md +726 -0
- package/skills/design-patterns/references/review-checklist.md +173 -0
- package/skills/design-patterns/scripts/example.py +1 -0
- package/skills/design-patterns/scripts/scaffold.py +807 -0
- package/skills/domain-driven-design/SKILL.md +142 -0
- package/skills/domain-driven-design/assets/example_asset.txt +1 -0
- package/skills/domain-driven-design/evals/evals.json +48 -0
- package/skills/domain-driven-design/evals/results.json +13 -0
- package/skills/domain-driven-design/examples/after.md +80 -0
- package/skills/domain-driven-design/examples/before.md +43 -0
- package/skills/domain-driven-design/references/api_reference.md +1 -0
- package/skills/domain-driven-design/references/patterns-catalog.md +545 -0
- package/skills/domain-driven-design/references/review-checklist.md +158 -0
- package/skills/domain-driven-design/scripts/example.py +1 -0
- package/skills/domain-driven-design/scripts/scaffold.py +421 -0
- package/skills/effective-java/SKILL.md +227 -0
- package/skills/effective-java/assets/example_asset.txt +1 -0
- package/skills/effective-java/evals/evals.json +46 -0
- package/skills/effective-java/evals/results.json +13 -0
- package/skills/effective-java/examples/after.md +83 -0
- package/skills/effective-java/examples/before.md +37 -0
- package/skills/effective-java/references/api_reference.md +1 -0
- package/skills/effective-java/references/items-catalog.md +955 -0
- package/skills/effective-java/references/review-checklist.md +216 -0
- package/skills/effective-java/scripts/checkstyle_setup.py +211 -0
- package/skills/effective-java/scripts/example.py +1 -0
- package/skills/effective-kotlin/SKILL.md +271 -0
- package/skills/effective-kotlin/assets/example_asset.txt +1 -0
- package/skills/effective-kotlin/audit.json +29 -0
- package/skills/effective-kotlin/evals/evals.json +45 -0
- package/skills/effective-kotlin/evals/results.json +13 -0
- package/skills/effective-kotlin/examples/after.md +36 -0
- package/skills/effective-kotlin/examples/before.md +38 -0
- package/skills/effective-kotlin/references/api_reference.md +1 -0
- package/skills/effective-kotlin/references/practices-catalog.md +1228 -0
- package/skills/effective-kotlin/references/review-checklist.md +126 -0
- package/skills/effective-kotlin/scripts/example.py +1 -0
- package/skills/effective-python/SKILL.md +441 -0
- package/skills/effective-python/evals/evals.json +44 -0
- package/skills/effective-python/evals/results.json +13 -0
- package/skills/effective-python/examples/after.md +56 -0
- package/skills/effective-python/examples/before.md +40 -0
- package/skills/effective-python/ref-01-pythonic-thinking.md +202 -0
- package/skills/effective-python/ref-02-lists-and-dicts.md +146 -0
- package/skills/effective-python/ref-03-functions.md +186 -0
- package/skills/effective-python/ref-04-comprehensions-generators.md +211 -0
- package/skills/effective-python/ref-05-classes-interfaces.md +188 -0
- package/skills/effective-python/ref-06-metaclasses-attributes.md +209 -0
- package/skills/effective-python/ref-07-concurrency.md +213 -0
- package/skills/effective-python/ref-08-robustness-performance.md +248 -0
- package/skills/effective-python/ref-09-testing-debugging.md +253 -0
- package/skills/effective-python/ref-10-collaboration.md +175 -0
- package/skills/effective-python/references/api_reference.md +218 -0
- package/skills/effective-python/references/practices-catalog.md +483 -0
- package/skills/effective-python/references/review-checklist.md +190 -0
- package/skills/effective-python/scripts/lint.py +173 -0
- package/skills/effective-typescript/SKILL.md +262 -0
- package/skills/effective-typescript/audit.json +29 -0
- package/skills/effective-typescript/evals/evals.json +37 -0
- package/skills/effective-typescript/evals/results.json +13 -0
- package/skills/effective-typescript/examples/after.md +70 -0
- package/skills/effective-typescript/examples/before.md +47 -0
- package/skills/effective-typescript/references/api_reference.md +118 -0
- package/skills/effective-typescript/references/practices-catalog.md +371 -0
- package/skills/effective-typescript/scripts/review.py +169 -0
- package/skills/kotlin-in-action/SKILL.md +261 -0
- package/skills/kotlin-in-action/assets/example_asset.txt +1 -0
- package/skills/kotlin-in-action/evals/evals.json +43 -0
- package/skills/kotlin-in-action/evals/results.json +13 -0
- package/skills/kotlin-in-action/examples/after.md +53 -0
- package/skills/kotlin-in-action/examples/before.md +39 -0
- package/skills/kotlin-in-action/references/api_reference.md +1 -0
- package/skills/kotlin-in-action/references/practices-catalog.md +436 -0
- package/skills/kotlin-in-action/references/review-checklist.md +204 -0
- package/skills/kotlin-in-action/scripts/example.py +1 -0
- package/skills/kotlin-in-action/scripts/setup_detekt.py +224 -0
- package/skills/lean-startup/SKILL.md +160 -0
- package/skills/lean-startup/assets/example_asset.txt +1 -0
- package/skills/lean-startup/evals/evals.json +43 -0
- package/skills/lean-startup/evals/results.json +13 -0
- package/skills/lean-startup/examples/after.md +80 -0
- package/skills/lean-startup/examples/before.md +34 -0
- package/skills/lean-startup/references/api_reference.md +319 -0
- package/skills/lean-startup/references/review-checklist.md +137 -0
- package/skills/lean-startup/scripts/example.py +1 -0
- package/skills/lean-startup/scripts/new_experiment.py +286 -0
- package/skills/microservices-patterns/SKILL.md +384 -0
- package/skills/microservices-patterns/evals/evals.json +45 -0
- package/skills/microservices-patterns/evals/results.json +13 -0
- package/skills/microservices-patterns/examples/after.md +69 -0
- package/skills/microservices-patterns/examples/before.md +40 -0
- package/skills/microservices-patterns/references/patterns-catalog.md +391 -0
- package/skills/microservices-patterns/references/review-checklist.md +169 -0
- package/skills/microservices-patterns/scripts/new_service.py +583 -0
- package/skills/programming-with-rust/SKILL.md +209 -0
- package/skills/programming-with-rust/evals/evals.json +37 -0
- package/skills/programming-with-rust/evals/results.json +13 -0
- package/skills/programming-with-rust/examples/after.md +107 -0
- package/skills/programming-with-rust/examples/before.md +59 -0
- package/skills/programming-with-rust/references/api_reference.md +152 -0
- package/skills/programming-with-rust/references/practices-catalog.md +335 -0
- package/skills/programming-with-rust/scripts/review.py +142 -0
- package/skills/refactoring-ui/SKILL.md +362 -0
- package/skills/refactoring-ui/assets/example_asset.txt +1 -0
- package/skills/refactoring-ui/evals/evals.json +45 -0
- package/skills/refactoring-ui/evals/results.json +13 -0
- package/skills/refactoring-ui/examples/after.md +85 -0
- package/skills/refactoring-ui/examples/before.md +58 -0
- package/skills/refactoring-ui/references/api_reference.md +355 -0
- package/skills/refactoring-ui/references/review-checklist.md +114 -0
- package/skills/refactoring-ui/scripts/audit_css.py +250 -0
- package/skills/refactoring-ui/scripts/example.py +1 -0
- package/skills/rust-in-action/SKILL.md +350 -0
- package/skills/rust-in-action/evals/evals.json +38 -0
- package/skills/rust-in-action/evals/results.json +13 -0
- package/skills/rust-in-action/examples/after.md +156 -0
- package/skills/rust-in-action/examples/before.md +56 -0
- package/skills/rust-in-action/references/practices-catalog.md +346 -0
- package/skills/rust-in-action/scripts/review.py +147 -0
- package/skills/skill-router/SKILL.md +186 -0
- package/skills/skill-router/evals/evals.json +38 -0
- package/skills/skill-router/evals/results.json +13 -0
- package/skills/skill-router/examples/after.md +63 -0
- package/skills/skill-router/examples/before.md +39 -0
- package/skills/skill-router/references/api_reference.md +24 -0
- package/skills/skill-router/references/routing-heuristics.md +89 -0
- package/skills/skill-router/references/skill-catalog.md +174 -0
- package/skills/skill-router/scripts/route.py +266 -0
- package/skills/spring-boot-in-action/SKILL.md +340 -0
- package/skills/spring-boot-in-action/evals/evals.json +39 -0
- package/skills/spring-boot-in-action/evals/results.json +13 -0
- package/skills/spring-boot-in-action/examples/after.md +185 -0
- package/skills/spring-boot-in-action/examples/before.md +84 -0
- package/skills/spring-boot-in-action/references/practices-catalog.md +403 -0
- package/skills/spring-boot-in-action/scripts/review.py +184 -0
- package/skills/storytelling-with-data/SKILL.md +241 -0
- package/skills/storytelling-with-data/assets/example_asset.txt +1 -0
- package/skills/storytelling-with-data/evals/evals.json +47 -0
- package/skills/storytelling-with-data/evals/results.json +13 -0
- package/skills/storytelling-with-data/examples/after.md +50 -0
- package/skills/storytelling-with-data/examples/before.md +33 -0
- package/skills/storytelling-with-data/references/api_reference.md +379 -0
- package/skills/storytelling-with-data/references/review-checklist.md +111 -0
- package/skills/storytelling-with-data/scripts/chart_review.py +301 -0
- package/skills/storytelling-with-data/scripts/example.py +1 -0
- package/skills/system-design-interview/SKILL.md +233 -0
- package/skills/system-design-interview/assets/example_asset.txt +1 -0
- package/skills/system-design-interview/evals/evals.json +46 -0
- package/skills/system-design-interview/evals/results.json +13 -0
- package/skills/system-design-interview/examples/after.md +94 -0
- package/skills/system-design-interview/examples/before.md +27 -0
- package/skills/system-design-interview/references/api_reference.md +582 -0
- package/skills/system-design-interview/references/review-checklist.md +201 -0
- package/skills/system-design-interview/scripts/example.py +1 -0
- package/skills/system-design-interview/scripts/new_design.py +421 -0
- package/skills/using-asyncio-python/SKILL.md +290 -0
- package/skills/using-asyncio-python/assets/example_asset.txt +1 -0
- package/skills/using-asyncio-python/evals/evals.json +43 -0
- package/skills/using-asyncio-python/evals/results.json +13 -0
- package/skills/using-asyncio-python/examples/after.md +68 -0
- package/skills/using-asyncio-python/examples/before.md +39 -0
- package/skills/using-asyncio-python/references/api_reference.md +267 -0
- package/skills/using-asyncio-python/references/review-checklist.md +149 -0
- package/skills/using-asyncio-python/scripts/check_blocking.py +270 -0
- package/skills/using-asyncio-python/scripts/example.py +1 -0
- package/skills/web-scraping-python/SKILL.md +280 -0
- package/skills/web-scraping-python/assets/example_asset.txt +1 -0
- package/skills/web-scraping-python/evals/evals.json +46 -0
- package/skills/web-scraping-python/evals/results.json +13 -0
- package/skills/web-scraping-python/examples/after.md +109 -0
- package/skills/web-scraping-python/examples/before.md +40 -0
- package/skills/web-scraping-python/references/api_reference.md +393 -0
- package/skills/web-scraping-python/references/review-checklist.md +163 -0
- package/skills/web-scraping-python/scripts/example.py +1 -0
- package/skills/web-scraping-python/scripts/new_scraper.py +231 -0
- package/skills/writing-plans/audit.json +34 -0
|
@@ -0,0 +1,641 @@
|
|
|
1
|
+
// lib/engine/context-map.js — keyword extraction, map building, and matching for runtime context injection
|
|
2
|
+
|
|
3
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync } from 'node:fs';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Stopwords filtered from code terms. Common English words that
|
|
8
|
+
* carry no signal for matching knowledge to source files.
|
|
9
|
+
*/
|
|
10
|
+
const STOPWORDS = new Set([
|
|
11
|
+
'a', 'an', 'the', 'in', 'on', 'at', 'to', 'for', 'of', 'and', 'or',
|
|
12
|
+
'is', 'are', 'was', 'were', 'be', 'been', 'being', 'have', 'has', 'had',
|
|
13
|
+
'do', 'does', 'did', 'will', 'would', 'shall', 'should', 'may', 'might',
|
|
14
|
+
'must', 'can', 'could', 'not', 'no', 'nor', 'so', 'yet', 'both', 'with',
|
|
15
|
+
'about', 'from', 'up', 'down', 'out', 'how', 'what', 'when', 'where',
|
|
16
|
+
'who', 'why', 'which', 'by', 'as', 'if', 'then', 'than', 'too', 'very',
|
|
17
|
+
'just', 'more', 'also', 'its', 'it', 'all', 'use', 'that', 'this',
|
|
18
|
+
'each', 'every', 'any', 'some', 'such',
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Terms that map to file glob patterns. When these appear in
|
|
23
|
+
* knowledge text, they hint which source files the rule applies to.
|
|
24
|
+
*/
|
|
25
|
+
const TERM_TO_GLOB = {
|
|
26
|
+
api: '**/api/**',
|
|
27
|
+
admin: '**/admin/**',
|
|
28
|
+
auth: '**/auth/**',
|
|
29
|
+
middleware: '**/middleware/**',
|
|
30
|
+
config: '**/config/**',
|
|
31
|
+
migration: '**/migrations/**',
|
|
32
|
+
test: '**/test*/**',
|
|
33
|
+
route: '**/routes/**',
|
|
34
|
+
controller: '**/controllers/**',
|
|
35
|
+
model: '**/models/**',
|
|
36
|
+
service: '**/services/**',
|
|
37
|
+
component: '**/components/**',
|
|
38
|
+
hook: '**/hooks/**',
|
|
39
|
+
util: '**/utils/**',
|
|
40
|
+
helper: '**/helpers/**',
|
|
41
|
+
schema: '**/schema*/**',
|
|
42
|
+
handler: '**/handlers/**',
|
|
43
|
+
worker: '**/workers/**',
|
|
44
|
+
job: '**/jobs/**',
|
|
45
|
+
plugin: '**/plugins/**',
|
|
46
|
+
endpoint: '**/api/**',
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Known packages whose mention in knowledge text signals an import trigger.
|
|
51
|
+
* Checked case-insensitively against words in the text.
|
|
52
|
+
*/
|
|
53
|
+
const KNOWN_PACKAGES = new Set([
|
|
54
|
+
'stripe', 'express', 'next', 'react', 'supabase', 'prisma',
|
|
55
|
+
'drizzle', 'zod', 'joi', 'lodash', 'axios', 'fastify', 'hono',
|
|
56
|
+
'trpc', 'graphql', 'apollo', 'sequelize', 'mongoose', 'redis',
|
|
57
|
+
'bullmq', 'kafkajs', 'socket.io', 'passport', 'jest', 'vitest',
|
|
58
|
+
'playwright', 'cypress', 'tailwindcss', 'webpack', 'vite', 'esbuild',
|
|
59
|
+
'turborepo', 'pino', 'winston', 'sentry', 'datadog', 'knex',
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
/** Regex to capture quoted package names like '@scope/pkg' or "pkg-name" */
|
|
63
|
+
const QUOTED_PKG_RE = /['"](@[\w-]+\/[\w.-]+|[\w][\w.-]*)['"`]/g;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Extract three types of keywords from knowledge text.
|
|
67
|
+
*
|
|
68
|
+
* @param {string} text - knowledge item text (markdown, notes, decisions)
|
|
69
|
+
* @returns {{ codeTerms: string[], filePatterns: string[], importTriggers: string[] }}
|
|
70
|
+
*/
|
|
71
|
+
export function extractKeywords(text) {
|
|
72
|
+
if (!text) return { codeTerms: [], filePatterns: [], importTriggers: [] };
|
|
73
|
+
|
|
74
|
+
const lowerText = text.toLowerCase();
|
|
75
|
+
const words = lowerText.split(/[^a-z0-9@/._-]+/).filter(Boolean);
|
|
76
|
+
|
|
77
|
+
const codeTerms = extractCodeTerms(words);
|
|
78
|
+
const filePatterns = extractFilePatterns(words);
|
|
79
|
+
const importTriggers = extractImportTriggers(words, text);
|
|
80
|
+
|
|
81
|
+
return { codeTerms, filePatterns, importTriggers };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Nouns and identifiers: filter stopwords, min 3 chars, deduplicated, lowercased.
|
|
86
|
+
* @param {string[]} words - pre-split lowercased tokens
|
|
87
|
+
* @returns {string[]}
|
|
88
|
+
*/
|
|
89
|
+
function extractCodeTerms(words) {
|
|
90
|
+
const seen = new Set();
|
|
91
|
+
return words.filter(w => {
|
|
92
|
+
if (w.length < 3 || STOPWORDS.has(w) || seen.has(w)) return false;
|
|
93
|
+
seen.add(w);
|
|
94
|
+
return true;
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Glob patterns inferred from path-like terms in text.
|
|
100
|
+
* @param {string[]} words - pre-split lowercased tokens
|
|
101
|
+
* @returns {string[]}
|
|
102
|
+
*/
|
|
103
|
+
function extractFilePatterns(words) {
|
|
104
|
+
const patterns = new Set();
|
|
105
|
+
for (const word of words) {
|
|
106
|
+
// Strip trailing 's' for simple plurals (e.g., "endpoints" -> "endpoint")
|
|
107
|
+
const singular = word.endsWith('s') ? word.slice(0, -1) : word;
|
|
108
|
+
const glob = TERM_TO_GLOB[word] ?? TERM_TO_GLOB[singular];
|
|
109
|
+
if (glob) patterns.add(glob);
|
|
110
|
+
}
|
|
111
|
+
return [...patterns];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Package names found by checking known packages and extracting quoted strings.
|
|
116
|
+
* @param {string[]} words - pre-split lowercased tokens
|
|
117
|
+
* @param {string} rawText - original text for quoted-string extraction
|
|
118
|
+
* @returns {string[]}
|
|
119
|
+
*/
|
|
120
|
+
function extractImportTriggers(words, rawText) {
|
|
121
|
+
const triggers = new Set();
|
|
122
|
+
|
|
123
|
+
for (const word of words) {
|
|
124
|
+
if (KNOWN_PACKAGES.has(word)) triggers.add(word);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
for (const match of rawText.matchAll(QUOTED_PKG_RE)) {
|
|
128
|
+
triggers.add(match[1]);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return [...triggers];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ── Code block extraction ────────────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
const CODE_BLOCK_RE = /```[\w]*\n([\s\S]*?)```/;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Extract the first markdown code block from text.
|
|
140
|
+
* @param {string} text
|
|
141
|
+
* @returns {string | null}
|
|
142
|
+
*/
|
|
143
|
+
function extractCodeBlock(text) {
|
|
144
|
+
const match = text.match(CODE_BLOCK_RE);
|
|
145
|
+
return match ? match[1].trim() : null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ── Injection text builder ───────────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Build pre-computed injection text for a knowledge item.
|
|
152
|
+
* For decisions/notes: constraint is first 200 chars, correction is null,
|
|
153
|
+
* and example is extracted from any markdown code block.
|
|
154
|
+
*
|
|
155
|
+
* @param {{ text: string }} item - knowledge item with a .text property
|
|
156
|
+
* @returns {{ correction: null, constraint: string, example: string | null }}
|
|
157
|
+
*/
|
|
158
|
+
export function buildInjectionText(item) {
|
|
159
|
+
const text = item?.text ?? '';
|
|
160
|
+
return {
|
|
161
|
+
correction: null,
|
|
162
|
+
constraint: text.slice(0, 200),
|
|
163
|
+
example: extractCodeBlock(text),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ── Simple glob matching (no external deps) ─────────────────────────────────
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Convert a file-glob pattern to a regex and test it against a file path.
|
|
171
|
+
* Supports **, *, and ? wildcards.
|
|
172
|
+
*
|
|
173
|
+
* @param {string} pattern - glob pattern (e.g., '** /api/**')
|
|
174
|
+
* @param {string} filePath - file path to test
|
|
175
|
+
* @returns {boolean}
|
|
176
|
+
*/
|
|
177
|
+
function simpleGlob(pattern, filePath) {
|
|
178
|
+
const escaped = pattern
|
|
179
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
180
|
+
.replace(/\*\*/g, '\x00')
|
|
181
|
+
.replace(/\*/g, '[^/]*')
|
|
182
|
+
.replace(/\?/g, '[^/]')
|
|
183
|
+
.replace(/\x00/g, '.*');
|
|
184
|
+
return new RegExp(`^${escaped}$`).test(filePath);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ── Match strength constants ────────────────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
const STRENGTH_IMPORT = 4;
|
|
190
|
+
const STRENGTH_FUNCTION = 3;
|
|
191
|
+
const STRENGTH_CODE_TERM = 2;
|
|
192
|
+
const STRENGTH_FILE_PATTERN = 1;
|
|
193
|
+
// Team knowledge needs stronger signal than a single codeTerm match
|
|
194
|
+
const MIN_STRENGTH_TEAM = 3; // require file pattern + code term, or function/import match
|
|
195
|
+
|
|
196
|
+
const MAX_MATCHES = 5;
|
|
197
|
+
|
|
198
|
+
// ── LLM batch size ──────────────────────────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
const LLM_BATCH_SIZE = 20;
|
|
201
|
+
|
|
202
|
+
// ── ContextMapBuilder ───────────────────────────────────────────────────────
|
|
203
|
+
|
|
204
|
+
export class ContextMapBuilder {
|
|
205
|
+
constructor(opts = {}) {
|
|
206
|
+
this.processingMode = opts.processingMode ?? 'fast';
|
|
207
|
+
this.apiKey = opts.apiKey;
|
|
208
|
+
this.ollamaModel = opts.ollamaModel;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Build a context map from knowledge items.
|
|
213
|
+
* @param {Array<{id: string, text: string, source?: string, type?: string}>} items
|
|
214
|
+
* @returns {Promise<{version: number, builtAt: string, items: Array}>}
|
|
215
|
+
*/
|
|
216
|
+
async buildFromKnowledge(items) {
|
|
217
|
+
if (!items?.length) return this._emptyMap();
|
|
218
|
+
|
|
219
|
+
const mapped = items.map(item => ({
|
|
220
|
+
id: item.id,
|
|
221
|
+
source: item.source ?? null,
|
|
222
|
+
type: item.type ?? null,
|
|
223
|
+
match: {
|
|
224
|
+
...extractKeywords(item.text),
|
|
225
|
+
functionPatterns: [],
|
|
226
|
+
},
|
|
227
|
+
injection: buildInjectionText(item),
|
|
228
|
+
}));
|
|
229
|
+
|
|
230
|
+
if (this.processingMode !== 'fast') {
|
|
231
|
+
await this._inferScopes(mapped, items);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return { version: 1, builtAt: new Date().toISOString(), items: mapped };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Build a context map from post-training knowledge gaps.
|
|
239
|
+
* @param {Array<{name: string, version?: string, ecosystem?: string, publishDate?: string}>} gaps
|
|
240
|
+
* @returns {Promise<{version: number, builtAt: string, items: Array}>}
|
|
241
|
+
*/
|
|
242
|
+
async buildFromGaps(gaps, opts = {}) {
|
|
243
|
+
if (!gaps?.length) return this._emptyMap();
|
|
244
|
+
const { booklibDir } = opts;
|
|
245
|
+
|
|
246
|
+
const mapped = gaps.map(gap => {
|
|
247
|
+
const thinCorrection = `${gap.name}@${gap.version ?? 'latest'} (published ${gap.publishDate ?? 'unknown'}). Post-training.`;
|
|
248
|
+
|
|
249
|
+
// Try to enrich correction from resolved docs (Context7/GitHub)
|
|
250
|
+
let richCorrection = thinCorrection;
|
|
251
|
+
if (booklibDir) {
|
|
252
|
+
try {
|
|
253
|
+
const safeName = gap.name.replace(/[@/]/g, '_').replace(/^_+/, '');
|
|
254
|
+
const sourceDirs = [
|
|
255
|
+
join(booklibDir, 'sources', `ctx7-${safeName}`),
|
|
256
|
+
join(booklibDir, 'sources', `gh-${safeName}`),
|
|
257
|
+
];
|
|
258
|
+
|
|
259
|
+
for (const dir of sourceDirs) {
|
|
260
|
+
if (!existsSync(dir)) continue;
|
|
261
|
+
const files = readdirSync(dir).filter(f => f.endsWith('.md'));
|
|
262
|
+
if (files.length === 0) continue;
|
|
263
|
+
|
|
264
|
+
// Extract a short summary — hooks inject 3-10 lines max.
|
|
265
|
+
// Full docs are available via MCP lookup tool.
|
|
266
|
+
const allContent = files.slice(0, 5).map(f =>
|
|
267
|
+
readFileSync(join(dir, f), 'utf8')
|
|
268
|
+
).join('\n');
|
|
269
|
+
|
|
270
|
+
// Pull key identifiers: imports, function names, config keys
|
|
271
|
+
const imports = [...allContent.matchAll(/import\s+\{([^}]+)\}/g)].map(m => m[1].trim()).join(', ');
|
|
272
|
+
const firstCodeBlock = allContent.match(/```\w*\n([\s\S]*?)```/)?.[1]?.slice(0, 200)?.trim();
|
|
273
|
+
|
|
274
|
+
if (imports || firstCodeBlock) {
|
|
275
|
+
const parts = [thinCorrection];
|
|
276
|
+
if (imports) parts.push(`Key exports: ${imports.slice(0, 150)}`);
|
|
277
|
+
if (firstCodeBlock) parts.push(`Usage: ${firstCodeBlock.split('\n').slice(0, 3).join(' ').slice(0, 150)}`);
|
|
278
|
+
parts.push('If unsure, call lookup with a specific question.');
|
|
279
|
+
richCorrection = parts.join(' | ');
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
} catch { /* best-effort — fall back to thin correction */ }
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
id: `gap:${gap.name}`,
|
|
288
|
+
source: 'gap-detector',
|
|
289
|
+
type: 'post-training',
|
|
290
|
+
match: {
|
|
291
|
+
codeTerms: [],
|
|
292
|
+
filePatterns: ['**'],
|
|
293
|
+
importTriggers: [gap.name],
|
|
294
|
+
functionPatterns: [],
|
|
295
|
+
},
|
|
296
|
+
injection: {
|
|
297
|
+
correction: richCorrection,
|
|
298
|
+
constraint: null,
|
|
299
|
+
example: null,
|
|
300
|
+
},
|
|
301
|
+
};
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
return { version: 1, builtAt: new Date().toISOString(), items: mapped };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Add a single item to an existing context map.
|
|
309
|
+
* @param {{version: number, builtAt: string, items: Array}} map
|
|
310
|
+
* @param {{id: string, text: string, source?: string, type?: string}} item
|
|
311
|
+
* @returns {Promise<{version: number, builtAt: string, items: Array}>}
|
|
312
|
+
*/
|
|
313
|
+
async addItem(map, item) {
|
|
314
|
+
const entry = {
|
|
315
|
+
id: item.id,
|
|
316
|
+
source: item.source ?? null,
|
|
317
|
+
type: item.type ?? null,
|
|
318
|
+
match: {
|
|
319
|
+
...extractKeywords(item.text),
|
|
320
|
+
functionPatterns: [],
|
|
321
|
+
},
|
|
322
|
+
injection: buildInjectionText(item),
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
if (this.processingMode !== 'fast') {
|
|
326
|
+
await this._inferScopes([entry], [item]);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
map.items.push(entry);
|
|
330
|
+
return map;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Write a context map to disk as JSON.
|
|
335
|
+
* @param {string} filePath
|
|
336
|
+
* @param {{version: number, builtAt: string, items: Array}} map
|
|
337
|
+
*/
|
|
338
|
+
save(filePath, map) {
|
|
339
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
340
|
+
writeFileSync(filePath, JSON.stringify(map, null, 2), 'utf8');
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Load a context map from disk. Returns null if missing or corrupt.
|
|
345
|
+
* @param {string} filePath
|
|
346
|
+
* @returns {{version: number, builtAt: string, items: Array} | null}
|
|
347
|
+
*/
|
|
348
|
+
static load(filePath) {
|
|
349
|
+
try {
|
|
350
|
+
const raw = readFileSync(filePath, 'utf8');
|
|
351
|
+
return JSON.parse(raw);
|
|
352
|
+
} catch {
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// ── Private helpers ─────────────────────────────────────────────────────
|
|
358
|
+
|
|
359
|
+
/** @returns {{version: number, builtAt: string, items: Array}} */
|
|
360
|
+
_emptyMap() {
|
|
361
|
+
return { version: 1, builtAt: new Date().toISOString(), items: [] };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Batch LLM inference to populate functionPatterns and importTriggers.
|
|
366
|
+
* Processes items in batches of LLM_BATCH_SIZE.
|
|
367
|
+
*/
|
|
368
|
+
async _inferScopes(mapped, sourceItems) {
|
|
369
|
+
for (let i = 0; i < mapped.length; i += LLM_BATCH_SIZE) {
|
|
370
|
+
const batchMapped = mapped.slice(i, i + LLM_BATCH_SIZE);
|
|
371
|
+
const batchSource = sourceItems.slice(i, i + LLM_BATCH_SIZE);
|
|
372
|
+
|
|
373
|
+
const prompt = this._buildScopePrompt(batchSource);
|
|
374
|
+
const response = await this._callLLM(prompt);
|
|
375
|
+
this._applyScopeResponse(batchMapped, response);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Build a prompt asking the LLM for functionPatterns and importTriggers.
|
|
381
|
+
* @param {Array<{id: string, text: string}>} items
|
|
382
|
+
* @returns {string}
|
|
383
|
+
*/
|
|
384
|
+
_buildScopePrompt(items) {
|
|
385
|
+
const itemList = items
|
|
386
|
+
.map((it, idx) => `[${idx}] id="${it.id}": ${it.text.slice(0, 300)}`)
|
|
387
|
+
.join('\n');
|
|
388
|
+
|
|
389
|
+
return [
|
|
390
|
+
'For each knowledge item below, return JSON: an array of objects with',
|
|
391
|
+
'{ "index": number, "functionPatterns": string[], "importTriggers": string[] }.',
|
|
392
|
+
'functionPatterns: regex patterns for function/method names this rule applies to.',
|
|
393
|
+
'importTriggers: npm/pip package names that signal this rule is relevant.',
|
|
394
|
+
'Return ONLY valid JSON array, no markdown fences.',
|
|
395
|
+
'',
|
|
396
|
+
itemList,
|
|
397
|
+
].join('\n');
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Parse LLM response and merge functionPatterns/importTriggers into mapped items.
|
|
402
|
+
*/
|
|
403
|
+
_applyScopeResponse(mapped, response) {
|
|
404
|
+
if (!response) return;
|
|
405
|
+
|
|
406
|
+
try {
|
|
407
|
+
const cleaned = response.replace(/```[\w]*\n?/g, '').trim();
|
|
408
|
+
const parsed = JSON.parse(cleaned);
|
|
409
|
+
if (!Array.isArray(parsed)) return;
|
|
410
|
+
|
|
411
|
+
for (const entry of parsed) {
|
|
412
|
+
const target = mapped[entry.index];
|
|
413
|
+
if (!target) continue;
|
|
414
|
+
if (Array.isArray(entry.functionPatterns)) {
|
|
415
|
+
target.match.functionPatterns = entry.functionPatterns;
|
|
416
|
+
}
|
|
417
|
+
if (Array.isArray(entry.importTriggers)) {
|
|
418
|
+
const merged = new Set([...target.match.importTriggers, ...entry.importTriggers]);
|
|
419
|
+
target.match.importTriggers = [...merged];
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
} catch {
|
|
423
|
+
// LLM returned unparseable response; skip gracefully
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Call the configured LLM. Override this method in tests.
|
|
429
|
+
* @param {string} prompt
|
|
430
|
+
* @returns {Promise<string | null>}
|
|
431
|
+
*/
|
|
432
|
+
async _callLLM(prompt) {
|
|
433
|
+
const mode = this.processingMode;
|
|
434
|
+
|
|
435
|
+
if (mode === 'api' && this.apiKey) {
|
|
436
|
+
return this._callApiLLM(prompt);
|
|
437
|
+
}
|
|
438
|
+
if (mode === 'local') {
|
|
439
|
+
return this._callOllamaLLM(prompt);
|
|
440
|
+
}
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/** Call Anthropic or OpenAI API based on key prefix. */
|
|
445
|
+
async _callApiLLM(prompt) {
|
|
446
|
+
const isAnthropic = this.apiKey.startsWith('sk-ant-');
|
|
447
|
+
const url = isAnthropic
|
|
448
|
+
? 'https://api.anthropic.com/v1/messages'
|
|
449
|
+
: 'https://api.openai.com/v1/chat/completions';
|
|
450
|
+
|
|
451
|
+
const body = isAnthropic
|
|
452
|
+
? { model: 'claude-sonnet-4-20250514', max_tokens: 2048, messages: [{ role: 'user', content: prompt }] }
|
|
453
|
+
: { model: 'gpt-4o-mini', messages: [{ role: 'user', content: prompt }], max_tokens: 2048 };
|
|
454
|
+
|
|
455
|
+
const headers = isAnthropic
|
|
456
|
+
? { 'Content-Type': 'application/json', 'x-api-key': this.apiKey, 'anthropic-version': '2023-06-01' }
|
|
457
|
+
: { 'Content-Type': 'application/json', Authorization: `Bearer ${this.apiKey}` };
|
|
458
|
+
|
|
459
|
+
const res = await fetch(url, { method: 'POST', headers, body: JSON.stringify(body) });
|
|
460
|
+
if (!res.ok) return null;
|
|
461
|
+
|
|
462
|
+
const data = await res.json();
|
|
463
|
+
return isAnthropic ? data.content?.[0]?.text : data.choices?.[0]?.message?.content;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/** Call local Ollama instance. */
|
|
467
|
+
async _callOllamaLLM(prompt) {
|
|
468
|
+
const model = this.ollamaModel ?? 'llama3';
|
|
469
|
+
const url = 'http://localhost:11434/api/generate';
|
|
470
|
+
const body = { model, prompt, stream: false };
|
|
471
|
+
|
|
472
|
+
try {
|
|
473
|
+
const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
|
474
|
+
if (!res.ok) return null;
|
|
475
|
+
const data = await res.json();
|
|
476
|
+
return data.response ?? null;
|
|
477
|
+
} catch {
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// ── ContextMapMatcher ───────────────────────────────────────────────────────
|
|
484
|
+
|
|
485
|
+
/** Prohibition patterns for contradiction checking. */
|
|
486
|
+
const PROHIBITION_RE = /(?:do not use|don't use|never use|avoid|deprecated|prefer\s+(\S+)\s+over)\s+(\S+)/gi;
|
|
487
|
+
|
|
488
|
+
export class ContextMapMatcher {
|
|
489
|
+
/**
|
|
490
|
+
* @param {Array} items - items from a loaded context map
|
|
491
|
+
*/
|
|
492
|
+
constructor(items) {
|
|
493
|
+
this.items = items ?? [];
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Match context map items against a file path, code block, and imports.
|
|
498
|
+
* Returns matched items sorted by strength (desc), capped at MAX_MATCHES.
|
|
499
|
+
*
|
|
500
|
+
* @param {string} filePath - current file being edited
|
|
501
|
+
* @param {string} codeBlock - code content
|
|
502
|
+
* @param {string[]} imports - imported package names
|
|
503
|
+
* @returns {Array} matched items with _strength property
|
|
504
|
+
*/
|
|
505
|
+
match(filePath, codeBlock, imports) {
|
|
506
|
+
const importSet = new Set((imports ?? []).map(i => i.toLowerCase()));
|
|
507
|
+
const codeLower = (codeBlock ?? '').toLowerCase();
|
|
508
|
+
|
|
509
|
+
const scored = [];
|
|
510
|
+
for (const item of this.items) {
|
|
511
|
+
const strength = this._scoreItem(item, filePath, codeLower, importSet);
|
|
512
|
+
// Post-training items: any match is enough (import trigger required by the check below)
|
|
513
|
+
// Team knowledge: require stronger signal to avoid broad keyword noise
|
|
514
|
+
const minStrength = item.type === 'post-training' ? 1 : MIN_STRENGTH_TEAM;
|
|
515
|
+
if (strength >= minStrength) scored.push({ ...item, _strength: strength });
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
scored.sort((a, b) => b._strength - a._strength);
|
|
519
|
+
return scored.slice(0, MAX_MATCHES);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Check for contradictions between new code and matched knowledge items.
|
|
524
|
+
* @param {string} newCode - code being written
|
|
525
|
+
* @param {Array} matchedItems - items returned by match()
|
|
526
|
+
* @returns {Array<{id: string, constraint: string, example: string | null, source: string | null}>}
|
|
527
|
+
*/
|
|
528
|
+
checkContradictions(newCode, matchedItems) {
|
|
529
|
+
if (!newCode || !matchedItems?.length) return [];
|
|
530
|
+
|
|
531
|
+
const codeLower = newCode.toLowerCase();
|
|
532
|
+
const violations = [];
|
|
533
|
+
|
|
534
|
+
for (const item of matchedItems) {
|
|
535
|
+
const text = item.injection?.constraint ?? '';
|
|
536
|
+
const found = this._findProhibitions(text, codeLower);
|
|
537
|
+
if (found.length > 0) {
|
|
538
|
+
violations.push({
|
|
539
|
+
id: item.id,
|
|
540
|
+
constraint: found.join('; '),
|
|
541
|
+
example: item.injection?.example ?? null,
|
|
542
|
+
source: item.source ?? null,
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
return violations;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// ── Private helpers ─────────────────────────────────────────────────────
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Score a single item against the current context.
|
|
554
|
+
* Post-training items with '**' filePattern require importTriggers to match.
|
|
555
|
+
*/
|
|
556
|
+
_scoreItem(item, filePath, codeLower, importSet) {
|
|
557
|
+
let strength = 0;
|
|
558
|
+
|
|
559
|
+
const m = item.match ?? {};
|
|
560
|
+
const importMatch = this._matchImports(m.importTriggers, importSet);
|
|
561
|
+
if (importMatch) strength += STRENGTH_IMPORT;
|
|
562
|
+
|
|
563
|
+
const fnMatch = this._matchFunctionPatterns(m.functionPatterns, codeLower);
|
|
564
|
+
if (fnMatch) strength += STRENGTH_FUNCTION;
|
|
565
|
+
|
|
566
|
+
const termMatch = this._matchCodeTerms(m.codeTerms, codeLower);
|
|
567
|
+
if (termMatch) strength += STRENGTH_CODE_TERM;
|
|
568
|
+
|
|
569
|
+
const fileMatch = this._matchFilePatterns(m.filePatterns, filePath);
|
|
570
|
+
if (fileMatch) strength += STRENGTH_FILE_PATTERN;
|
|
571
|
+
|
|
572
|
+
// Post-training items with only '**' glob must have importTriggers match
|
|
573
|
+
if (item.type === 'post-training' && this._hasOnlyWildcard(m.filePatterns) && !importMatch) {
|
|
574
|
+
return 0;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
return strength;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/** Check if any importTrigger matches the import set. */
|
|
581
|
+
_matchImports(triggers, importSet) {
|
|
582
|
+
if (!triggers?.length) return false;
|
|
583
|
+
return triggers.some(t => {
|
|
584
|
+
const tLower = t.toLowerCase();
|
|
585
|
+
// Exact match: "next" === "next"
|
|
586
|
+
if (importSet.has(tLower)) return true;
|
|
587
|
+
// Subpath match: trigger "next" matches import "next/navigation"
|
|
588
|
+
for (const imp of importSet) {
|
|
589
|
+
if (imp.startsWith(tLower + '/')) return true;
|
|
590
|
+
}
|
|
591
|
+
return false;
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/** Check if any functionPattern regex matches the code. */
|
|
596
|
+
_matchFunctionPatterns(patterns, codeLower) {
|
|
597
|
+
if (!patterns?.length) return false;
|
|
598
|
+
return patterns.some(p => {
|
|
599
|
+
try {
|
|
600
|
+
return new RegExp(p, 'i').test(codeLower);
|
|
601
|
+
} catch {
|
|
602
|
+
return false;
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/** Check if any code term appears in the code block. */
|
|
608
|
+
_matchCodeTerms(terms, codeLower) {
|
|
609
|
+
if (!terms?.length) return false;
|
|
610
|
+
return terms.some(t => codeLower.includes(t.toLowerCase()));
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/** Check if any file pattern matches the file path. */
|
|
614
|
+
_matchFilePatterns(patterns, filePath) {
|
|
615
|
+
if (!patterns?.length || !filePath) return false;
|
|
616
|
+
return patterns.some(p => simpleGlob(p, filePath));
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/** True when filePatterns is exactly ['**'] or empty. */
|
|
620
|
+
_hasOnlyWildcard(patterns) {
|
|
621
|
+
if (!patterns?.length) return true;
|
|
622
|
+
return patterns.length === 1 && patterns[0] === '**';
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/** Extract prohibition patterns and check if code violates them. */
|
|
626
|
+
_findProhibitions(constraintText, codeLower) {
|
|
627
|
+
const violations = [];
|
|
628
|
+
PROHIBITION_RE.lastIndex = 0;
|
|
629
|
+
|
|
630
|
+
let match;
|
|
631
|
+
while ((match = PROHIBITION_RE.exec(constraintText)) !== null) {
|
|
632
|
+
// match[1] = preferred term (from "prefer X over Y"), match[2] = prohibited term
|
|
633
|
+
const prohibited = (match[2] ?? '').toLowerCase().replace(/[.,;:!?]/g, '');
|
|
634
|
+
if (prohibited && codeLower.includes(prohibited)) {
|
|
635
|
+
violations.push(match[0].trim());
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
return violations;
|
|
640
|
+
}
|
|
641
|
+
}
|