kibi-plugin-builtin 0.2.0 → 0.4.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 +57 -4
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -2
- package/dist/symbols/ts-morph-enrichment.js +5 -5
- package/dist/symbols/ts-morph-extractor.d.ts +3 -1
- package/dist/symbols/ts-morph-extractor.d.ts.map +1 -1
- package/dist/symbols/ts-morph-extractor.js +259 -39
- package/dist/symbols/ts-morph-granularity.js +2 -2
- package/dist/symbols/ts-morph-runtime.d.ts +10 -0
- package/dist/symbols/ts-morph-runtime.d.ts.map +1 -0
- package/dist/symbols/ts-morph-runtime.js +31 -0
- package/dist/symbols/ts-morph-shared.d.ts +1 -1
- package/dist/symbols/ts-morph-shared.d.ts.map +1 -1
- package/dist/symbols/ts-morph-shared.js +7 -7
- package/dist/vocabulary/builtin-vocabulary-alignment.d.ts +47 -0
- package/dist/vocabulary/builtin-vocabulary-alignment.d.ts.map +1 -0
- package/dist/vocabulary/builtin-vocabulary-alignment.js +261 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,17 +1,72 @@
|
|
|
1
1
|
# kibi-plugin-builtin
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- db5376c: Kibi can now analyze source in Python, Go, Rust and a broader set of common programming and configuration languages, offline, while JavaScript and TypeScript keep working as before. Staged checks read source and authored knowledge from one immutable Git tree, inspect both sides of every change, and keep incomplete analysis explicit instead of treating it as proof. Large files now return an explicit analysis failure instead of a second validation error.
|
|
8
|
+
- Add the asynchronous `kibi.symbol-extractor.v2` contract with validated UTF-16 ranges, bounded inputs and host-assigned provenance, plus a ts-morph v2 adapter.
|
|
9
|
+
- Add the optional `kibi-plugin-treesitter` package with pinned WASM grammars, queries, licenses and an approved-analyzer closure verified before import.
|
|
10
|
+
- Capture staged and explicit-diff snapshots from Git objects; merge duplicate symbol-manifest records deterministically and fail conflicting authored fields with `SOURCE_DUPLICATE_CONFLICT`.
|
|
11
|
+
- Add opt-in, bounded parser-phase and Prolog round-trip timing observations that never enter result schemas.
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- 8622782: An impact review prepared on one machine now still validates in CI, and the MCP server and the CLI agree on whether it is current. Checks only block on incomplete source analysis the author can act on: the committed side of a change never blocks, and a known analyzer limitation such as a Rust macro or a Python decorator matters only where it overlaps the changed lines. Syntax, parse, timeout and integrity problems in the staged file still block. Long-running hosts no longer grow memory with every analyzed file, and Tree-sitter analysis reuses warm workers instead of starting one per file.
|
|
16
|
+
- Identify the impact evaluator by its contract version and the canonical JSON of the schemas it consumes, instead of hashing the installed file tree and `process.version`. Identify the builtin analyzer runtime by its package identity rather than its installed tree.
|
|
17
|
+
- `kibi-runtime` bundles the CLI operations again (`kibi-cli` is a build-time dependency only); it no longer installs `kibi-cli`.
|
|
18
|
+
- Add one shared analysis gate (`analysisObligation`) used by staged checks, impact-review preparation and validation, and impact reports.
|
|
19
|
+
- Classify source paths from a single extension table in `source-classification`, with parity tests against the Tree-sitter catalog and the builtin extractor.
|
|
20
|
+
- Remove analyzed files from the ts-morph project after each v1 and v2 analysis.
|
|
21
|
+
- Add an optional `timeoutMs` budget to `SymbolExtractorV2AnalyzeInput`; the host sets it slightly below its own deadline, so providers report their own timeout. Tree-sitter uses a bounded, persistent worker pool with grammars loaded once per worker, and the host analyzes up to four changed files at a time.
|
|
22
|
+
|
|
23
|
+
- Updated dependencies [db5376c]
|
|
24
|
+
- Updated dependencies [8622782]
|
|
25
|
+
- kibi-plugin-sdk@0.4.0
|
|
26
|
+
|
|
27
|
+
## 0.3.0
|
|
28
|
+
|
|
29
|
+
### Minor Changes
|
|
30
|
+
|
|
31
|
+
- 0128b56: Kibi now helps agents keep one shared vocabulary instead of letting every requirement invent its own. `kibi check` warns when two unrelated requirements state the same obligation. Units are compared in canonical form, so "30 min" matches "1800 s", and each warning names the exact facts involved. It also flags subject keys copied from a requirement ID, subject keys that don't follow `component.aspect`, predicates whose arguments read like prose, and entity files whose name doesn't match their ID. All of these are non-blocking warnings or info notes, so existing knowledge bases keep passing.
|
|
32
|
+
|
|
33
|
+
When you model a new requirement, Kibi now ranks the subjects that already exist and either reuses one or explicitly declares a new one. It also flags claims that look like possible duplicates. An intentional restatement can be recorded with the new `restates` relationship. Skills and docs now recommend naming entities by the behavior they govern (`REQ-cli-gc`) instead of a sequence number (`REQ-042`). Existing numbered IDs stay valid.
|
|
34
|
+
|
|
35
|
+
Predicate schemas can now declare the allowed values for an argument, plus the old spellings that map onto them. New facts must use those values. `kibi check` reports predicate facts that don't match any schema, and `kibi migrate` can fix the mechanical cases after you approve the plan hash. It moves a fact to the only namespace whose schema matches, and rewrites old spellings to the declared value. Everything that needs judgment stays a review item.
|
|
36
|
+
- core: new `semantic_quality.pl` (`entity-id-style`, `domain-redundancy`, `domain-implication`, `subject-key-identity`, `subject-key-shape`, `ontology-quality`) and `units.pl`. Unit canonicalization is used for comparison only, and unknown or ambiguous units such as `KB` are never equated. Adds the `restates` req→req relationship and an optional `diagnosticSeverity` in the rule registry. Adds `:- encoding(utf8)` to modules that contain non-ASCII text.
|
|
37
|
+
- cli/mcp: `restates` is wired through the extractors, schemas, and mutation paths. `entity-id-style` warnings are reported on `kb_upsert` creates and on staged added or renamed entity files. `kb_model_requirement` returns `vocabularyAlignment` (subject decision, candidates, redundancy candidates, stamps, `fallbackUsed`). Ontology-quality thresholds can be set with `KIBI_ONTOLOGY_QUALITY_MAX_SINGLETON_RATIO` / `KIBI_ONTOLOGY_QUALITY_MIN_FACTS`. `kb_compile_intent` create mode now keeps a caller-supplied `requirementId`. `kibi check --staged` now also prints its pass line when metadata-only staged changes have only advisory findings, matching the staged-symbol path.
|
|
38
|
+
- cli/mcp: `predicate_schema` facts accept `argument_constants` and `argument_aliases`, stored like `rule_ir` as JSON. `kb_upsert` / `kb_validate_upsert` reject malformed vocabularies and predicate facts that use undeclared values or aliases. `kb_suggest_predicates` binds aliases to their constant and leaves undeclared values unbound. The new advisory TypeScript rule `predicate-schema-conformance` checks predicate facts against project schemas and the built-in catalog. Its mechanical repairs become automatic `predicate_schema_alignment` migration actions that carry the exact `kb_upsert` input and re-read the fact before writing.
|
|
39
|
+
- core: `ontology-quality` findings name the prose-like arguments.
|
|
40
|
+
- plugin-sdk: new `kibi.vocabulary-alignment.v1` capability (`rankSubjects`, `compareClaims`) with result validators.
|
|
41
|
+
- plugin-builtin: deterministic offline provider (IDF-weighted subject ranking and negation- and quantity-aware claim similarity).
|
|
42
|
+
- plugin-jev: Jev-backed provider. It runs in replace, augment, or shadow mode with builtin fallback, and makes network calls only when it is activated for `kb_model_requirement`. `kb_check` never calls a provider.
|
|
43
|
+
- runtime and agent adapters: regenerated skill mirrors with the naming guidance and `restates` direction docs.
|
|
44
|
+
|
|
45
|
+
### Patch Changes
|
|
46
|
+
|
|
47
|
+
- e5ab646: `kibi status` and every CLI command start faster on large knowledge bases. On
|
|
48
|
+
this repository, `kibi status` for a fresh KB drops from about 3 seconds to
|
|
49
|
+
about 1.7, and a command that needs neither schema validation nor symbol
|
|
50
|
+
extraction starts about 0.5 seconds sooner.
|
|
51
|
+
- `kb_status_json` skips the full-entity stale-reason scan when the KB is
|
|
52
|
+
fresh, because a fresh verdict already rules out every stale reason.
|
|
53
|
+
- `kibi-plugin-builtin` loads ts-morph (the TypeScript compiler) on first
|
|
54
|
+
symbol analysis instead of at import time.
|
|
55
|
+
- The CLI compiles its entity and relationship JSON schemas on first use.
|
|
56
|
+
|
|
57
|
+
- Updated dependencies [0128b56]
|
|
58
|
+
- kibi-plugin-sdk@0.3.0
|
|
59
|
+
|
|
3
60
|
## 0.2.0
|
|
4
61
|
|
|
5
62
|
### Minor Changes
|
|
6
63
|
|
|
7
64
|
- 783cc75: Coordinate enrichment, granularity candidate collection, and private-member helpers now live in `kibi-plugin-builtin`, so the CLI can analyze TypeScript/JavaScript symbols without depending on `ts-morph` directly. Hosts that only install `kibi-cli` still get the same enrichment and staged-symbol behavior through the builtin plugin.
|
|
8
|
-
|
|
9
65
|
- Export `enrichSymbolCoordinatesWithTsMorph`, `collectGranularityCandidates`, `onlyCandidate`, and `isPrivateClassMember` from `kibi-plugin-builtin`
|
|
10
66
|
- Thin-wrap those helpers from CLI `symbols-ts`, `symbol-granularity`, and `symbol-extract`
|
|
11
67
|
- Drop `ts-morph` from `kibi-cli` runtime dependencies (kept as a test-only devDependency for AST spies)
|
|
12
68
|
|
|
13
69
|
- 783cc75: Kibi ships a default capability plugin package so TypeScript symbol extraction, ontology matching, and semantic lane classification live behind the public plugin protocol instead of private CLI paths.
|
|
14
|
-
|
|
15
70
|
- Add `kibi-plugin-builtin@0.1.0` with `kibiPlugin` exporting all three capabilities
|
|
16
71
|
- Built-in ts-morph extractor, ontology pack (launcher→core→policy→product→product-tail), and sync semantic classifier
|
|
17
72
|
- CLI depends on this package and thin-wraps the extractor / ontology pack for default parity
|
|
@@ -19,12 +74,10 @@
|
|
|
19
74
|
### Patch Changes
|
|
20
75
|
|
|
21
76
|
- b375e8f: This maintenance update brings the affected package code and tests into line with Kibi's Biome checks while preserving runtime behavior. It also replaces MCP non-null assertions with receiver-preserving method calls.
|
|
22
|
-
|
|
23
77
|
- Format affected files, sort imports, and remove unnecessary template literals.
|
|
24
78
|
- Preserve EngineClient `this` when forwarding optional Prolog methods.
|
|
25
79
|
|
|
26
80
|
- e6571b4: External classifiers stay limited to the two allowlisted operations, ontology matches keep their claim keys through host composition, and CLI predicate rule tables now re-export the builtin pack so advisor and modeling cannot drift. Lane selection from the builtin classifier also accepts host snake_case signal shapes, which unblocks typecheck/build for capability-plugin call sites.
|
|
27
|
-
|
|
28
81
|
- fix(builtin): chooseLane accepts kind-only LaneSignal (unblocks analysis-receipt typecheck)
|
|
29
82
|
- feat(cli): stamp claimKey on composed ontology match candidates
|
|
30
83
|
- refactor(cli): re-export predicate rule tables from kibi-plugin-builtin
|
package/dist/index.d.ts
CHANGED
|
@@ -8,10 +8,11 @@ export { PRODUCT_PREDICATE_RULES } from "./ontology/predicate-rules-product.js";
|
|
|
8
8
|
export { PRODUCT_TAIL_PREDICATE_RULES } from "./ontology/predicate-rules-product-tail.js";
|
|
9
9
|
export { normalizeKey, normalizePredicateToken, normalizeSubjectKey, singularize, commaList, } from "./ontology/normalize.js";
|
|
10
10
|
export { createBuiltinSemanticClassifier, BuiltinSemanticClassifier, detectSignals, chooseLane, } from "./semantic/builtin-classifier.js";
|
|
11
|
-
export { createBuiltinTsMorphSymbolExtractor, createBuiltinTsMorphSourceAnalysisProvider, } from "./symbols/ts-morph-extractor.js";
|
|
11
|
+
export { createBuiltinTsMorphSymbolExtractor, createBuiltinTsMorphSymbolExtractorV2, createBuiltinTsMorphSourceAnalysisProvider, } from "./symbols/ts-morph-extractor.js";
|
|
12
12
|
export { enrichSymbolCoordinatesWithTsMorph, type ManifestSymbolEntry, type SymbolCoordinates, } from "./symbols/ts-morph-enrichment.js";
|
|
13
13
|
export { collectGranularityCandidates, type GranularitySymbolCandidate, } from "./symbols/ts-morph-granularity.js";
|
|
14
14
|
export { isPrivateClassMember, onlyCandidate, } from "./symbols/ts-morph-shared.js";
|
|
15
|
+
export { BUILTIN_CLAIM_SIMILARITY_THRESHOLD, BUILTIN_SUBJECT_REUSE_THRESHOLD, BuiltinVocabularyAlignment, builtinSubjectDecision, claimSimilarity, createBuiltinVocabularyAlignment, rankSubjectCandidates, vocabularyTokens, type SubjectVocabularyEntry, } from "./vocabulary/builtin-vocabulary-alignment.js";
|
|
15
16
|
/**
|
|
16
17
|
* Default Kibi capability plugin.
|
|
17
18
|
*/
|
|
@@ -28,6 +29,8 @@ export declare const kibiPlugin: {
|
|
|
28
29
|
semanticClassifier: import("kibi-plugin-sdk").SemanticClassifierV1;
|
|
29
30
|
ontologyPack: import("kibi-plugin-sdk").OntologyPackV1;
|
|
30
31
|
symbolExtractor: import("kibi-plugin-sdk").SymbolExtractorV1;
|
|
32
|
+
symbolExtractorV2: import("kibi-plugin-sdk").SymbolExtractorV2;
|
|
33
|
+
vocabularyAlignment: import("./index.js").BuiltinVocabularyAlignment;
|
|
31
34
|
};
|
|
32
35
|
};
|
|
33
36
|
export default kibiPlugin;
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAiCA,OAAO,EAAE,yBAAyB,EAAE,MAAM,qCAAqC,CAAC;AAEhF,OAAO,EACL,mBAAmB,EACnB,2BAA2B,GAC5B,MAAM,qCAAqC,CAAC;AAE7C,OAAO,EACL,oBAAoB,EACpB,WAAW,EACX,KAAK,aAAa,GACnB,MAAM,8BAA8B,CAAC;AAEtC,OAAO,EAAE,oBAAoB,EAAE,MAAM,oCAAoC,CAAC;AAE1E,OAAO,EACL,wBAAwB,EACxB,0BAA0B,GAC3B,MAAM,wCAAwC,CAAC;AAEhD,OAAO,EAAE,sBAAsB,EAAE,MAAM,sCAAsC,CAAC;AAE9E,OAAO,EAAE,uBAAuB,EAAE,MAAM,uCAAuC,CAAC;AAEhF,OAAO,EAAE,4BAA4B,EAAE,MAAM,4CAA4C,CAAC;AAE1F,OAAO,EACL,YAAY,EACZ,uBAAuB,EACvB,mBAAmB,EACnB,WAAW,EACX,SAAS,GACV,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EACL,+BAA+B,EAC/B,yBAAyB,EACzB,aAAa,EACb,UAAU,GACX,MAAM,kCAAkC,CAAC;AAE1C,OAAO,EACL,mCAAmC,EACnC,qCAAqC,EACrC,0CAA0C,GAC3C,MAAM,iCAAiC,CAAC;AAEzC,OAAO,EACL,kCAAkC,EAClC,KAAK,mBAAmB,EACxB,KAAK,iBAAiB,GACvB,MAAM,kCAAkC,CAAC;AAE1C,OAAO,EACL,4BAA4B,EAC5B,KAAK,0BAA0B,GAChC,MAAM,mCAAmC,CAAC;AAE3C,OAAO,EACL,oBAAoB,EACpB,aAAa,GACd,MAAM,8BAA8B,CAAC;AAGtC,OAAO,EACL,kCAAkC,EAClC,+BAA+B,EAC/B,0BAA0B,EAC1B,sBAAsB,EACtB,eAAe,EACf,gCAAgC,EAChC,qBAAqB,EACrB,gBAAgB,EAChB,KAAK,sBAAsB,GAC5B,MAAM,8CAA8C,CAAC;AAEtD;;GAEG;AAEH,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;CAgBrB,CAAC;AAGH,eAAe,UAAU,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -19,7 +19,8 @@ import { createRequire } from "node:module";
|
|
|
19
19
|
import { KIBI_PLUGIN_API_VERSION, defineKibiPlugin } from "kibi-plugin-sdk";
|
|
20
20
|
import { createBuiltinOntologyPack } from "./ontology/builtin-ontology-pack.js";
|
|
21
21
|
import { createBuiltinSemanticClassifier } from "./semantic/builtin-classifier.js";
|
|
22
|
-
import { createBuiltinTsMorphSymbolExtractor } from "./symbols/ts-morph-extractor.js";
|
|
22
|
+
import { createBuiltinTsMorphSymbolExtractor, createBuiltinTsMorphSymbolExtractorV2, } from "./symbols/ts-morph-extractor.js";
|
|
23
|
+
import { createBuiltinVocabularyAlignment } from "./vocabulary/builtin-vocabulary-alignment.js";
|
|
23
24
|
const packageJson = createRequire(import.meta.url)("../package.json");
|
|
24
25
|
// implements REQ-capability-plugin-builtin-parity-v1
|
|
25
26
|
export { createBuiltinOntologyPack } from "./ontology/builtin-ontology-pack.js";
|
|
@@ -42,13 +43,15 @@ export { normalizeKey, normalizePredicateToken, normalizeSubjectKey, singularize
|
|
|
42
43
|
// implements REQ-capability-plugin-builtin-parity-v1
|
|
43
44
|
export { createBuiltinSemanticClassifier, BuiltinSemanticClassifier, detectSignals, chooseLane, } from "./semantic/builtin-classifier.js";
|
|
44
45
|
// implements REQ-capability-plugin-builtin-parity-v1
|
|
45
|
-
export { createBuiltinTsMorphSymbolExtractor, createBuiltinTsMorphSourceAnalysisProvider, } from "./symbols/ts-morph-extractor.js";
|
|
46
|
+
export { createBuiltinTsMorphSymbolExtractor, createBuiltinTsMorphSymbolExtractorV2, createBuiltinTsMorphSourceAnalysisProvider, } from "./symbols/ts-morph-extractor.js";
|
|
46
47
|
// implements REQ-capability-plugin-builtin-parity-v1
|
|
47
48
|
export { enrichSymbolCoordinatesWithTsMorph, } from "./symbols/ts-morph-enrichment.js";
|
|
48
49
|
// implements REQ-capability-plugin-builtin-parity-v1
|
|
49
50
|
export { collectGranularityCandidates, } from "./symbols/ts-morph-granularity.js";
|
|
50
51
|
// implements REQ-capability-plugin-builtin-parity-v1
|
|
51
52
|
export { isPrivateClassMember, onlyCandidate, } from "./symbols/ts-morph-shared.js";
|
|
53
|
+
// implements REQ-kibi-vocabulary-alignment-capability
|
|
54
|
+
export { BUILTIN_CLAIM_SIMILARITY_THRESHOLD, BUILTIN_SUBJECT_REUSE_THRESHOLD, BuiltinVocabularyAlignment, builtinSubjectDecision, claimSimilarity, createBuiltinVocabularyAlignment, rankSubjectCandidates, vocabularyTokens, } from "./vocabulary/builtin-vocabulary-alignment.js";
|
|
52
55
|
/**
|
|
53
56
|
* Default Kibi capability plugin.
|
|
54
57
|
*/
|
|
@@ -66,6 +69,8 @@ export const kibiPlugin = defineKibiPlugin({
|
|
|
66
69
|
semanticClassifier: createBuiltinSemanticClassifier(),
|
|
67
70
|
ontologyPack: createBuiltinOntologyPack(),
|
|
68
71
|
symbolExtractor: createBuiltinTsMorphSymbolExtractor(),
|
|
72
|
+
symbolExtractorV2: createBuiltinTsMorphSymbolExtractorV2(),
|
|
73
|
+
vocabularyAlignment: createBuiltinVocabularyAlignment(),
|
|
69
74
|
},
|
|
70
75
|
});
|
|
71
76
|
// implements REQ-capability-plugin-builtin-parity-v1
|
|
@@ -17,11 +17,11 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { access, readFile } from "node:fs/promises";
|
|
19
19
|
import * as path from "node:path";
|
|
20
|
-
import {
|
|
20
|
+
import { tsMorph } from "./ts-morph-runtime.js";
|
|
21
21
|
import { SUPPORTED_SOURCE_EXTENSIONS, onlyCandidate, } from "./ts-morph-shared.js";
|
|
22
22
|
// implements REQ-capability-plugin-builtin-parity-v1
|
|
23
23
|
export async function enrichSymbolCoordinatesWithTsMorph(entries, workspaceRoot) {
|
|
24
|
-
const project = new Project({
|
|
24
|
+
const project = new (tsMorph().Project)({
|
|
25
25
|
skipAddingFilesFromTsConfig: true,
|
|
26
26
|
});
|
|
27
27
|
const sourceFileCache = new Map();
|
|
@@ -146,7 +146,7 @@ function findNamedDeclaration(sourceFile, title) {
|
|
|
146
146
|
for (const declaration of statement.getDeclarations()) {
|
|
147
147
|
if (declaration.getName() !== qualifiedMethod.className)
|
|
148
148
|
continue;
|
|
149
|
-
const classExpression = declaration.getInitializerIfKind(SyntaxKind.ClassExpression);
|
|
149
|
+
const classExpression = declaration.getInitializerIfKind(tsMorph().SyntaxKind.ClassExpression);
|
|
150
150
|
const match = classExpression
|
|
151
151
|
? findClassMember(classExpression, qualifiedMethod.methodName)
|
|
152
152
|
: null;
|
|
@@ -248,7 +248,7 @@ function findNamedDeclaration(sourceFile, title) {
|
|
|
248
248
|
if (!statement.isExported())
|
|
249
249
|
continue;
|
|
250
250
|
for (const declaration of statement.getDeclarations()) {
|
|
251
|
-
const classExpression = declaration.getInitializerIfKind(SyntaxKind.ClassExpression);
|
|
251
|
+
const classExpression = declaration.getInitializerIfKind(tsMorph().SyntaxKind.ClassExpression);
|
|
252
252
|
if (!classExpression)
|
|
253
253
|
continue;
|
|
254
254
|
for (const method of classExpression.getMethods()) {
|
|
@@ -294,7 +294,7 @@ function findNamedDeclaration(sourceFile, title) {
|
|
|
294
294
|
if (!statement.isExported())
|
|
295
295
|
continue;
|
|
296
296
|
for (const declaration of statement.getDeclarations()) {
|
|
297
|
-
const classExpression = declaration.getInitializerIfKind(SyntaxKind.ClassExpression);
|
|
297
|
+
const classExpression = declaration.getInitializerIfKind(tsMorph().SyntaxKind.ClassExpression);
|
|
298
298
|
if (!classExpression)
|
|
299
299
|
continue;
|
|
300
300
|
for (const property of classExpression.getProperties()) {
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import type { SourceAnalysisProvider, SymbolExtractorV1 } from "kibi-plugin-sdk";
|
|
1
|
+
import type { SourceAnalysisProvider, SymbolExtractorV1, SymbolExtractorV2 } from "kibi-plugin-sdk";
|
|
2
2
|
/**
|
|
3
3
|
* Built-in ts-morph symbol extractor. Behavior matches the historical CLI
|
|
4
4
|
* `createTsMorphSourceAnalysisProvider` analysis path.
|
|
5
5
|
*/
|
|
6
6
|
export declare function createBuiltinTsMorphSymbolExtractor(): SymbolExtractorV1;
|
|
7
|
+
/** Built-in asynchronous ts-morph extractor for the source-bound v2 contract. */
|
|
8
|
+
export declare function createBuiltinTsMorphSymbolExtractorV2(): SymbolExtractorV2;
|
|
7
9
|
export declare function createBuiltinTsMorphSourceAnalysisProvider(): SourceAnalysisProvider;
|
|
8
10
|
//# sourceMappingURL=ts-morph-extractor.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ts-morph-extractor.d.ts","sourceRoot":"","sources":["../../src/symbols/ts-morph-extractor.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EACV,sBAAsB,
|
|
1
|
+
{"version":3,"file":"ts-morph-extractor.d.ts","sourceRoot":"","sources":["../../src/symbols/ts-morph-extractor.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EACV,sBAAsB,EAMtB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,iBAAiB,CAAC;AAsDzB;;;GAGG;AAEH,wBAAgB,mCAAmC,IAAI,iBAAiB,CA4CvE;AAED,iFAAiF;AAEjF,wBAAgB,qCAAqC,IAAI,iBAAiB,CAqHzE;AA+BD,wBAAgB,0CAA0C,IAAI,sBAAsB,CAEnF"}
|
|
@@ -17,63 +17,229 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import * as path from "node:path";
|
|
19
19
|
import { toSourceAnalysisProvider } from "kibi-plugin-sdk";
|
|
20
|
-
import {
|
|
20
|
+
import { tsMorph } from "./ts-morph-runtime.js";
|
|
21
21
|
import { SUPPORTED_SOURCE_EXTENSIONS, chooseScriptKind, formatMethodSymbolName, isPrivateClassMember, } from "./ts-morph-shared.js";
|
|
22
22
|
const EXTRACTOR_ID = "kibi-plugin-builtin.ts-morph";
|
|
23
|
+
function normalizeV2ScriptLanguage(language) {
|
|
24
|
+
const normalized = language?.trim().toLowerCase();
|
|
25
|
+
if (["javascript", "js", "node", "nodejs"].includes(normalized ?? "")) {
|
|
26
|
+
return "javascript";
|
|
27
|
+
}
|
|
28
|
+
if (["typescript", "ts"].includes(normalized ?? "")) {
|
|
29
|
+
return "typescript";
|
|
30
|
+
}
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
function v2ScriptKind(filePath, languageHint) {
|
|
34
|
+
const extension = path.extname(filePath).toLowerCase();
|
|
35
|
+
const language = normalizeV2ScriptLanguage(languageHint);
|
|
36
|
+
if (SUPPORTED_SOURCE_EXTENSIONS.has(extension)) {
|
|
37
|
+
if (languageHint !== undefined && language === undefined)
|
|
38
|
+
return undefined;
|
|
39
|
+
if (language !== undefined && language !== inferSourceLanguage(filePath)) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
return chooseScriptKind(filePath);
|
|
43
|
+
}
|
|
44
|
+
if (extension === "" && language !== undefined) {
|
|
45
|
+
return language === "typescript"
|
|
46
|
+
? tsMorph().ScriptKind.TS
|
|
47
|
+
: tsMorph().ScriptKind.JS;
|
|
48
|
+
}
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
23
51
|
/**
|
|
24
52
|
* Built-in ts-morph symbol extractor. Behavior matches the historical CLI
|
|
25
53
|
* `createTsMorphSourceAnalysisProvider` analysis path.
|
|
26
54
|
*/
|
|
27
55
|
// implements REQ-capability-plugin-builtin-parity-v1
|
|
28
56
|
export function createBuiltinTsMorphSymbolExtractor() {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
57
|
+
// Plugin registration creates the extractor on every CLI start; the project
|
|
58
|
+
// (and the TypeScript compiler behind it) is built on the first analysis.
|
|
59
|
+
let project;
|
|
60
|
+
const getProject = () => {
|
|
61
|
+
project ??= new (tsMorph().Project)({ skipAddingFilesFromTsConfig: true });
|
|
62
|
+
return project;
|
|
63
|
+
};
|
|
32
64
|
return {
|
|
33
65
|
id: EXTRACTOR_ID,
|
|
34
66
|
supports(input) {
|
|
35
67
|
return SUPPORTED_SOURCE_EXTENSIONS.has(path.extname(input.path).toLowerCase());
|
|
36
68
|
},
|
|
37
69
|
analyze(input) {
|
|
38
|
-
const sourceFile =
|
|
70
|
+
const sourceFile = getProject().createSourceFile(input.path, input.content, {
|
|
39
71
|
overwrite: true,
|
|
40
72
|
scriptKind: chooseScriptKind(input.path),
|
|
41
73
|
});
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
module: {
|
|
46
|
-
title: inferModuleTitle(input.path),
|
|
74
|
+
try {
|
|
75
|
+
return {
|
|
76
|
+
sourceFile: input.path,
|
|
47
77
|
language: inferSourceLanguage(input.path),
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
78
|
+
module: {
|
|
79
|
+
title: inferModuleTitle(input.path),
|
|
80
|
+
language: inferSourceLanguage(input.path),
|
|
81
|
+
analysisMode: "parser",
|
|
82
|
+
},
|
|
83
|
+
symbols: collectSourceSymbols(sourceFile),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
// Analysis is per file and purely syntactic; keeping files would grow
|
|
88
|
+
// the project without bound in a long-running daemon.
|
|
89
|
+
getProject().removeSourceFile(sourceFile);
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/** Built-in asynchronous ts-morph extractor for the source-bound v2 contract. */
|
|
95
|
+
// implements REQ-capability-plugin-builtin-parity-v1
|
|
96
|
+
export function createBuiltinTsMorphSymbolExtractorV2() {
|
|
97
|
+
// Like the v1 extractor, build the project (and load the TypeScript
|
|
98
|
+
// compiler) on the first analysis rather than at plugin registration.
|
|
99
|
+
let project;
|
|
100
|
+
const getProject = () => {
|
|
101
|
+
project ??= new (tsMorph().Project)({ skipAddingFilesFromTsConfig: true });
|
|
102
|
+
return project;
|
|
103
|
+
};
|
|
104
|
+
return {
|
|
105
|
+
id: `${EXTRACTOR_ID}.v2`,
|
|
106
|
+
supports(input) {
|
|
107
|
+
return v2ScriptKind(input.path, input.language) !== undefined;
|
|
108
|
+
},
|
|
109
|
+
async analyze(input) {
|
|
110
|
+
const extension = path.extname(input.path).toLowerCase();
|
|
111
|
+
const language = normalizeV2ScriptLanguage(input.language) ??
|
|
112
|
+
(input.language?.trim() ||
|
|
113
|
+
(SUPPORTED_SOURCE_EXTENSIONS.has(extension)
|
|
114
|
+
? inferSourceLanguage(input.path)
|
|
115
|
+
: "unknown"));
|
|
116
|
+
const module = {
|
|
117
|
+
title: inferModuleTitle(input.path),
|
|
118
|
+
language,
|
|
119
|
+
analysisMode: "parser",
|
|
51
120
|
};
|
|
121
|
+
const scriptKind = v2ScriptKind(input.path, input.language);
|
|
122
|
+
if (scriptKind === undefined) {
|
|
123
|
+
return {
|
|
124
|
+
contractVersion: "kibi.symbol-extractor.v2",
|
|
125
|
+
status: "unsupported",
|
|
126
|
+
sourceFile: input.path,
|
|
127
|
+
language,
|
|
128
|
+
module,
|
|
129
|
+
symbols: [],
|
|
130
|
+
diagnostics: [
|
|
131
|
+
{
|
|
132
|
+
code: "UNSUPPORTED_SOURCE_TYPE",
|
|
133
|
+
message: `The built-in ts-morph extractor does not support '${path.extname(input.path) || "extensionless"}' files`,
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
uncoveredRanges: [],
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
let sourceFile;
|
|
140
|
+
try {
|
|
141
|
+
sourceFile = getProject().createSourceFile(input.path, input.content, {
|
|
142
|
+
overwrite: true,
|
|
143
|
+
scriptKind,
|
|
144
|
+
});
|
|
145
|
+
const analyzed = sourceFile;
|
|
146
|
+
const diagnostics = analyzed
|
|
147
|
+
.getProject()
|
|
148
|
+
.getProgram()
|
|
149
|
+
.getSyntacticDiagnostics(analyzed)
|
|
150
|
+
.map((diagnostic) => {
|
|
151
|
+
const start = diagnostic.getStart();
|
|
152
|
+
const length = diagnostic.getLength();
|
|
153
|
+
const range = start === undefined
|
|
154
|
+
? wholeSourceRange(analyzed, input.content)
|
|
155
|
+
: sourceRangeAtOffsets(analyzed, input.content, start, start + (length ?? 0));
|
|
156
|
+
const message = diagnostic.getMessageText();
|
|
157
|
+
return {
|
|
158
|
+
code: `TS${diagnostic.getCode()}`,
|
|
159
|
+
message: typeof message === "string"
|
|
160
|
+
? message
|
|
161
|
+
: message.getMessageText(),
|
|
162
|
+
range,
|
|
163
|
+
};
|
|
164
|
+
});
|
|
165
|
+
const result = {
|
|
166
|
+
contractVersion: "kibi.symbol-extractor.v2",
|
|
167
|
+
status: diagnostics.length > 0 ? "partial" : "ok",
|
|
168
|
+
sourceFile: input.path,
|
|
169
|
+
language,
|
|
170
|
+
module,
|
|
171
|
+
symbols: collectSourceSymbols(analyzed, true),
|
|
172
|
+
diagnostics,
|
|
173
|
+
uncoveredRanges: diagnostics.map(({ range, code }) => ({
|
|
174
|
+
...range,
|
|
175
|
+
reason: `The TypeScript parser reported ${code} at this source span.`,
|
|
176
|
+
})),
|
|
177
|
+
};
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
return {
|
|
182
|
+
contractVersion: "kibi.symbol-extractor.v2",
|
|
183
|
+
status: "failed",
|
|
184
|
+
sourceFile: input.path,
|
|
185
|
+
language,
|
|
186
|
+
module,
|
|
187
|
+
symbols: [],
|
|
188
|
+
diagnostics: [
|
|
189
|
+
{
|
|
190
|
+
code: "SOURCE_ANALYSIS_FAILED",
|
|
191
|
+
message: error instanceof Error
|
|
192
|
+
? error.message
|
|
193
|
+
: "The built-in ts-morph extractor failed",
|
|
194
|
+
},
|
|
195
|
+
],
|
|
196
|
+
uncoveredRanges: [],
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
finally {
|
|
200
|
+
if (sourceFile)
|
|
201
|
+
getProject().removeSourceFile(sourceFile);
|
|
202
|
+
}
|
|
52
203
|
},
|
|
53
204
|
};
|
|
54
205
|
}
|
|
206
|
+
function sourceRangeAtOffsets(sourceFile, content, startOffset, endOffset) {
|
|
207
|
+
const boundedStart = Math.min(Math.max(startOffset, 0), content.length);
|
|
208
|
+
const boundedEnd = Math.min(Math.max(endOffset, boundedStart), content.length);
|
|
209
|
+
const start = sourceFile.getLineAndColumnAtPos(boundedStart);
|
|
210
|
+
const end = sourceFile.getLineAndColumnAtPos(boundedEnd);
|
|
211
|
+
return {
|
|
212
|
+
startLine: start.line,
|
|
213
|
+
startColumn: Math.max(0, start.column - 1),
|
|
214
|
+
endLine: end.line,
|
|
215
|
+
endColumn: Math.max(0, end.column - 1),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
function wholeSourceRange(sourceFile, content) {
|
|
219
|
+
return sourceRangeAtOffsets(sourceFile, content, 0, content.length);
|
|
220
|
+
}
|
|
55
221
|
// implements REQ-capability-plugin-builtin-parity-v1
|
|
56
222
|
export function createBuiltinTsMorphSourceAnalysisProvider() {
|
|
57
223
|
return toSourceAnalysisProvider(createBuiltinTsMorphSymbolExtractor());
|
|
58
224
|
}
|
|
59
|
-
function collectSourceSymbols(sourceFile) {
|
|
225
|
+
function collectSourceSymbols(sourceFile, includeV2Fields = false) {
|
|
60
226
|
const symbols = [];
|
|
61
227
|
for (const decl of sourceFile.getFunctions()) {
|
|
62
228
|
if (!decl.isExported())
|
|
63
229
|
continue;
|
|
64
|
-
pushSymbol(symbols, () => toSourceSymbolAnalysis(sourceFile, decl.getName() ?? "<anonymous>", "function", decl.getNameNode() ?? decl, decl,
|
|
230
|
+
pushSymbol(symbols, includeV2Fields, () => toSourceSymbolAnalysis(sourceFile, decl.getName() ?? "<anonymous>", "function", decl.getNameNode() ?? decl, decl,
|
|
65
231
|
// Functions: leading trivia via getFullText + JSDoc (historical CLI).
|
|
66
|
-
fullTextWithJsDocs(decl)));
|
|
232
|
+
fullTextWithJsDocs(decl), includeV2Fields));
|
|
67
233
|
}
|
|
68
234
|
for (const decl of sourceFile.getClasses()) {
|
|
69
235
|
if (!decl.isExported())
|
|
70
236
|
continue;
|
|
71
|
-
pushSymbol(symbols, () => toSourceSymbolAnalysis(sourceFile, decl.getName() ?? "<anonymous>", "class", decl.getNameNode() ?? decl, decl,
|
|
237
|
+
pushSymbol(symbols, includeV2Fields, () => toSourceSymbolAnalysis(sourceFile, decl.getName() ?? "<anonymous>", "class", decl.getNameNode() ?? decl, decl,
|
|
72
238
|
// Leading comments + JSDoc only — never the class body (method
|
|
73
239
|
// `// implements` must stay on the method symbol).
|
|
74
|
-
leadingCommentsAndJsDocs(sourceFile, decl)));
|
|
240
|
+
leadingCommentsAndJsDocs(sourceFile, decl), includeV2Fields));
|
|
75
241
|
try {
|
|
76
|
-
appendClassMembers(sourceFile, decl, decl.getName(), symbols);
|
|
242
|
+
appendClassMembers(sourceFile, decl, decl.getName(), symbols, includeV2Fields);
|
|
77
243
|
}
|
|
78
244
|
catch {
|
|
79
245
|
// Skip malformed class member walks; keep the class symbol.
|
|
@@ -82,42 +248,44 @@ function collectSourceSymbols(sourceFile) {
|
|
|
82
248
|
for (const decl of sourceFile.getInterfaces()) {
|
|
83
249
|
if (!decl.isExported())
|
|
84
250
|
continue;
|
|
85
|
-
pushSymbol(symbols, () => toSourceSymbolAnalysis(sourceFile, decl.getName() ?? "<anonymous>", "interface", decl.getNameNode() ?? decl, decl,
|
|
251
|
+
pushSymbol(symbols, includeV2Fields, () => toSourceSymbolAnalysis(sourceFile, decl.getName() ?? "<anonymous>", "interface", decl.getNameNode() ?? decl, decl,
|
|
86
252
|
// Historical CLI used getText(); keep calling it so characterization
|
|
87
253
|
// mocks that throw from getText still isolate the failure.
|
|
88
|
-
`${leadingCommentsAndJsDocs(sourceFile, decl)}\n${safeGetText(decl)}
|
|
254
|
+
`${leadingCommentsAndJsDocs(sourceFile, decl)}\n${safeGetText(decl)}`, includeV2Fields));
|
|
89
255
|
}
|
|
90
256
|
for (const decl of sourceFile.getTypeAliases()) {
|
|
91
257
|
if (!decl.isExported())
|
|
92
258
|
continue;
|
|
93
|
-
pushSymbol(symbols, () => toSourceSymbolAnalysis(sourceFile, decl.getName() ?? "<anonymous>", "type", decl.getNameNode() ?? decl, decl, `${leadingCommentsAndJsDocs(sourceFile, decl)}\n${safeGetText(decl)}
|
|
259
|
+
pushSymbol(symbols, includeV2Fields, () => toSourceSymbolAnalysis(sourceFile, decl.getName() ?? "<anonymous>", "type", decl.getNameNode() ?? decl, decl, `${leadingCommentsAndJsDocs(sourceFile, decl)}\n${safeGetText(decl)}`, includeV2Fields));
|
|
94
260
|
}
|
|
95
261
|
for (const decl of sourceFile.getEnums()) {
|
|
96
262
|
if (!decl.isExported())
|
|
97
263
|
continue;
|
|
98
|
-
pushSymbol(symbols, () => toSourceSymbolAnalysis(sourceFile, decl.getName() ?? "<anonymous>", "enum", decl.getNameNode() ?? decl, decl,
|
|
264
|
+
pushSymbol(symbols, includeV2Fields, () => toSourceSymbolAnalysis(sourceFile, decl.getName() ?? "<anonymous>", "enum", decl.getNameNode() ?? decl, decl,
|
|
99
265
|
// Enums historically used getText(); prefer leading comments when the
|
|
100
266
|
// real AST is available, else getText for test doubles.
|
|
101
|
-
`${leadingCommentsAndJsDocs(sourceFile, decl)}\n${safeGetText(decl)}
|
|
267
|
+
`${leadingCommentsAndJsDocs(sourceFile, decl)}\n${safeGetText(decl)}`, includeV2Fields));
|
|
102
268
|
}
|
|
103
269
|
for (const statement of sourceFile.getVariableStatements()) {
|
|
104
270
|
if (!statement.isExported())
|
|
105
271
|
continue;
|
|
106
272
|
for (const declaration of statement.getDeclarations()) {
|
|
107
|
-
pushSymbol(symbols, () => toSourceSymbolAnalysis(sourceFile, declaration.getName(), "variable", declaration.getNameNode() ?? declaration, declaration,
|
|
273
|
+
pushSymbol(symbols, includeV2Fields, () => toSourceSymbolAnalysis(sourceFile, declaration.getName(), "variable", declaration.getNameNode() ?? declaration, declaration,
|
|
108
274
|
// Statement carries leading `// implements`; declaration text is the
|
|
109
275
|
// historical fallback when only getText is stubbed in tests.
|
|
110
|
-
`${safeFullText(statement)}\n${safeGetText(declaration)}
|
|
276
|
+
`${safeFullText(statement)}\n${safeGetText(declaration)}`, includeV2Fields));
|
|
111
277
|
try {
|
|
112
278
|
const classExpression = typeof declaration.getInitializerIfKind === "function"
|
|
113
|
-
? declaration.getInitializerIfKind(SyntaxKind.ClassExpression)
|
|
279
|
+
? declaration.getInitializerIfKind(tsMorph().SyntaxKind.ClassExpression)
|
|
114
280
|
: undefined;
|
|
115
281
|
if (classExpression) {
|
|
116
|
-
appendClassMembers(sourceFile, classExpression, declaration.getName(), symbols);
|
|
282
|
+
appendClassMembers(sourceFile, classExpression, declaration.getName(), symbols, includeV2Fields);
|
|
117
283
|
}
|
|
118
284
|
}
|
|
119
|
-
catch {
|
|
120
|
-
|
|
285
|
+
catch (error) {
|
|
286
|
+
if (includeV2Fields)
|
|
287
|
+
throw error;
|
|
288
|
+
// Preserve legacy v1 class-expression recovery.
|
|
121
289
|
}
|
|
122
290
|
}
|
|
123
291
|
}
|
|
@@ -160,7 +328,7 @@ function safeFullText(node) {
|
|
|
160
328
|
function safeGetText(node) {
|
|
161
329
|
return typeof node.getText === "function" ? node.getText() : "";
|
|
162
330
|
}
|
|
163
|
-
function appendClassMembers(sourceFile, declaration, className, symbols) {
|
|
331
|
+
function appendClassMembers(sourceFile, declaration, className, symbols, includeV2Fields) {
|
|
164
332
|
// Inherit class *leading* ownership comments onto members (not the class
|
|
165
333
|
// body). Method-local `// implements` stay on the method via getFullText.
|
|
166
334
|
const classLeading = leadingCommentsAndJsDocs(sourceFile, declaration);
|
|
@@ -170,7 +338,7 @@ function appendClassMembers(sourceFile, declaration, className, symbols) {
|
|
|
170
338
|
for (const method of methods) {
|
|
171
339
|
if (isPrivateClassMember(method))
|
|
172
340
|
continue;
|
|
173
|
-
pushSymbol(symbols, () => toSourceSymbolAnalysis(sourceFile, formatMethodSymbolName(className, method.getName()), "method", method.getNameNode() ?? method, method, `${classLeading}\n${fullTextWithJsDocs(method)}
|
|
341
|
+
pushSymbol(symbols, includeV2Fields, () => toSourceSymbolAnalysis(sourceFile, formatMethodSymbolName(className, method.getName()), "method", method.getNameNode() ?? method, method, `${classLeading}\n${fullTextWithJsDocs(method)}`, includeV2Fields, className));
|
|
174
342
|
}
|
|
175
343
|
const properties = typeof declaration.getProperties === "function"
|
|
176
344
|
? declaration.getProperties()
|
|
@@ -178,7 +346,7 @@ function appendClassMembers(sourceFile, declaration, className, symbols) {
|
|
|
178
346
|
for (const property of properties) {
|
|
179
347
|
if (isPrivateClassMember(property))
|
|
180
348
|
continue;
|
|
181
|
-
pushSymbol(symbols, () => toSourceSymbolAnalysis(sourceFile, formatMethodSymbolName(className, property.getName()), "property", property.getNameNode() ?? property, property, `${classLeading}\n${fullTextWithJsDocs(property)}
|
|
349
|
+
pushSymbol(symbols, includeV2Fields, () => toSourceSymbolAnalysis(sourceFile, formatMethodSymbolName(className, property.getName()), "property", property.getNameNode() ?? property, property, `${classLeading}\n${fullTextWithJsDocs(property)}`, includeV2Fields, className));
|
|
182
350
|
}
|
|
183
351
|
const accessors = [
|
|
184
352
|
...(typeof declaration.getGetAccessors === "function"
|
|
@@ -188,24 +356,47 @@ function appendClassMembers(sourceFile, declaration, className, symbols) {
|
|
|
188
356
|
? declaration.getSetAccessors()
|
|
189
357
|
: []),
|
|
190
358
|
];
|
|
359
|
+
const pairedAccessors = new Set();
|
|
191
360
|
for (const accessor of accessors) {
|
|
192
361
|
if (isPrivateClassMember(accessor))
|
|
193
362
|
continue;
|
|
194
|
-
|
|
363
|
+
if (pairedAccessors.has(accessor))
|
|
364
|
+
continue;
|
|
365
|
+
if (includeV2Fields) {
|
|
366
|
+
const candidates = accessors.filter((candidate) => candidate.getName() === accessor.getName() &&
|
|
367
|
+
candidate.isStatic() === accessor.isStatic());
|
|
368
|
+
const other = candidates.find((candidate) => candidate !== accessor);
|
|
369
|
+
if (candidates.length === 2 &&
|
|
370
|
+
other &&
|
|
371
|
+
other.getKind() !== accessor.getKind()) {
|
|
372
|
+
const first = accessor.getStart() < other.getStart() ? accessor : other;
|
|
373
|
+
const last = first === accessor ? other : accessor;
|
|
374
|
+
// A complementary getter/setter pair is one authored property. Keep
|
|
375
|
+
// both bodies in its span without assigning the same ID twice.
|
|
376
|
+
const symbol = toSourceSymbolAnalysis(sourceFile, formatMethodSymbolName(className, accessor.getName()), "accessor", first.getNameNode() ?? first, last, `${classLeading}\n${fullTextWithJsDocs(first)}\n${fullTextWithJsDocs(last)}`, true, className);
|
|
377
|
+
symbols.push({ ...symbol, nativeKind: "AccessorPair" });
|
|
378
|
+
pairedAccessors.add(accessor);
|
|
379
|
+
pairedAccessors.add(other);
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
pushSymbol(symbols, includeV2Fields, () => toSourceSymbolAnalysis(sourceFile, formatMethodSymbolName(className, accessor.getName()), "accessor", accessor.getNameNode() ?? accessor, accessor, `${classLeading}\n${fullTextWithJsDocs(accessor)}`, includeV2Fields, className));
|
|
195
384
|
}
|
|
196
385
|
}
|
|
197
|
-
function pushSymbol(symbols, build) {
|
|
386
|
+
function pushSymbol(symbols, strict, build) {
|
|
198
387
|
try {
|
|
199
388
|
symbols.push(build());
|
|
200
389
|
}
|
|
201
|
-
catch {
|
|
202
|
-
|
|
390
|
+
catch (error) {
|
|
391
|
+
if (strict)
|
|
392
|
+
throw error;
|
|
393
|
+
// Preserve legacy v1 recovery; v2 must expose extraction failure.
|
|
203
394
|
}
|
|
204
395
|
}
|
|
205
|
-
function toSourceSymbolAnalysis(sourceFile, name, kind, startNode, endNode, directiveText) {
|
|
396
|
+
function toSourceSymbolAnalysis(sourceFile, name, kind, startNode, endNode, directiveText, includeV2Fields = false, containerName) {
|
|
206
397
|
const start = sourceFile.getLineAndColumnAtPos(startNode.getStart());
|
|
207
398
|
const end = sourceFile.getLineAndColumnAtPos(endNode.getEnd());
|
|
208
|
-
|
|
399
|
+
const result = {
|
|
209
400
|
name,
|
|
210
401
|
kind,
|
|
211
402
|
startLine: start.line,
|
|
@@ -214,6 +405,35 @@ function toSourceSymbolAnalysis(sourceFile, name, kind, startNode, endNode, dire
|
|
|
214
405
|
endColumn: Math.max(0, end.column - 1),
|
|
215
406
|
directiveText,
|
|
216
407
|
};
|
|
408
|
+
if (!includeV2Fields)
|
|
409
|
+
return result;
|
|
410
|
+
const nameKind = startNode.getKindName();
|
|
411
|
+
const hasNameNode = [
|
|
412
|
+
"Identifier",
|
|
413
|
+
"PrivateIdentifier",
|
|
414
|
+
"StringLiteral",
|
|
415
|
+
"NumericLiteral",
|
|
416
|
+
"NoSubstitutionTemplateLiteral",
|
|
417
|
+
"ComputedPropertyName",
|
|
418
|
+
].includes(nameKind);
|
|
419
|
+
const nameRangeStart = sourceFile.getLineAndColumnAtPos(startNode.getStart());
|
|
420
|
+
const nameRangeEnd = sourceFile.getLineAndColumnAtPos(startNode.getEnd());
|
|
421
|
+
return {
|
|
422
|
+
...result,
|
|
423
|
+
qualifiedName: name,
|
|
424
|
+
...(containerName ? { containerName } : {}),
|
|
425
|
+
nativeKind: endNode.getKindName(),
|
|
426
|
+
...(hasNameNode
|
|
427
|
+
? {
|
|
428
|
+
nameRange: {
|
|
429
|
+
startLine: nameRangeStart.line,
|
|
430
|
+
startColumn: Math.max(0, nameRangeStart.column - 1),
|
|
431
|
+
endLine: nameRangeEnd.line,
|
|
432
|
+
endColumn: Math.max(0, nameRangeEnd.column - 1),
|
|
433
|
+
},
|
|
434
|
+
}
|
|
435
|
+
: {}),
|
|
436
|
+
};
|
|
217
437
|
}
|
|
218
438
|
function inferSourceLanguage(filePath) {
|
|
219
439
|
const extension = path.extname(filePath).toLowerCase();
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
You should have received a copy of the GNU Affero General Public License
|
|
16
16
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
*/
|
|
18
|
-
import {
|
|
18
|
+
import { tsMorph } from "./ts-morph-runtime.js";
|
|
19
19
|
import { chooseScriptKind, isPrivateClassMember } from "./ts-morph-shared.js";
|
|
20
20
|
/**
|
|
21
21
|
* Collect exported (and unique bare method) symbol candidates used by
|
|
@@ -23,7 +23,7 @@ import { chooseScriptKind, isPrivateClassMember } from "./ts-morph-shared.js";
|
|
|
23
23
|
*/
|
|
24
24
|
// implements REQ-capability-plugin-builtin-parity-v1
|
|
25
25
|
export function collectGranularityCandidates(filePath, content) {
|
|
26
|
-
const source = new Project({
|
|
26
|
+
const source = new (tsMorph().Project)({
|
|
27
27
|
skipAddingFilesFromTsConfig: true,
|
|
28
28
|
}).createSourceFile(`${filePath}::granularity`, content, {
|
|
29
29
|
overwrite: true,
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type * as TsMorph from "ts-morph";
|
|
2
|
+
/**
|
|
3
|
+
* ts-morph loads the full TypeScript compiler (~150 ms). Consumers import this
|
|
4
|
+
* package for its predicate rules and plugin registry on every CLI command, so
|
|
5
|
+
* the compiler is loaded on first use by a symbol extractor instead of at
|
|
6
|
+
* import time. ts-morph is this package's own dependency, so resolving it from
|
|
7
|
+
* here always finds the declared version.
|
|
8
|
+
*/
|
|
9
|
+
export declare function tsMorph(): typeof TsMorph;
|
|
10
|
+
//# sourceMappingURL=ts-morph-runtime.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ts-morph-runtime.d.ts","sourceRoot":"","sources":["../../src/symbols/ts-morph-runtime.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,KAAK,OAAO,MAAM,UAAU,CAAC;AAIzC;;;;;;GAMG;AACH,wBAAgB,OAAO,IAAI,OAAO,OAAO,CAGxC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Kibi — repo-local, per-branch, queryable long-term memory for software projects
|
|
3
|
+
Copyright (C) 2026 Piotr Franczyk
|
|
4
|
+
|
|
5
|
+
This program is free software: you can redistribute it and/or modify
|
|
6
|
+
it under the terms of the GNU Affero General Public License as published by
|
|
7
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
(at your option) any later version.
|
|
9
|
+
|
|
10
|
+
This program is distributed in the hope that it will be useful,
|
|
11
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
GNU Affero General Public License for more details.
|
|
14
|
+
|
|
15
|
+
You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
*/
|
|
18
|
+
// implements REQ-capability-plugin-builtin-parity-v1
|
|
19
|
+
import { createRequire } from "node:module";
|
|
20
|
+
let loaded;
|
|
21
|
+
/**
|
|
22
|
+
* ts-morph loads the full TypeScript compiler (~150 ms). Consumers import this
|
|
23
|
+
* package for its predicate rules and plugin registry on every CLI command, so
|
|
24
|
+
* the compiler is loaded on first use by a symbol extractor instead of at
|
|
25
|
+
* import time. ts-morph is this package's own dependency, so resolving it from
|
|
26
|
+
* here always finds the declared version.
|
|
27
|
+
*/
|
|
28
|
+
export function tsMorph() {
|
|
29
|
+
loaded ??= createRequire(import.meta.url)("ts-morph");
|
|
30
|
+
return loaded;
|
|
31
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Scope, ScriptKind, SyntaxKind } from "ts-morph";
|
|
1
|
+
import type { Scope, ScriptKind, SyntaxKind } from "ts-morph";
|
|
2
2
|
export declare const SUPPORTED_SOURCE_EXTENSIONS: Set<string>;
|
|
3
3
|
export declare function chooseScriptKind(filePath: string): ScriptKind;
|
|
4
4
|
export declare function formatMethodSymbolName(className: string | undefined, methodName: string): string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ts-morph-shared.d.ts","sourceRoot":"","sources":["../../src/symbols/ts-morph-shared.ts"],"names":[],"mappings":"AAkBA,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"ts-morph-shared.d.ts","sourceRoot":"","sources":["../../src/symbols/ts-morph-shared.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAI9D,eAAO,MAAM,2BAA2B,aAStC,CAAC;AAGH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,CAY7D;AAGD,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,UAAU,EAAE,MAAM,GACjB,MAAM,CAER;AAED;;;GAGG;AAEH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE;IAC3C,OAAO,CAAC,EAAE,MAAM,MAAM,CAAC;IACvB,WAAW,CAAC,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC;IACxC,QAAQ,CAAC,EAAE,MAAM,KAAK,GAAG,MAAM,GAAG,MAAM,CAAC;CAC1C,GAAG,OAAO,CAYV;AAGD,wBAAgB,aAAa,CAAC,CAAC,EAAE,UAAU,EAAE,SAAS,CAAC,EAAE,GAAG,CAAC,GAAG,SAAS,CAGxE"}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
You should have received a copy of the GNU Affero General Public License
|
|
16
16
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
*/
|
|
18
|
-
import {
|
|
18
|
+
import { tsMorph } from "./ts-morph-runtime.js";
|
|
19
19
|
// implements REQ-capability-plugin-builtin-parity-v1
|
|
20
20
|
export const SUPPORTED_SOURCE_EXTENSIONS = new Set([
|
|
21
21
|
".ts",
|
|
@@ -31,15 +31,15 @@ export const SUPPORTED_SOURCE_EXTENSIONS = new Set([
|
|
|
31
31
|
export function chooseScriptKind(filePath) {
|
|
32
32
|
const lower = filePath.toLowerCase();
|
|
33
33
|
if (lower.endsWith(".tsx"))
|
|
34
|
-
return ScriptKind.TSX;
|
|
34
|
+
return tsMorph().ScriptKind.TSX;
|
|
35
35
|
if (lower.endsWith(".ts") ||
|
|
36
36
|
lower.endsWith(".mts") ||
|
|
37
37
|
lower.endsWith(".cts")) {
|
|
38
|
-
return ScriptKind.TS;
|
|
38
|
+
return tsMorph().ScriptKind.TS;
|
|
39
39
|
}
|
|
40
40
|
if (lower.endsWith(".jsx"))
|
|
41
|
-
return ScriptKind.JSX;
|
|
42
|
-
return ScriptKind.JS;
|
|
41
|
+
return tsMorph().ScriptKind.JSX;
|
|
42
|
+
return tsMorph().ScriptKind.JS;
|
|
43
43
|
}
|
|
44
44
|
// implements REQ-capability-plugin-builtin-parity-v1
|
|
45
45
|
export function formatMethodSymbolName(className, methodName) {
|
|
@@ -56,10 +56,10 @@ export function isPrivateClassMember(member) {
|
|
|
56
56
|
return true;
|
|
57
57
|
}
|
|
58
58
|
if (typeof member.hasModifier === "function") {
|
|
59
|
-
return member.hasModifier(SyntaxKind.PrivateKeyword);
|
|
59
|
+
return member.hasModifier(tsMorph().SyntaxKind.PrivateKeyword);
|
|
60
60
|
}
|
|
61
61
|
if (typeof member.getScope === "function") {
|
|
62
|
-
return member.getScope() === Scope.Private;
|
|
62
|
+
return member.getScope() === tsMorph().Scope.Private;
|
|
63
63
|
}
|
|
64
64
|
return false;
|
|
65
65
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type CompareClaimsInput, type CompareClaimsResult, type RankSubjectsInput, type RankSubjectsResult, type SubjectCandidate, type SubjectRankingDecision, type VocabularyAlignmentV1 } from "kibi-plugin-sdk";
|
|
2
|
+
/** Minimum builtin score for reusing an existing subject instead of a new one. */
|
|
3
|
+
export declare const BUILTIN_SUBJECT_REUSE_THRESHOLD = 0.4;
|
|
4
|
+
/** Conservative token-similarity bar for flagging a possible duplicate claim. */
|
|
5
|
+
export declare const BUILTIN_CLAIM_SIMILARITY_THRESHOLD = 0.75;
|
|
6
|
+
/** One existing subject fact as read from the KB. */
|
|
7
|
+
export type SubjectVocabularyEntry = Readonly<{
|
|
8
|
+
subjectKey: string;
|
|
9
|
+
title?: string;
|
|
10
|
+
factId?: string;
|
|
11
|
+
requirements?: readonly Readonly<{
|
|
12
|
+
id: string;
|
|
13
|
+
title?: string;
|
|
14
|
+
}>[];
|
|
15
|
+
}>;
|
|
16
|
+
/**
|
|
17
|
+
* Deterministic content tokens: lowercase alphanumeric words, stopwords
|
|
18
|
+
* removed, simple plural folding. Dots and underscores split subject keys.
|
|
19
|
+
*/
|
|
20
|
+
export declare function vocabularyTokens(text: string): string[];
|
|
21
|
+
/**
|
|
22
|
+
* Rank existing subjects for one clause. Score = 0.5 * subject-key match +
|
|
23
|
+
* 0.3 * subject-title match + 0.2 * best constraining-requirement-title match,
|
|
24
|
+
* where a match is the IDF-weighted Tversky index over the vocabulary (tokens shared
|
|
25
|
+
* by every subject, such as a product name, weigh little).
|
|
26
|
+
* Ties break by subject key, so the ranking is fully deterministic.
|
|
27
|
+
*/
|
|
28
|
+
export declare function rankSubjectCandidates(clauseText: string, vocabulary: readonly SubjectVocabularyEntry[], options?: Readonly<{
|
|
29
|
+
limit?: number;
|
|
30
|
+
}>): SubjectCandidate[];
|
|
31
|
+
/**
|
|
32
|
+
* Dice similarity over content tokens. Claims whose negation differs are never
|
|
33
|
+
* similar ("must expire" vs "must not expire"), and neither are claims whose
|
|
34
|
+
* quantities differ ("15 minutes" vs "60 minutes"): token overlap cannot tell
|
|
35
|
+
* which of two different bounds is meant, so the builtin stays conservative and
|
|
36
|
+
* leaves unit-converted paraphrases to Prolog canonicalization or a provider.
|
|
37
|
+
*/
|
|
38
|
+
export declare function claimSimilarity(left: string, right: string): number;
|
|
39
|
+
export declare function builtinSubjectDecision(clause: RankSubjectsInput["clauses"][number]): SubjectRankingDecision;
|
|
40
|
+
/** Deterministic, offline vocabulary-alignment provider. */
|
|
41
|
+
export declare class BuiltinVocabularyAlignment implements VocabularyAlignmentV1 {
|
|
42
|
+
readonly id = "kibi-plugin-builtin.vocabulary-alignment";
|
|
43
|
+
rankSubjects(input: RankSubjectsInput): RankSubjectsResult;
|
|
44
|
+
compareClaims(input: CompareClaimsInput): CompareClaimsResult;
|
|
45
|
+
}
|
|
46
|
+
export declare function createBuiltinVocabularyAlignment(): BuiltinVocabularyAlignment;
|
|
47
|
+
//# sourceMappingURL=builtin-vocabulary-alignment.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"builtin-vocabulary-alignment.d.ts","sourceRoot":"","sources":["../../src/vocabulary/builtin-vocabulary-alignment.ts"],"names":[],"mappings":"AAkBA,OAAO,EAEL,KAAK,kBAAkB,EACvB,KAAK,mBAAmB,EAExB,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,EACrB,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,EAC3B,MAAM,iBAAiB,CAAC;AAGzB,kFAAkF;AAElF,eAAO,MAAM,+BAA+B,MAAM,CAAC;AAEnD,iFAAiF;AAEjF,eAAO,MAAM,kCAAkC,OAAO,CAAC;AAEvD,qDAAqD;AAErD,MAAM,MAAM,sBAAsB,GAAG,QAAQ,CAAC;IAC5C,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,SAAS,QAAQ,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,EAAE,CAAC;CACpE,CAAC,CAAC;AA8CH;;;GAGG;AAEH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAOvD;AA6CD;;;;;;GAMG;AAEH,wBAAgB,qBAAqB,CACnC,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,SAAS,sBAAsB,EAAE,EAC7C,OAAO,GAAE,QAAQ,CAAC;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAM,GACzC,gBAAgB,EAAE,CAoDpB;AAYD;;;;;;GAMG;AAEH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAWnE;AAcD,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,iBAAiB,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,GAC3C,sBAAsB,CA4BxB;AAED,4DAA4D;AAE5D,qBAAa,0BAA2B,YAAW,qBAAqB;IACtE,QAAQ,CAAC,EAAE,8CAA8C;IAEzD,YAAY,CAAC,KAAK,EAAE,iBAAiB,GAAG,kBAAkB;IAI1D,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,mBAAmB;CAc9D;AAGD,wBAAgB,gCAAgC,IAAI,0BAA0B,CAE7E"}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Kibi — repo-local, per-branch, queryable long-term memory for software projects
|
|
3
|
+
Copyright (C) 2026 Piotr Franczyk
|
|
4
|
+
|
|
5
|
+
This program is free software: you can redistribute it and/or modify
|
|
6
|
+
it under the terms of the GNU Affero General Public License as published by
|
|
7
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
(at your option) any later version.
|
|
9
|
+
|
|
10
|
+
This program is distributed in the hope that it will be useful,
|
|
11
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
GNU Affero General Public License for more details.
|
|
14
|
+
|
|
15
|
+
You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
*/
|
|
18
|
+
import { NEW_SUBJECT_CHOICE, } from "kibi-plugin-sdk";
|
|
19
|
+
import { singularize } from "../ontology/normalize.js";
|
|
20
|
+
/** Minimum builtin score for reusing an existing subject instead of a new one. */
|
|
21
|
+
// implements REQ-kibi-vocabulary-alignment-capability
|
|
22
|
+
export const BUILTIN_SUBJECT_REUSE_THRESHOLD = 0.4;
|
|
23
|
+
/** Conservative token-similarity bar for flagging a possible duplicate claim. */
|
|
24
|
+
// implements REQ-kibi-vocabulary-alignment-capability
|
|
25
|
+
export const BUILTIN_CLAIM_SIMILARITY_THRESHOLD = 0.75;
|
|
26
|
+
const STOPWORDS = new Set([
|
|
27
|
+
"a",
|
|
28
|
+
"an",
|
|
29
|
+
"and",
|
|
30
|
+
"are",
|
|
31
|
+
"as",
|
|
32
|
+
"at",
|
|
33
|
+
"be",
|
|
34
|
+
"by",
|
|
35
|
+
"can",
|
|
36
|
+
"for",
|
|
37
|
+
"from",
|
|
38
|
+
"has",
|
|
39
|
+
"have",
|
|
40
|
+
"in",
|
|
41
|
+
"into",
|
|
42
|
+
"is",
|
|
43
|
+
"it",
|
|
44
|
+
"its",
|
|
45
|
+
"may",
|
|
46
|
+
"must",
|
|
47
|
+
"of",
|
|
48
|
+
"on",
|
|
49
|
+
"or",
|
|
50
|
+
"shall",
|
|
51
|
+
"should",
|
|
52
|
+
"so",
|
|
53
|
+
"than",
|
|
54
|
+
"that",
|
|
55
|
+
"the",
|
|
56
|
+
"their",
|
|
57
|
+
"then",
|
|
58
|
+
"this",
|
|
59
|
+
"to",
|
|
60
|
+
"was",
|
|
61
|
+
"when",
|
|
62
|
+
"which",
|
|
63
|
+
"while",
|
|
64
|
+
"will",
|
|
65
|
+
"with",
|
|
66
|
+
]);
|
|
67
|
+
const NEGATIONS = new Set(["not", "never", "no", "without", "forbid"]);
|
|
68
|
+
/**
|
|
69
|
+
* Deterministic content tokens: lowercase alphanumeric words, stopwords
|
|
70
|
+
* removed, simple plural folding. Dots and underscores split subject keys.
|
|
71
|
+
*/
|
|
72
|
+
// implements REQ-kibi-vocabulary-alignment-capability
|
|
73
|
+
export function vocabularyTokens(text) {
|
|
74
|
+
return text
|
|
75
|
+
.toLowerCase()
|
|
76
|
+
.replace(/['’]/g, "")
|
|
77
|
+
.split(/[^a-z0-9]+/)
|
|
78
|
+
.filter((token) => token.length > 0 && !STOPWORDS.has(token))
|
|
79
|
+
.map((token) => singularize(token));
|
|
80
|
+
}
|
|
81
|
+
function tokenSet(text) {
|
|
82
|
+
return new Set(vocabularyTokens(text));
|
|
83
|
+
}
|
|
84
|
+
function round3(value) {
|
|
85
|
+
return Math.round(value * 1000) / 1000;
|
|
86
|
+
}
|
|
87
|
+
/** Weight of clause tokens the subject does not explain (Tversky beta). */
|
|
88
|
+
const UNEXPLAINED_CLAUSE_WEIGHT = 0.2;
|
|
89
|
+
/**
|
|
90
|
+
* IDF-weighted Tversky index between the clause and one target token set.
|
|
91
|
+
* Target tokens the clause misses count fully; clause tokens the target does
|
|
92
|
+
* not explain count lightly, because a subject names a component rather than
|
|
93
|
+
* the whole clause. A specific subject that matches more of the clause
|
|
94
|
+
* therefore beats a short generic one that happens to be fully covered.
|
|
95
|
+
*/
|
|
96
|
+
function weightedMatch(clause, target, weight) {
|
|
97
|
+
let matched = 0;
|
|
98
|
+
let missedTarget = 0;
|
|
99
|
+
for (const token of target) {
|
|
100
|
+
const w = weight(token);
|
|
101
|
+
if (clause.has(token))
|
|
102
|
+
matched += w;
|
|
103
|
+
else
|
|
104
|
+
missedTarget += w;
|
|
105
|
+
}
|
|
106
|
+
if (matched === 0)
|
|
107
|
+
return 0;
|
|
108
|
+
let unexplainedClause = 0;
|
|
109
|
+
for (const token of clause) {
|
|
110
|
+
if (!target.has(token))
|
|
111
|
+
unexplainedClause += weight(token);
|
|
112
|
+
}
|
|
113
|
+
return (matched /
|
|
114
|
+
(matched + missedTarget + UNEXPLAINED_CLAUSE_WEIGHT * unexplainedClause));
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Rank existing subjects for one clause. Score = 0.5 * subject-key match +
|
|
118
|
+
* 0.3 * subject-title match + 0.2 * best constraining-requirement-title match,
|
|
119
|
+
* where a match is the IDF-weighted Tversky index over the vocabulary (tokens shared
|
|
120
|
+
* by every subject, such as a product name, weigh little).
|
|
121
|
+
* Ties break by subject key, so the ranking is fully deterministic.
|
|
122
|
+
*/
|
|
123
|
+
// implements REQ-kibi-vocabulary-alignment-capability
|
|
124
|
+
export function rankSubjectCandidates(clauseText, vocabulary, options = {}) {
|
|
125
|
+
const clause = tokenSet(clauseText);
|
|
126
|
+
if (clause.size === 0 || vocabulary.length === 0)
|
|
127
|
+
return [];
|
|
128
|
+
const documents = vocabulary.map((entry) => ({
|
|
129
|
+
entry,
|
|
130
|
+
key: tokenSet(entry.subjectKey),
|
|
131
|
+
title: tokenSet(entry.title ?? ""),
|
|
132
|
+
requirements: (entry.requirements ?? []).map((requirement) => tokenSet(requirement.title ?? "")),
|
|
133
|
+
}));
|
|
134
|
+
const documentFrequency = new Map();
|
|
135
|
+
for (const document of documents) {
|
|
136
|
+
const tokens = new Set([
|
|
137
|
+
...document.key,
|
|
138
|
+
...document.title,
|
|
139
|
+
...document.requirements.flatMap((set) => [...set]),
|
|
140
|
+
]);
|
|
141
|
+
for (const token of tokens) {
|
|
142
|
+
documentFrequency.set(token, (documentFrequency.get(token) ?? 0) + 1);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const weight = (token) => Math.log(1 + documents.length / (documentFrequency.get(token) ?? 1));
|
|
146
|
+
const candidates = documents.map((document) => {
|
|
147
|
+
const keyScore = weightedMatch(clause, document.key, weight);
|
|
148
|
+
const titleScore = weightedMatch(clause, document.title, weight);
|
|
149
|
+
const requirementScore = Math.max(0, ...document.requirements.map((set) => weightedMatch(clause, set, weight)));
|
|
150
|
+
const requirementTitles = (document.entry.requirements ?? [])
|
|
151
|
+
.map((requirement) => requirement.title)
|
|
152
|
+
.filter((title) => typeof title === "string");
|
|
153
|
+
return {
|
|
154
|
+
subjectKey: document.entry.subjectKey,
|
|
155
|
+
score: round3(0.5 * keyScore + 0.3 * titleScore + 0.2 * requirementScore),
|
|
156
|
+
...(document.entry.title !== undefined
|
|
157
|
+
? { title: document.entry.title }
|
|
158
|
+
: {}),
|
|
159
|
+
...(requirementTitles.length > 0 ? { requirementTitles } : {}),
|
|
160
|
+
};
|
|
161
|
+
});
|
|
162
|
+
return candidates
|
|
163
|
+
.filter((candidate) => candidate.score > 0)
|
|
164
|
+
.sort((left, right) => right.score - left.score ||
|
|
165
|
+
left.subjectKey.localeCompare(right.subjectKey))
|
|
166
|
+
.slice(0, options.limit ?? 5);
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Quantities as written: number plus the unit symbol that follows it, with the
|
|
170
|
+
* unit's case preserved (3 MB and 3 Mb are different quantities).
|
|
171
|
+
*/
|
|
172
|
+
function quantities(text) {
|
|
173
|
+
return [...text.matchAll(/(\d+(?:\.\d+)?)\s*([A-Za-z%]+)?/g)]
|
|
174
|
+
.map((match) => `${Number(match[1])}|${match[2] ?? ""}`)
|
|
175
|
+
.sort();
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Dice similarity over content tokens. Claims whose negation differs are never
|
|
179
|
+
* similar ("must expire" vs "must not expire"), and neither are claims whose
|
|
180
|
+
* quantities differ ("15 minutes" vs "60 minutes"): token overlap cannot tell
|
|
181
|
+
* which of two different bounds is meant, so the builtin stays conservative and
|
|
182
|
+
* leaves unit-converted paraphrases to Prolog canonicalization or a provider.
|
|
183
|
+
*/
|
|
184
|
+
// implements REQ-kibi-vocabulary-alignment-capability
|
|
185
|
+
export function claimSimilarity(left, right) {
|
|
186
|
+
const a = tokenSet(left);
|
|
187
|
+
const b = tokenSet(right);
|
|
188
|
+
if (a.size === 0 || b.size === 0)
|
|
189
|
+
return 0;
|
|
190
|
+
if (quantities(left).join(" ") !== quantities(right).join(" "))
|
|
191
|
+
return 0;
|
|
192
|
+
const negatedA = [...a].some((token) => NEGATIONS.has(token));
|
|
193
|
+
const negatedB = [...b].some((token) => NEGATIONS.has(token));
|
|
194
|
+
if (negatedA !== negatedB)
|
|
195
|
+
return 0;
|
|
196
|
+
let shared = 0;
|
|
197
|
+
for (const token of a)
|
|
198
|
+
if (b.has(token))
|
|
199
|
+
shared += 1;
|
|
200
|
+
return round3((2 * shared) / (a.size + b.size));
|
|
201
|
+
}
|
|
202
|
+
function normalizedKey(value) {
|
|
203
|
+
return value
|
|
204
|
+
.trim()
|
|
205
|
+
.toLowerCase()
|
|
206
|
+
.replace(/[\\/]+/g, ".")
|
|
207
|
+
.replace(/[^a-z0-9.]+/g, "_")
|
|
208
|
+
.replace(/_+/g, "_")
|
|
209
|
+
.replace(/\.+/g, ".")
|
|
210
|
+
.replace(/(^[._]+|[._]+$)/g, "");
|
|
211
|
+
}
|
|
212
|
+
// implements REQ-kibi-vocabulary-alignment-capability
|
|
213
|
+
export function builtinSubjectDecision(clause) {
|
|
214
|
+
const proposed = clause.proposedSubjectKey !== undefined
|
|
215
|
+
? normalizedKey(clause.proposedSubjectKey)
|
|
216
|
+
: undefined;
|
|
217
|
+
const exact = clause.candidates.find((candidate) => candidate.subjectKey === proposed);
|
|
218
|
+
if (exact) {
|
|
219
|
+
return {
|
|
220
|
+
claimKey: clause.claimKey,
|
|
221
|
+
choice: exact.subjectKey,
|
|
222
|
+
confidence: 1,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
const best = clause.candidates[0];
|
|
226
|
+
if (best && best.score >= BUILTIN_SUBJECT_REUSE_THRESHOLD) {
|
|
227
|
+
return {
|
|
228
|
+
claimKey: clause.claimKey,
|
|
229
|
+
choice: best.subjectKey,
|
|
230
|
+
confidence: best.score,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
claimKey: clause.claimKey,
|
|
235
|
+
choice: NEW_SUBJECT_CHOICE,
|
|
236
|
+
confidence: round3(1 - (best?.score ?? 0)),
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
/** Deterministic, offline vocabulary-alignment provider. */
|
|
240
|
+
// implements REQ-kibi-vocabulary-alignment-capability
|
|
241
|
+
export class BuiltinVocabularyAlignment {
|
|
242
|
+
id = "kibi-plugin-builtin.vocabulary-alignment";
|
|
243
|
+
rankSubjects(input) {
|
|
244
|
+
return { decisions: input.clauses.map(builtinSubjectDecision) };
|
|
245
|
+
}
|
|
246
|
+
compareClaims(input) {
|
|
247
|
+
const judgments = input.pairs.map((pair) => {
|
|
248
|
+
const similarity = claimSimilarity(pair.left.claimText, pair.right.claimText);
|
|
249
|
+
return {
|
|
250
|
+
pairKey: pair.pairKey,
|
|
251
|
+
sameObligation: similarity >= BUILTIN_CLAIM_SIMILARITY_THRESHOLD,
|
|
252
|
+
confidence: similarity,
|
|
253
|
+
};
|
|
254
|
+
});
|
|
255
|
+
return { judgments };
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
// implements REQ-kibi-vocabulary-alignment-capability
|
|
259
|
+
export function createBuiltinVocabularyAlignment() {
|
|
260
|
+
return new BuiltinVocabularyAlignment();
|
|
261
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kibi-plugin-builtin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Default Kibi capability plugins: TS symbol extraction, ontology pack, and semantic classifier",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"access": "public"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"kibi-plugin-sdk": "^0.
|
|
35
|
+
"kibi-plugin-sdk": "^0.4.0",
|
|
36
36
|
"ts-morph": "^23.0.0"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|