cwtools-shared 0.2.1 → 0.2.3
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/dist/knowledge/rules.d.ts +1 -1
- package/dist/knowledge/rules.js +286 -36
- package/package.json +24 -24
|
@@ -147,7 +147,7 @@ export interface CwtRuleValueReference {
|
|
|
147
147
|
}
|
|
148
148
|
export interface RuleSemanticHint {
|
|
149
149
|
text: string;
|
|
150
|
-
source: 'trigger_docs.log' | 'scopes.cwt' | 'cwt-comment' | 'modifiers.log';
|
|
150
|
+
source: 'trigger_docs.log' | 'scopes.cwt' | 'links.cwt' | 'cwt-comment' | 'modifiers.log';
|
|
151
151
|
file?: string;
|
|
152
152
|
line?: number;
|
|
153
153
|
confidence: 'hint';
|
package/dist/knowledge/rules.js
CHANGED
|
@@ -49,9 +49,15 @@ async function queryRulesWithHost(host, args) {
|
|
|
49
49
|
: args.category === 'modifier'
|
|
50
50
|
? cache.modifiers
|
|
51
51
|
: cache.scopeChanges;
|
|
52
|
+
const dottedScopeChain = args.name && args.category === 'scope_change'
|
|
53
|
+
? buildDottedScopeChainRule(args.name, args.scope, cache.scopeChanges)
|
|
54
|
+
: undefined;
|
|
52
55
|
if (args.name) {
|
|
53
|
-
const needle = args.name.
|
|
54
|
-
const filtered = rules
|
|
56
|
+
const needle = normalizeRuleNameQuery(args.name, args.category);
|
|
57
|
+
const filtered = rules
|
|
58
|
+
.filter(rule => rule.name.toLowerCase().includes(needle))
|
|
59
|
+
.sort((a, b) => scoreRuleNameMatch(a.name, needle) - scoreRuleNameMatch(b.name, needle)
|
|
60
|
+
|| a.name.localeCompare(b.name));
|
|
55
61
|
if (filtered.length === 0 && rules.length > 0) {
|
|
56
62
|
rules = rules
|
|
57
63
|
.map(rule => ({ rule, score: levenshtein(needle, rule.name.toLowerCase()) }))
|
|
@@ -66,6 +72,12 @@ async function queryRulesWithHost(host, args) {
|
|
|
66
72
|
rules = filtered;
|
|
67
73
|
}
|
|
68
74
|
}
|
|
75
|
+
if (dottedScopeChain) {
|
|
76
|
+
rules = [
|
|
77
|
+
dottedScopeChain,
|
|
78
|
+
...rules.filter(rule => rule.name !== dottedScopeChain.name),
|
|
79
|
+
];
|
|
80
|
+
}
|
|
69
81
|
if (args.scope) {
|
|
70
82
|
const scope = args.scope.toLowerCase();
|
|
71
83
|
rules = rules.filter(rule => rule.scopes.length === 0
|
|
@@ -322,13 +334,45 @@ function normalizeCwtSchemaTarget(host, value) {
|
|
|
322
334
|
}
|
|
323
335
|
async function findCwtSchemaFiles(host, root, maxFiles) {
|
|
324
336
|
if (host.rules?.listCwtFiles) {
|
|
325
|
-
return (await host.rules.listCwtFiles(root, { limit: maxFiles }))
|
|
337
|
+
return (await host.rules.listCwtFiles(root, { limit: maxFiles }))
|
|
338
|
+
.slice(0, maxFiles)
|
|
339
|
+
.sort((a, b) => a.localeCompare(b));
|
|
340
|
+
}
|
|
341
|
+
if (fs.existsSync(root)) {
|
|
342
|
+
const results = [];
|
|
343
|
+
const ignoredDirs = new Set(['.git', 'node_modules', 'logs', 'config']);
|
|
344
|
+
const walkDisk = (dir, depth) => {
|
|
345
|
+
if (results.length >= maxFiles || depth > 8)
|
|
346
|
+
return;
|
|
347
|
+
let entries;
|
|
348
|
+
try {
|
|
349
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
355
|
+
for (const entry of entries) {
|
|
356
|
+
if (results.length >= maxFiles)
|
|
357
|
+
break;
|
|
358
|
+
const fullPath = path.join(dir, entry.name);
|
|
359
|
+
if (entry.isDirectory()) {
|
|
360
|
+
if (!ignoredDirs.has(entry.name))
|
|
361
|
+
walkDisk(fullPath, depth + 1);
|
|
362
|
+
}
|
|
363
|
+
else if (entry.isFile() && entry.name.toLowerCase().endsWith('.cwt')) {
|
|
364
|
+
results.push(fullPath);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
walkDisk(root, 0);
|
|
369
|
+
return results.sort((a, b) => a.localeCompare(b));
|
|
326
370
|
}
|
|
327
371
|
const rootRelative = workspaceRelativePath(host.workspaceRoot, root);
|
|
328
372
|
if (!rootRelative)
|
|
329
373
|
return [];
|
|
330
374
|
const results = [];
|
|
331
|
-
const ignoredDirs = new Set(['.git', 'node_modules', 'logs']);
|
|
375
|
+
const ignoredDirs = new Set(['.git', 'node_modules', 'logs', 'config']);
|
|
332
376
|
const walk = async (relativeDir, depth) => {
|
|
333
377
|
if (results.length >= maxFiles || depth > 8)
|
|
334
378
|
return;
|
|
@@ -339,6 +383,7 @@ async function findCwtSchemaFiles(host, root, maxFiles) {
|
|
|
339
383
|
catch {
|
|
340
384
|
return;
|
|
341
385
|
}
|
|
386
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
342
387
|
for (const entry of entries) {
|
|
343
388
|
if (results.length >= maxFiles)
|
|
344
389
|
break;
|
|
@@ -354,7 +399,16 @@ async function findCwtSchemaFiles(host, root, maxFiles) {
|
|
|
354
399
|
}
|
|
355
400
|
};
|
|
356
401
|
await walk(rootRelative, 0);
|
|
357
|
-
return results;
|
|
402
|
+
return results.sort((a, b) => a.localeCompare(b));
|
|
403
|
+
}
|
|
404
|
+
async function collectCwtRuleSourceFiles(host, root, includeMissingLogs) {
|
|
405
|
+
const files = await findCwtSchemaFiles(host, root, CWT_RULE_FILE_SCAN_LIMIT);
|
|
406
|
+
for (const relativeLog of CWT_RULE_LOG_CANDIDATES) {
|
|
407
|
+
const fullPath = path.join(root, relativeLog);
|
|
408
|
+
if (includeMissingLogs || fs.existsSync(fullPath))
|
|
409
|
+
files.push(fullPath);
|
|
410
|
+
}
|
|
411
|
+
return Array.from(new Set(files)).sort((a, b) => a.localeCompare(b));
|
|
358
412
|
}
|
|
359
413
|
function workspaceRelativePath(workspaceRoot, fullPath) {
|
|
360
414
|
const relative = path.relative(workspaceRoot, fullPath);
|
|
@@ -715,27 +769,18 @@ function scoreCwtSchemaEntity(summary, normalizedTarget, name) {
|
|
|
715
769
|
//
|
|
716
770
|
// loadCwtRules used to re-read and re-parse every rule file on each query.
|
|
717
771
|
// The memo keeps one parsed CwtRuleCache per host identity, invalidated by an
|
|
718
|
-
// mtime/size signature over
|
|
719
|
-
//
|
|
720
|
-
// sha256 (16 hex chars) over the length-prefixed concatenation
|
|
721
|
-
//
|
|
772
|
+
// mtime/size signature over every .cwt file under the active config roots plus
|
|
773
|
+
// the docs/modifier logs. `generation` is a per-host monotonic reload counter;
|
|
774
|
+
// `contentHash` is sha256 (16 hex chars) over the length-prefixed concatenation
|
|
775
|
+
// of the same files' content — the same algorithm the extension-side
|
|
722
776
|
// LspToolHandler uses, so both ends describe rule revisions with the same
|
|
723
777
|
// hash semantics. The cache is process-local and bounded
|
|
724
778
|
// (CWT_RULES_MEMO_MAX_ENTRIES).
|
|
725
|
-
const
|
|
726
|
-
'scopes.cwt',
|
|
779
|
+
const CWT_RULE_LOG_CANDIDATES = [
|
|
727
780
|
path.join('logs', 'trigger_docs.log'),
|
|
728
781
|
path.join('logs', 'modifiers.log'),
|
|
729
|
-
'triggers.cwt',
|
|
730
|
-
'trigger.cwt',
|
|
731
|
-
path.join('generated', 'triggers.generated.cwt'),
|
|
732
|
-
'effects.cwt',
|
|
733
|
-
'effect.cwt',
|
|
734
|
-
path.join('generated', 'effects.generated.cwt'),
|
|
735
|
-
'modifier.cwt',
|
|
736
|
-
'scope_changes.cwt',
|
|
737
|
-
path.join('generated', 'scope_changes.generated.cwt'),
|
|
738
782
|
];
|
|
783
|
+
const CWT_RULE_FILE_SCAN_LIMIT = 5000;
|
|
739
784
|
const CWT_RULES_MEMO_MAX_ENTRIES = 8;
|
|
740
785
|
/**
|
|
741
786
|
* When no candidate rule file exists on disk, the mtime signature cannot
|
|
@@ -744,12 +789,11 @@ const CWT_RULES_MEMO_MAX_ENTRIES = 8;
|
|
|
744
789
|
*/
|
|
745
790
|
const CWT_RULES_MEMO_REFRESH_MS = 30000;
|
|
746
791
|
const cwtRulesMemo = new Map();
|
|
747
|
-
function computeRulesSignature(configPaths) {
|
|
792
|
+
async function computeRulesSignature(host, configPaths) {
|
|
748
793
|
const parts = [];
|
|
749
794
|
let sawDiskFiles = false;
|
|
750
795
|
for (const configPath of configPaths) {
|
|
751
|
-
for (const
|
|
752
|
-
const fullPath = path.join(configPath, file);
|
|
796
|
+
for (const fullPath of await collectCwtRuleSourceFiles(host, configPath, true)) {
|
|
753
797
|
try {
|
|
754
798
|
const stat = fs.statSync(fullPath);
|
|
755
799
|
parts.push(`${fullPath}:${stat.mtimeMs}:${stat.size}`);
|
|
@@ -772,8 +816,8 @@ function computeRulesSignature(configPaths) {
|
|
|
772
816
|
async function computeRulesContentHash(host, configPaths) {
|
|
773
817
|
const hash = crypto.createHash('sha256');
|
|
774
818
|
for (const configPath of configPaths) {
|
|
775
|
-
for (const file of
|
|
776
|
-
const read = await readRulesTextFile(host,
|
|
819
|
+
for (const file of await collectCwtRuleSourceFiles(host, configPath, true)) {
|
|
820
|
+
const read = await readRulesTextFile(host, file).catch(() => ({ exists: false, content: '', hasBom: false }));
|
|
777
821
|
if (!read.exists)
|
|
778
822
|
continue;
|
|
779
823
|
hash.update(`${read.content.length}:`);
|
|
@@ -788,7 +832,7 @@ function cwtRulesHostKey(host) {
|
|
|
788
832
|
async function loadCwtRulesMemoized(host) {
|
|
789
833
|
const configPaths = await resolveRulesConfigPaths(host);
|
|
790
834
|
const hostKey = cwtRulesHostKey(host);
|
|
791
|
-
const { signature, sawDiskFiles } = computeRulesSignature(configPaths);
|
|
835
|
+
const { signature, sawDiskFiles } = await computeRulesSignature(host, configPaths);
|
|
792
836
|
const memo = cwtRulesMemo.get(hostKey);
|
|
793
837
|
if (memo && memo.signature === signature && (memo.sawDiskFiles || host.now() - memo.computedAt < CWT_RULES_MEMO_REFRESH_MS)) {
|
|
794
838
|
return { cache: memo.cache, meta: { generation: memo.generation, contentHash: memo.contentHash } };
|
|
@@ -824,10 +868,25 @@ async function loadCwtRulesFromPaths(host, configPaths) {
|
|
|
824
868
|
const scopes = scopesRead.exists
|
|
825
869
|
? parseScopesFile(scopesRead.content, path.join(configPath, 'scopes.cwt'))
|
|
826
870
|
: new Map();
|
|
827
|
-
const triggers =
|
|
828
|
-
const effects =
|
|
829
|
-
const scopeChanges =
|
|
830
|
-
const modifierAliases =
|
|
871
|
+
const triggers = [];
|
|
872
|
+
const effects = [];
|
|
873
|
+
const scopeChanges = [];
|
|
874
|
+
const modifierAliases = [];
|
|
875
|
+
for (const file of await findCwtSchemaFiles(host, configPath, CWT_RULE_FILE_SCAN_LIMIT)) {
|
|
876
|
+
const relativeFile = path.relative(configPath, file).replace(/\\/g, '/');
|
|
877
|
+
const parsed = await readRulesFile(host, file, scopeChangeFileCategoryOverride(relativeFile), docs, scopes);
|
|
878
|
+
for (const rule of parsed) {
|
|
879
|
+
if (rule.category === 'trigger')
|
|
880
|
+
triggers.push(rule);
|
|
881
|
+
else if (rule.category === 'effect')
|
|
882
|
+
effects.push(rule);
|
|
883
|
+
else if (rule.category === 'modifier')
|
|
884
|
+
modifierAliases.push(rule);
|
|
885
|
+
else
|
|
886
|
+
scopeChanges.push(rule);
|
|
887
|
+
}
|
|
888
|
+
scopeChanges.push(...await readLinksFile(host, file, scopes));
|
|
889
|
+
}
|
|
831
890
|
const modifierLog = await readModifiersLog(host, path.join(configPath, 'logs', 'modifiers.log'));
|
|
832
891
|
const modifiers = [...modifierAliases];
|
|
833
892
|
const modifierNames = new Set(modifiers.map(rule => rule.name.toLowerCase()));
|
|
@@ -925,6 +984,12 @@ async function readRulesFile(host, filePath, category, docs, scopes) {
|
|
|
925
984
|
return [];
|
|
926
985
|
return parseCwtFile(read.content, filePath, category, docs, scopes);
|
|
927
986
|
}
|
|
987
|
+
function scopeChangeFileCategoryOverride(relativeRuleFile) {
|
|
988
|
+
const base = path.posix.basename(relativeRuleFile.replace(/\\/g, '/')).toLowerCase();
|
|
989
|
+
return base === 'scope_changes.cwt' || base === 'scope_changes.generated.cwt' || base === 'scope_change.cwt'
|
|
990
|
+
? 'scope_change'
|
|
991
|
+
: undefined;
|
|
992
|
+
}
|
|
928
993
|
async function readModifiersLog(host, filePath) {
|
|
929
994
|
const read = await readRulesTextFile(host, filePath).catch(() => ({ exists: false, content: '', hasBom: false }));
|
|
930
995
|
if (!read.exists)
|
|
@@ -1035,7 +1100,7 @@ function parseScopesFile(content, filePath) {
|
|
|
1035
1100
|
}
|
|
1036
1101
|
return scopes;
|
|
1037
1102
|
}
|
|
1038
|
-
function parseCwtFile(content, filePath,
|
|
1103
|
+
function parseCwtFile(content, filePath, categoryOverride, docs, scopes) {
|
|
1039
1104
|
const results = [];
|
|
1040
1105
|
let currentScopes = [];
|
|
1041
1106
|
let currentSupportedScopes = [];
|
|
@@ -1080,9 +1145,11 @@ function parseCwtFile(content, filePath, category, docs, scopes) {
|
|
|
1080
1145
|
currentDesc = comment;
|
|
1081
1146
|
continue;
|
|
1082
1147
|
}
|
|
1083
|
-
const nameMatch = line.match(/^alias\[(
|
|
1084
|
-
if (nameMatch?.[1]) {
|
|
1085
|
-
const
|
|
1148
|
+
const nameMatch = line.match(/^alias\[(trigger|effect|modifier):([^\]]+)\]\s*=\s*(.*)/);
|
|
1149
|
+
if (nameMatch?.[1] && nameMatch[2]) {
|
|
1150
|
+
const aliasKind = nameMatch[1];
|
|
1151
|
+
const category = categoryOverride ?? aliasKind;
|
|
1152
|
+
const name = nameMatch[2];
|
|
1086
1153
|
const doc = docs.get(name);
|
|
1087
1154
|
const cwtBlockText = collectCwtBlockText(lines, i);
|
|
1088
1155
|
const scopesForRule = doc?.scopes.length
|
|
@@ -1090,7 +1157,7 @@ function parseCwtFile(content, filePath, category, docs, scopes) {
|
|
|
1090
1157
|
: currentSupportedScopes.length
|
|
1091
1158
|
? currentSupportedScopes
|
|
1092
1159
|
: currentScopes;
|
|
1093
|
-
const syntax = doc?.syntax || normalizeInlineSyntax(name, nameMatch[
|
|
1160
|
+
const syntax = doc?.syntax || normalizeInlineSyntax(name, nameMatch[3] ?? '');
|
|
1094
1161
|
const description = doc?.description || currentDesc;
|
|
1095
1162
|
const semanticHints = buildSemanticHints({
|
|
1096
1163
|
description,
|
|
@@ -1134,6 +1201,92 @@ function parseCwtFile(content, filePath, category, docs, scopes) {
|
|
|
1134
1201
|
}
|
|
1135
1202
|
return results;
|
|
1136
1203
|
}
|
|
1204
|
+
async function readLinksFile(host, filePath, scopes) {
|
|
1205
|
+
const read = await readRulesTextFile(host, filePath).catch(() => ({ exists: false, content: '', hasBom: false }));
|
|
1206
|
+
if (!read.exists || !/^\s*links\s*=\s*\{/im.test(read.content))
|
|
1207
|
+
return [];
|
|
1208
|
+
return parseLinksCwtFile(read.content, filePath, scopes);
|
|
1209
|
+
}
|
|
1210
|
+
function parseLinksCwtFile(content, filePath, scopes) {
|
|
1211
|
+
const results = [];
|
|
1212
|
+
const lines = content.split(/\r?\n/);
|
|
1213
|
+
let inLinks = false;
|
|
1214
|
+
let depth = 0;
|
|
1215
|
+
let current;
|
|
1216
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1217
|
+
const rawLine = lines[i] ?? '';
|
|
1218
|
+
const line = stripCwtLineComment(rawLine).trim();
|
|
1219
|
+
if (!inLinks) {
|
|
1220
|
+
if (/^links\s*=\s*\{/.test(line)) {
|
|
1221
|
+
inLinks = true;
|
|
1222
|
+
depth = countBraceDelta(line);
|
|
1223
|
+
}
|
|
1224
|
+
continue;
|
|
1225
|
+
}
|
|
1226
|
+
if (!current && depth === 1) {
|
|
1227
|
+
const linkMatch = line.match(/^([A-Za-z_][\w.-]*)\s*=\s*\{\s*$/);
|
|
1228
|
+
if (linkMatch?.[1])
|
|
1229
|
+
current = { name: linkMatch[1], line: i + 1, inputScopes: [] };
|
|
1230
|
+
}
|
|
1231
|
+
else if (current) {
|
|
1232
|
+
const inputMatch = line.match(/^input_scopes\s*=\s*(.*)$/i);
|
|
1233
|
+
if (inputMatch?.[1])
|
|
1234
|
+
current.inputScopes = splitRuleValueList(inputMatch[1]).map(normalizeScopeName);
|
|
1235
|
+
const outputMatch = line.match(/^output_scope\s*=\s*(.*)$/i);
|
|
1236
|
+
if (outputMatch?.[1]) {
|
|
1237
|
+
const outputScope = stripRuleValueBraces(outputMatch[1]).split(/\s+/)[0];
|
|
1238
|
+
if (outputScope)
|
|
1239
|
+
current.outputScope = normalizeScopeName(outputScope);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
depth += countBraceDelta(line);
|
|
1243
|
+
if (current && depth <= 1) {
|
|
1244
|
+
if (current.outputScope) {
|
|
1245
|
+
const inputScopes = current.inputScopes.length ? current.inputScopes : ['all'];
|
|
1246
|
+
const syntax = `${current.name} = scope link (${inputScopes.join(' | ')} -> ${current.outputScope})`;
|
|
1247
|
+
const linkHint = {
|
|
1248
|
+
text: `Legal scope link '${current.name}' accepts input scopes { ${inputScopes.join(' ')} } and outputs '${current.outputScope}'. Context pointers such as from/prev/root/this select the current input scope; they are not fixed object fields.`,
|
|
1249
|
+
source: 'links.cwt',
|
|
1250
|
+
file: filePath,
|
|
1251
|
+
line: current.line,
|
|
1252
|
+
confidence: 'hint',
|
|
1253
|
+
};
|
|
1254
|
+
const scopeHints = buildSemanticHints({
|
|
1255
|
+
description: '',
|
|
1256
|
+
cwtDescription: '',
|
|
1257
|
+
scopes,
|
|
1258
|
+
relatedScopeNames: [...inputScopes, current.outputScope],
|
|
1259
|
+
cwtFile: filePath,
|
|
1260
|
+
cwtLine: current.line,
|
|
1261
|
+
});
|
|
1262
|
+
results.push({
|
|
1263
|
+
name: current.name,
|
|
1264
|
+
description: `Legal scope link from { ${inputScopes.join(' ')} } to ${current.outputScope}.`,
|
|
1265
|
+
scopes: inputScopes,
|
|
1266
|
+
syntax,
|
|
1267
|
+
category: 'scope_change',
|
|
1268
|
+
sourceFile: filePath,
|
|
1269
|
+
sourceLine: current.line,
|
|
1270
|
+
hardFacts: {
|
|
1271
|
+
category: 'scope_change',
|
|
1272
|
+
supportedScopes: inputScopes,
|
|
1273
|
+
pushScope: current.outputScope,
|
|
1274
|
+
valueReferences: [],
|
|
1275
|
+
syntax,
|
|
1276
|
+
cwtSource: { file: filePath, line: current.line },
|
|
1277
|
+
},
|
|
1278
|
+
semanticHints: [linkHint, ...scopeHints].slice(0, 8),
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1281
|
+
current = undefined;
|
|
1282
|
+
}
|
|
1283
|
+
if (inLinks && depth <= 0) {
|
|
1284
|
+
inLinks = false;
|
|
1285
|
+
current = undefined;
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
return results;
|
|
1289
|
+
}
|
|
1137
1290
|
function buildSemanticHints(args) {
|
|
1138
1291
|
const hints = [];
|
|
1139
1292
|
const seen = new Set();
|
|
@@ -1287,7 +1440,12 @@ function expandIntentTokens(intent) {
|
|
|
1287
1440
|
[/触发器|觸發器/g, ['trigger']],
|
|
1288
1441
|
[/效果|效应|效應/g, ['effect']],
|
|
1289
1442
|
];
|
|
1290
|
-
const expanded = [
|
|
1443
|
+
const expanded = [];
|
|
1444
|
+
for (const token of direct) {
|
|
1445
|
+
expanded.push(token);
|
|
1446
|
+
if (/[.:]/.test(token))
|
|
1447
|
+
expanded.push(...token.split(/[.:]+/).filter(Boolean));
|
|
1448
|
+
}
|
|
1291
1449
|
for (const [pattern, tokens] of synonyms) {
|
|
1292
1450
|
pattern.lastIndex = 0;
|
|
1293
1451
|
if (pattern.test(intent))
|
|
@@ -1363,6 +1521,98 @@ function splitRuleValueList(value) {
|
|
|
1363
1521
|
function stripRuleValueBraces(value) {
|
|
1364
1522
|
return value.replace(/^\{\s*/, '').replace(/\s*\}$/, '').trim();
|
|
1365
1523
|
}
|
|
1524
|
+
function normalizeRuleNameQuery(name, category) {
|
|
1525
|
+
const lowered = name.trim().toLowerCase();
|
|
1526
|
+
if (category !== 'scope_change' || !lowered.includes('.'))
|
|
1527
|
+
return lowered;
|
|
1528
|
+
const parts = lowered.split('.').map(part => part.trim()).filter(Boolean);
|
|
1529
|
+
return parts[parts.length - 1] ?? lowered;
|
|
1530
|
+
}
|
|
1531
|
+
function buildDottedScopeChainRule(query, contextScope, scopeChanges) {
|
|
1532
|
+
const rawParts = query.trim().split('.').map(normalizeScopeName).filter(Boolean);
|
|
1533
|
+
if (rawParts.length < 2)
|
|
1534
|
+
return undefined;
|
|
1535
|
+
const contextPointers = new Set(['from', 'prev', 'root', 'this']);
|
|
1536
|
+
const first = rawParts[0];
|
|
1537
|
+
const startsWithPointer = contextPointers.has(first);
|
|
1538
|
+
const initialScope = startsWithPointer ? contextScope?.trim().toLowerCase() : first;
|
|
1539
|
+
const linkNames = rawParts.slice(1);
|
|
1540
|
+
if (!initialScope || linkNames.length === 0)
|
|
1541
|
+
return undefined;
|
|
1542
|
+
const hops = [];
|
|
1543
|
+
let currentScope = initialScope;
|
|
1544
|
+
for (const linkName of linkNames) {
|
|
1545
|
+
const link = scopeChanges.find(rule => rule.name.toLowerCase() === linkName
|
|
1546
|
+
&& !!rule.hardFacts?.pushScope
|
|
1547
|
+
&& scopeListContains(rule.hardFacts.supportedScopes ?? rule.scopes, currentScope));
|
|
1548
|
+
const outputScope = link?.hardFacts?.pushScope?.trim().toLowerCase();
|
|
1549
|
+
if (!link || !outputScope)
|
|
1550
|
+
return undefined;
|
|
1551
|
+
hops.push({ link, inputScope: currentScope, outputScope });
|
|
1552
|
+
currentScope = outputScope;
|
|
1553
|
+
}
|
|
1554
|
+
if (hops.length === 0)
|
|
1555
|
+
return undefined;
|
|
1556
|
+
const normalizedQuery = rawParts.join('.');
|
|
1557
|
+
const syntax = `${normalizedQuery} = { ... }`;
|
|
1558
|
+
const source = hops[0].link;
|
|
1559
|
+
const hopText = hops.map(hop => `${hop.inputScope}.${hop.link.name} -> ${hop.outputScope}`).join('; ');
|
|
1560
|
+
const linkHints = hops.map((hop) => ({
|
|
1561
|
+
text: `Dotted scope chain hop '${hop.inputScope}.${hop.link.name}' is legal because links.cwt declares '${hop.link.name}' with input scope '${hop.inputScope}' and output scope '${hop.outputScope}'.`,
|
|
1562
|
+
source: 'links.cwt',
|
|
1563
|
+
file: hop.link.sourceFile,
|
|
1564
|
+
line: hop.link.sourceLine,
|
|
1565
|
+
confidence: 'hint',
|
|
1566
|
+
}));
|
|
1567
|
+
return {
|
|
1568
|
+
name: normalizedQuery,
|
|
1569
|
+
description: `Legal dotted scope chain from ${initialScope} to ${currentScope}. Hops: ${hopText}.`,
|
|
1570
|
+
scopes: [initialScope],
|
|
1571
|
+
syntax,
|
|
1572
|
+
category: 'scope_change',
|
|
1573
|
+
sourceFile: source.sourceFile,
|
|
1574
|
+
sourceLine: source.sourceLine,
|
|
1575
|
+
hardFacts: {
|
|
1576
|
+
category: 'scope_change',
|
|
1577
|
+
supportedScopes: [initialScope],
|
|
1578
|
+
pushScope: currentScope,
|
|
1579
|
+
valueReferences: [],
|
|
1580
|
+
syntax,
|
|
1581
|
+
cwtSource: source.sourceFile && source.sourceLine
|
|
1582
|
+
? { file: source.sourceFile, line: source.sourceLine }
|
|
1583
|
+
: undefined,
|
|
1584
|
+
},
|
|
1585
|
+
semanticHints: linkHints.slice(0, 8),
|
|
1586
|
+
};
|
|
1587
|
+
}
|
|
1588
|
+
function scopeListContains(scopes, scope) {
|
|
1589
|
+
const lowerScope = scope.toLowerCase();
|
|
1590
|
+
return scopes.some(candidate => {
|
|
1591
|
+
const lower = candidate.toLowerCase();
|
|
1592
|
+
return lower === lowerScope || lower === 'all' || lower === 'any';
|
|
1593
|
+
});
|
|
1594
|
+
}
|
|
1595
|
+
function scoreRuleNameMatch(name, needle) {
|
|
1596
|
+
const lower = name.toLowerCase();
|
|
1597
|
+
if (lower === needle)
|
|
1598
|
+
return 0;
|
|
1599
|
+
if (lower.startsWith(needle))
|
|
1600
|
+
return 1;
|
|
1601
|
+
return 2;
|
|
1602
|
+
}
|
|
1603
|
+
function normalizeScopeName(scope) {
|
|
1604
|
+
return scope.replace(/^["']|["']$/g, '').trim().toLowerCase();
|
|
1605
|
+
}
|
|
1606
|
+
function countBraceDelta(line) {
|
|
1607
|
+
let delta = 0;
|
|
1608
|
+
for (const ch of line) {
|
|
1609
|
+
if (ch === '{')
|
|
1610
|
+
delta += 1;
|
|
1611
|
+
else if (ch === '}')
|
|
1612
|
+
delta -= 1;
|
|
1613
|
+
}
|
|
1614
|
+
return delta;
|
|
1615
|
+
}
|
|
1366
1616
|
function splitWords(value) {
|
|
1367
1617
|
return value.split(/\s+/).map(part => part.trim()).filter(Boolean);
|
|
1368
1618
|
}
|
package/package.json
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "cwtools-shared",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "Shared protocol and tool contracts for the CWTools MCP server",
|
|
5
|
-
"license": "MIT",
|
|
6
|
-
"repository": {
|
|
7
|
-
"type": "git",
|
|
8
|
-
"url": "git+https://github.com/Aa728848/cwtools-mcp.git"
|
|
9
|
-
},
|
|
10
|
-
"engines": {
|
|
11
|
-
"node": ">=18"
|
|
12
|
-
},
|
|
13
|
-
"main": "dist/index.js",
|
|
14
|
-
"types": "dist/index.d.ts",
|
|
15
|
-
"files": [
|
|
16
|
-
"dist",
|
|
17
|
-
"package.json"
|
|
18
|
-
],
|
|
19
|
-
"scripts": {
|
|
20
|
-
"build": "tsc -p tsconfig.json",
|
|
21
|
-
"prepack": "npm run build",
|
|
22
|
-
"test:contracts": "ts-mocha -p tsconfig.test.json \"src/test/**/*.test.ts\""
|
|
23
|
-
}
|
|
24
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "cwtools-shared",
|
|
3
|
+
"version": "0.2.3",
|
|
4
|
+
"description": "Shared protocol and tool contracts for the CWTools MCP server",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/Aa728848/cwtools-mcp.git"
|
|
9
|
+
},
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=18"
|
|
12
|
+
},
|
|
13
|
+
"main": "dist/index.js",
|
|
14
|
+
"types": "dist/index.d.ts",
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"package.json"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc -p tsconfig.json",
|
|
21
|
+
"prepack": "npm run build",
|
|
22
|
+
"test:contracts": "ts-mocha -p tsconfig.test.json \"src/test/**/*.test.ts\""
|
|
23
|
+
}
|
|
24
|
+
}
|