@nazty_labs/common-ground 0.5.1
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/LICENSE +21 -0
- package/README.md +64 -0
- package/SETUP.md +219 -0
- package/dist/access.d.ts +172 -0
- package/dist/access.js +175 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +198 -0
- package/dist/commands.d.ts +189 -0
- package/dist/commands.js +202 -0
- package/dist/discovery.d.ts +73 -0
- package/dist/discovery.js +417 -0
- package/dist/errors.d.ts +15 -0
- package/dist/errors.js +22 -0
- package/dist/export.d.ts +14 -0
- package/dist/export.js +86 -0
- package/dist/guidance.d.ts +11 -0
- package/dist/guidance.js +75 -0
- package/dist/hooks.d.ts +4 -0
- package/dist/hooks.js +141 -0
- package/dist/init.d.ts +304 -0
- package/dist/init.js +150 -0
- package/dist/maintenance.d.ts +126 -0
- package/dist/maintenance.js +23 -0
- package/dist/matching.d.ts +17 -0
- package/dist/matching.js +32 -0
- package/dist/model.d.ts +974 -0
- package/dist/model.js +21 -0
- package/dist/navigation.d.ts +164 -0
- package/dist/navigation.js +164 -0
- package/dist/operations.d.ts +10 -0
- package/dist/operations.js +130 -0
- package/dist/paging.d.ts +5 -0
- package/dist/paging.js +32 -0
- package/dist/retrieval.d.ts +146 -0
- package/dist/retrieval.js +150 -0
- package/dist/review-files.d.ts +3 -0
- package/dist/review-files.js +106 -0
- package/dist/review.d.ts +86 -0
- package/dist/review.js +124 -0
- package/dist/server.d.ts +8 -0
- package/dist/server.js +105 -0
- package/dist/source-search.d.ts +63 -0
- package/dist/source-search.js +245 -0
- package/dist/store.d.ts +452 -0
- package/dist/store.js +718 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +2 -0
- package/dist/workflow.d.ts +450 -0
- package/dist/workflow.js +317 -0
- package/docs/architecture.md +65 -0
- package/docs/audit-0.4.0.md +42 -0
- package/docs/demo.md +42 -0
- package/docs/discovery.md +70 -0
- package/docs/knowledge-policy.md +51 -0
- package/docs/pillar-contract.md +98 -0
- package/docs/quiet-workflow.md +98 -0
- package/docs/releases.md +157 -0
- package/package.json +52 -0
- package/schemas/admission.schema.json +75 -0
- package/schemas/knowledge.schema.json +192 -0
- package/schemas/patch.schema.json +220 -0
- package/schemas/update.schema.json +218 -0
package/dist/init.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { version } from './version.js';
|
|
4
|
+
import { parse, modify, applyEdits } from 'jsonc-parser';
|
|
5
|
+
import { installHook } from './hooks.js';
|
|
6
|
+
import { refreshKnowledgeExport } from './export.js';
|
|
7
|
+
import { discover } from './discovery.js';
|
|
8
|
+
import { GroundError, errorPayload } from './errors.js';
|
|
9
|
+
export { discover } from './discovery.js';
|
|
10
|
+
import { rules, startGuide, policy, bootstrapNext } from './guidance.js';
|
|
11
|
+
export { rules } from './guidance.js';
|
|
12
|
+
async function managed(store, relative, body) {
|
|
13
|
+
await store.safe(relative, true);
|
|
14
|
+
const file = path.join(store.root, relative);
|
|
15
|
+
let old = '';
|
|
16
|
+
try {
|
|
17
|
+
old = await fs.readFile(file, 'utf8');
|
|
18
|
+
}
|
|
19
|
+
catch (e) {
|
|
20
|
+
if (e.code !== 'ENOENT')
|
|
21
|
+
throw e;
|
|
22
|
+
}
|
|
23
|
+
const start = relative === '.gitignore' ? '# common-ground:start' : '<!-- common-ground:start -->', end = relative === '.gitignore' ? '# common-ground:end' : '<!-- common-ground:end -->';
|
|
24
|
+
if (old.includes(start) !== old.includes(end) || old.split(start).length > 2 || old.split(end).length > 2 || (old.includes(start) && old.indexOf(end) < old.indexOf(start)))
|
|
25
|
+
throw new Error(`Malformed managed block in ${relative}`);
|
|
26
|
+
const block = `${start}\n${body.trim()}\n${end}`;
|
|
27
|
+
const next = old.includes(start) ? old.slice(0, old.indexOf(start)) + block + old.slice(old.indexOf(end) + end.length) : old + (old && !old.endsWith('\n') ? '\n' : '') + (old ? '\n' : '') + block + '\n';
|
|
28
|
+
if (next !== old) {
|
|
29
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
30
|
+
await fs.writeFile(file, next);
|
|
31
|
+
}
|
|
32
|
+
return next !== old;
|
|
33
|
+
}
|
|
34
|
+
export async function initialize(store, options = {}) {
|
|
35
|
+
const steps = ['local-directory', 'registry', 'mcp-validation', ...Object.keys(guidanceFiles), '.gitignore', 'mcp-config', 'discovery', 'hook', 'markdown'];
|
|
36
|
+
const completed = [], skipped = [], failed = [];
|
|
37
|
+
const retry = 'Retry cground init after fixing the failed step; completed managed writes are safe to repeat and existing knowledge and bootstrap drafts are preserved. Use cground init --skip-hook to omit the advisory hook, or cground hook install later.';
|
|
38
|
+
async function step(name, work) {
|
|
39
|
+
try {
|
|
40
|
+
const result = await work();
|
|
41
|
+
completed.push(name);
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
const cause = errorPayload(error);
|
|
46
|
+
throw new GroundError('INIT_INCOMPLETE', `Initialization failed at ${name}: ${cause.message}. Completed: ${completed.join(', ') || 'none'}.`, cause.fields, retry, { completed: completed.filter(id => !failed.some(f => f.step === id)), skipped: [...skipped], failed: [...failed, { step: name, error: cause }], pending: steps.slice(steps.indexOf(name) + 1), failedStepMayHaveWritten: true });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
await step('local-directory', async () => { await store.safe('.common-ground/local', true); await fs.mkdir(store.file('local'), { recursive: true }); });
|
|
50
|
+
const exists = await step('registry', async () => { try {
|
|
51
|
+
await store.read();
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
catch (e) {
|
|
55
|
+
if (e.code !== 'ENOENT')
|
|
56
|
+
throw e;
|
|
57
|
+
return false;
|
|
58
|
+
} });
|
|
59
|
+
const configFile = path.join(store.root, '.vscode/mcp.json');
|
|
60
|
+
let configText = '{}\n';
|
|
61
|
+
const nextConfig = await step('mcp-validation', async () => {
|
|
62
|
+
await store.safe('.vscode/mcp.json', true);
|
|
63
|
+
try {
|
|
64
|
+
configText = await fs.readFile(configFile, 'utf8');
|
|
65
|
+
}
|
|
66
|
+
catch (e) {
|
|
67
|
+
if (e.code !== 'ENOENT')
|
|
68
|
+
throw e;
|
|
69
|
+
}
|
|
70
|
+
const errors = [];
|
|
71
|
+
const config = parse(configText, errors, { allowTrailingComma: true });
|
|
72
|
+
if (errors.length || !config || typeof config !== 'object' || Array.isArray(config) || (config.servers && (typeof config.servers !== 'object' || Array.isArray(config.servers))))
|
|
73
|
+
throw new Error('Invalid .vscode/mcp.json; refusing to overwrite.');
|
|
74
|
+
if (config.servers?.commonGround && config.servers.commonGround.command !== 'cground')
|
|
75
|
+
throw new Error('Existing commonGround MCP entry conflicts; resolve it before init.');
|
|
76
|
+
const entry = { type: 'stdio', command: 'cground', args: ['serve', '--root', '${workspaceFolder}'] };
|
|
77
|
+
return applyEdits(configText, modify(configText, ['servers', 'commonGround'], entry, { formattingOptions: { insertSpaces: true, tabSize: 2 } }));
|
|
78
|
+
});
|
|
79
|
+
for (const [file, body] of Object.entries(guidanceFiles))
|
|
80
|
+
await step(file, () => managed(store, file, body));
|
|
81
|
+
await step('.gitignore', () => managed(store, '.gitignore', '.common-ground/local/'));
|
|
82
|
+
await step('mcp-config', async () => { await fs.mkdir(path.dirname(configFile), { recursive: true }); if (configText !== nextConfig)
|
|
83
|
+
await fs.writeFile(configFile, nextConfig); });
|
|
84
|
+
const proposal = await step('discovery', async () => {
|
|
85
|
+
if (exists)
|
|
86
|
+
return { existingRegistry: true, message: 'Existing pillars preserved. No new discovery or pillar creation.' };
|
|
87
|
+
try {
|
|
88
|
+
const file = await store.safe('.common-ground/local/bootstrap.json');
|
|
89
|
+
if (!(await fs.stat(file)).isFile())
|
|
90
|
+
throw new Error('Existing bootstrap draft is not a regular file.');
|
|
91
|
+
return { existingDraft: true, proposalPath: '.common-ground/local/bootstrap.json', message: 'Existing bootstrap draft preserved. Read it before refining; cground scan can provide fresh signals.' };
|
|
92
|
+
}
|
|
93
|
+
catch (e) {
|
|
94
|
+
if (e.code !== 'ENOENT')
|
|
95
|
+
throw e;
|
|
96
|
+
}
|
|
97
|
+
const proposal = await discover(store);
|
|
98
|
+
await store.atomic('local/bootstrap.json', proposal);
|
|
99
|
+
return proposal;
|
|
100
|
+
});
|
|
101
|
+
let hook;
|
|
102
|
+
if (options.skipHook) {
|
|
103
|
+
skipped.push('hook');
|
|
104
|
+
hook = 'Advisory hook skipped. Run cground hook install later to enable it.';
|
|
105
|
+
}
|
|
106
|
+
else
|
|
107
|
+
hook = await step('hook', async () => {
|
|
108
|
+
try {
|
|
109
|
+
return await installHook(store);
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
if (!['EROFS', 'EACCES', 'EPERM'].includes(error.code))
|
|
113
|
+
throw error;
|
|
114
|
+
failed.push({ step: 'hook', error: errorPayload(error) });
|
|
115
|
+
return `Advisory hook unavailable: ${error.message}. Repository setup continues. Run cground hook install when Git metadata is writable.`;
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
const markdown = await step('markdown', () => refreshKnowledgeExport(store));
|
|
119
|
+
const setup = { status: failed.length ? 'complete-with-warnings' : 'complete', completed: completed.filter(name => !failed.some(f => f.step === name)), skipped, failed, pending: [], retry: failed.length ? retry : null };
|
|
120
|
+
return exists ? { ...proposal, hook, markdown, setup } : { ...proposal, state: 'bootstrap-required', next: bootstrapNext, hook, markdown, setup };
|
|
121
|
+
}
|
|
122
|
+
const guidanceFiles = {
|
|
123
|
+
'AGENTS.md': rules,
|
|
124
|
+
'.common-ground/START_HERE.md': startGuide,
|
|
125
|
+
'.common-ground/POLICY.md': policy,
|
|
126
|
+
'.github/copilot-instructions.md': 'Common Ground repository knowledge rules are in [AGENTS.md](../AGENTS.md). Follow its Common Ground section when reading or maintaining pillars.\nCode is the source of truth; these notes only help navigation.\nFor repository questions, consult Common Ground and verify source. If knowledge is missing or stale, answer from source and prompt to update it.\nIf the developer disputes the code, clarify current versus intended behavior together. Record only verified, durable knowledge under the note-taking rules.',
|
|
127
|
+
};
|
|
128
|
+
export async function guidanceStatus(store) {
|
|
129
|
+
const files = {};
|
|
130
|
+
for (const [file, body] of Object.entries(guidanceFiles)) {
|
|
131
|
+
try {
|
|
132
|
+
const text = (await fs.readFile(await store.safe(file), 'utf8')).replace(/\r\n/g, '\n');
|
|
133
|
+
const expected = `<!-- common-ground:start -->\n${body.trim()}\n<!-- common-ground:end -->`;
|
|
134
|
+
files[file] = text.includes(expected) && text.split('<!-- common-ground:start -->').length === 2 && text.split('<!-- common-ground:end -->').length === 2 ? 'current' : 'stale';
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
if (error.code !== 'ENOENT')
|
|
138
|
+
throw error;
|
|
139
|
+
files[file] = 'missing';
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return { installedVersion: version, basis: 'Managed content compared with installed guidance templates', status: Object.values(files).every(value => value === 'current') ? 'current' : 'outdated', files,
|
|
143
|
+
next: Object.values(files).every(value => value === 'current') ? null : 'Run cground refresh-guidance to update managed instructions while preserving surrounding text.' };
|
|
144
|
+
}
|
|
145
|
+
export async function refreshGuidance(store) {
|
|
146
|
+
const changedFiles = [], unchangedFiles = [];
|
|
147
|
+
for (const [file, body] of Object.entries(guidanceFiles))
|
|
148
|
+
(await managed(store, file, body) ? changedFiles : unchangedFiles).push(file);
|
|
149
|
+
return { writesKnowledge: false, changedFiles, unchangedFiles, ...await guidanceStatus(store) };
|
|
150
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { Store } from './store.js';
|
|
2
|
+
export declare function checkKnowledge(store: Store, target?: string, cleanup?: boolean, cursor?: string, limit?: number, expectedAffected?: unknown, allResults?: boolean): Promise<{
|
|
3
|
+
cleanup: {
|
|
4
|
+
targets: string[];
|
|
5
|
+
next: string;
|
|
6
|
+
tidyId: null;
|
|
7
|
+
instruction: string;
|
|
8
|
+
requiredChapters?: undefined;
|
|
9
|
+
} | {
|
|
10
|
+
targets: string[];
|
|
11
|
+
next: string;
|
|
12
|
+
tidyId: `${string}-${string}-${string}-${string}-${string}`;
|
|
13
|
+
requiredChapters: string[];
|
|
14
|
+
instruction: string;
|
|
15
|
+
} | null;
|
|
16
|
+
markdown: {
|
|
17
|
+
path: string;
|
|
18
|
+
changed: boolean;
|
|
19
|
+
};
|
|
20
|
+
items: {
|
|
21
|
+
dependencyIssues: {
|
|
22
|
+
recordChanged: boolean;
|
|
23
|
+
changedPaths: string[];
|
|
24
|
+
errors: string[];
|
|
25
|
+
factId: string;
|
|
26
|
+
}[];
|
|
27
|
+
locallyReviewed: boolean;
|
|
28
|
+
changedPaths: string[];
|
|
29
|
+
errors: string[];
|
|
30
|
+
factId: string;
|
|
31
|
+
chapterId: string;
|
|
32
|
+
status: string;
|
|
33
|
+
}[];
|
|
34
|
+
total: number;
|
|
35
|
+
nextCursor: string | null;
|
|
36
|
+
target: string;
|
|
37
|
+
writesKnowledge: boolean;
|
|
38
|
+
valid: boolean;
|
|
39
|
+
basis: string;
|
|
40
|
+
affected: {
|
|
41
|
+
pillars: string[];
|
|
42
|
+
chapters: string[];
|
|
43
|
+
facts: string[];
|
|
44
|
+
};
|
|
45
|
+
cleanupPrompt: string | null;
|
|
46
|
+
summary: {
|
|
47
|
+
selectedFacts: number;
|
|
48
|
+
checkedFacts: number;
|
|
49
|
+
needsReview: number;
|
|
50
|
+
unpopulatedChapters: string[];
|
|
51
|
+
};
|
|
52
|
+
}>;
|
|
53
|
+
export declare function startTidy(store: Store, target: string, cursor?: string, limit?: number): Promise<{
|
|
54
|
+
markdown: {
|
|
55
|
+
path: string;
|
|
56
|
+
changed: boolean;
|
|
57
|
+
};
|
|
58
|
+
tidyId: null;
|
|
59
|
+
instruction: string;
|
|
60
|
+
requiredChapters?: undefined;
|
|
61
|
+
target: string;
|
|
62
|
+
writesKnowledge: boolean;
|
|
63
|
+
agentActionRequired: boolean;
|
|
64
|
+
policy: string;
|
|
65
|
+
next: string;
|
|
66
|
+
requiredChapterCount: number;
|
|
67
|
+
chapters: {
|
|
68
|
+
items: {
|
|
69
|
+
chapterId: string;
|
|
70
|
+
revision: number;
|
|
71
|
+
factCount: number;
|
|
72
|
+
duplicateGroupCount: number;
|
|
73
|
+
duplicateHints: string[][];
|
|
74
|
+
}[];
|
|
75
|
+
total: number;
|
|
76
|
+
nextCursor: string | null;
|
|
77
|
+
};
|
|
78
|
+
} | {
|
|
79
|
+
markdown: {
|
|
80
|
+
path: string;
|
|
81
|
+
changed: boolean;
|
|
82
|
+
};
|
|
83
|
+
tidyId: `${string}-${string}-${string}-${string}-${string}`;
|
|
84
|
+
requiredChapters: string[];
|
|
85
|
+
instruction: string;
|
|
86
|
+
target: string;
|
|
87
|
+
writesKnowledge: boolean;
|
|
88
|
+
agentActionRequired: boolean;
|
|
89
|
+
policy: string;
|
|
90
|
+
next: string;
|
|
91
|
+
requiredChapterCount: number;
|
|
92
|
+
chapters: {
|
|
93
|
+
items: {
|
|
94
|
+
chapterId: string;
|
|
95
|
+
revision: number;
|
|
96
|
+
factCount: number;
|
|
97
|
+
duplicateGroupCount: number;
|
|
98
|
+
duplicateHints: string[][];
|
|
99
|
+
}[];
|
|
100
|
+
total: number;
|
|
101
|
+
nextCursor: string | null;
|
|
102
|
+
};
|
|
103
|
+
} | {
|
|
104
|
+
markdown: {
|
|
105
|
+
path: string;
|
|
106
|
+
changed: boolean;
|
|
107
|
+
};
|
|
108
|
+
target: string;
|
|
109
|
+
writesKnowledge: boolean;
|
|
110
|
+
agentActionRequired: boolean;
|
|
111
|
+
policy: string;
|
|
112
|
+
next: string;
|
|
113
|
+
requiredChapterCount: number;
|
|
114
|
+
chapters: {
|
|
115
|
+
items: {
|
|
116
|
+
chapterId: string;
|
|
117
|
+
revision: number;
|
|
118
|
+
factCount: number;
|
|
119
|
+
duplicateGroupCount: number;
|
|
120
|
+
duplicateHints: string[][];
|
|
121
|
+
}[];
|
|
122
|
+
total: number;
|
|
123
|
+
nextCursor: string | null;
|
|
124
|
+
};
|
|
125
|
+
}>;
|
|
126
|
+
export declare function staleMessage(result: Awaited<ReturnType<typeof checkKnowledge>>): string;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { same } from './store.js';
|
|
2
|
+
import { validateKnowledge, tidyPlan } from './navigation.js';
|
|
3
|
+
import { refreshKnowledgeExport } from './export.js';
|
|
4
|
+
export async function checkKnowledge(store, target = 'all', cleanup = false, cursor, limit, expectedAffected, allResults = true) {
|
|
5
|
+
const registry = await store.read();
|
|
6
|
+
const result = await validateKnowledge(store, target, cursor, limit, allResults);
|
|
7
|
+
if (expectedAffected && !same(expectedAffected, result.affected))
|
|
8
|
+
throw new Error('Affected knowledge changed; rerun the check and confirm the new cleanup scope.');
|
|
9
|
+
if (!same(registry, await store.read()))
|
|
10
|
+
throw new Error('Knowledge changed; rerun the check.');
|
|
11
|
+
const cleanupPlan = cleanup && result.affected.chapters.length
|
|
12
|
+
? { ...await store.requestTidy(result.affected.chapters, registry), targets: result.affected.chapters,
|
|
13
|
+
next: 'Start task_context if needed. Read every required chapter and source/documentation using read_knowledge checklist/review. Submit verified corrections with prepare_patch (or prepare_update) and tidyId, then commit_update. Re-run validate. Do not add facts or expand ownership without developer approval.' }
|
|
14
|
+
: null;
|
|
15
|
+
return { ...result, cleanup: cleanupPlan, markdown: await refreshKnowledgeExport(store) };
|
|
16
|
+
}
|
|
17
|
+
export async function startTidy(store, target, cursor, limit) {
|
|
18
|
+
const registry = await store.read();
|
|
19
|
+
return { ...await tidyPlan(store, target, cursor, limit), ...(!cursor ? await store.requestTidy(target, registry) : {}), markdown: await refreshKnowledgeExport(store) };
|
|
20
|
+
}
|
|
21
|
+
export function staleMessage(result) {
|
|
22
|
+
return ['pillars', 'chapters', 'facts'].filter(kind => result.affected[kind].length).map(kind => `The following ${kind.slice(0, -1).toUpperCase()} is out of date:\n${result.affected[kind].map(id => ` ${id}`).join('\n')}`).join('\n');
|
|
23
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Shared technical tokens. Versions remain atomic; identifiers split at camel-case boundaries. */
|
|
2
|
+
export declare function words(text: string): string[];
|
|
3
|
+
export declare const queryTerms: (text: string) => string[];
|
|
4
|
+
export declare const isVersion: (term: string) => boolean;
|
|
5
|
+
export declare function termMatch(text: string, term: string): number;
|
|
6
|
+
/** Query weights derive from the current registry, never from durable search statistics. */
|
|
7
|
+
export declare function termWeights(terms: string[], documents: string[]): {
|
|
8
|
+
weights: Map<string, number>;
|
|
9
|
+
commonTerms: string[];
|
|
10
|
+
distinctiveTerms: string[];
|
|
11
|
+
};
|
|
12
|
+
export declare function matchFields(terms: string[], topic: string, statement: string, weights?: Map<string, number>): {
|
|
13
|
+
matchedTerms: string[];
|
|
14
|
+
weight: number;
|
|
15
|
+
};
|
|
16
|
+
/** A literal path in query text gets the same priority as --path, without filesystem probing. */
|
|
17
|
+
export declare function queryPath(query: string | undefined): string | undefined;
|
package/dist/matching.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Shared technical tokens. Versions remain atomic; identifiers split at camel-case boundaries. */
|
|
2
|
+
export function words(text) {
|
|
3
|
+
const normalized = text.replace(/(\p{Ll}|\p{N})(\p{Lu})/gu, '$1 $2').replace(/(\p{Lu})(\p{Lu}\p{Ll})/gu, '$1 $2').toLowerCase();
|
|
4
|
+
return [...new Set((normalized.match(/v?\d+(?:\.\d+)+(?:[-+][\p{L}\p{N}]+(?:[.-][\p{L}\p{N}]+)*)?|[\p{L}\p{N}]+/gu) ?? []).map(token => token.replace(/^v(?=\d+\.)/, '')))];
|
|
5
|
+
}
|
|
6
|
+
const stopWords = new Set(['a', 'an', 'the', 'and', 'or', 'of', 'to', 'in', 'on', 'for', 'from', 'with', 'by', 'is', 'are', 'was', 'be', 'this', 'that', 'it', 'its', 'how', 'what', 'where', 'which', 'when', 'does', 'do', 'can', 'our', 'my']);
|
|
7
|
+
export const queryTerms = (text) => words(text).filter(term => !stopWords.has(term));
|
|
8
|
+
export const isVersion = (term) => /^\d+(?:\.\d+)+(?:[-+].+)?$/.test(term);
|
|
9
|
+
export function termMatch(text, term) {
|
|
10
|
+
const tokens = words(text);
|
|
11
|
+
if (tokens.includes(term))
|
|
12
|
+
return 2;
|
|
13
|
+
return !isVersion(term) && term.length > 3 && tokens.some(token => !isVersion(token) && token.includes(term)) ? 1 : 0;
|
|
14
|
+
}
|
|
15
|
+
/** Query weights derive from the current registry, never from durable search statistics. */
|
|
16
|
+
export function termWeights(terms, documents) {
|
|
17
|
+
const frequencies = new Map(terms.map(term => [term, documents.filter(text => termMatch(text, term) > 0).length]));
|
|
18
|
+
const commonTerms = terms.filter(term => documents.length >= 3 && frequencies.get(term) / documents.length >= 0.6 && !isVersion(term));
|
|
19
|
+
const weights = new Map(terms.map(term => [term, commonTerms.includes(term) ? 0.2 : 1 + Math.log2((documents.length + 1) / (frequencies.get(term) + 1))]));
|
|
20
|
+
return { weights, commonTerms, distinctiveTerms: terms.filter(term => !commonTerms.includes(term)) };
|
|
21
|
+
}
|
|
22
|
+
export function matchFields(terms, topic, statement, weights) {
|
|
23
|
+
const strengths = terms.map(term => Math.max(termMatch(topic, term) * 2, termMatch(statement, term)));
|
|
24
|
+
return { matchedTerms: terms.filter((_, index) => strengths[index] > 0), weight: strengths.reduce((n, strength, index) => n + strength * (weights?.get(terms[index]) ?? 1), 0) };
|
|
25
|
+
}
|
|
26
|
+
/** A literal path in query text gets the same priority as --path, without filesystem probing. */
|
|
27
|
+
export function queryPath(query) {
|
|
28
|
+
const candidate = query?.trim().replace(/^['"`]|['"`]$/g, '');
|
|
29
|
+
if (!candidate || !candidate.includes('/') || /\s/.test(candidate) || candidate.startsWith('/') || candidate.includes('\\') || candidate.split('/').some(p => !p || p === '.' || p === '..'))
|
|
30
|
+
return undefined;
|
|
31
|
+
return candidate;
|
|
32
|
+
}
|