@h1v35/hivex 0.1.0 → 0.2.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.
@@ -3,15 +3,47 @@ import { z } from 'zod';
3
3
  import { HivexError } from '../errors.ts';
4
4
 
5
5
  const row = z.object({ id: z.string(), score: z.number() });
6
- export const searchTerms = (text: string) => [
7
- ...new Set(
8
- text
9
- .toLowerCase()
10
- .normalize('NFKC')
11
- .match(/[\p{L}\p{N}]+/gu) ?? [],
12
- ),
13
- ];
14
- type Record = { id: string; title: string; content: string };
6
+ export const searchTerms = (text: string) => {
7
+ const terms = text
8
+ .toLowerCase()
9
+ .normalize('NFKC')
10
+ .match(/[\p{L}\p{N}]+/gu);
11
+ if (terms === null) {
12
+ return [];
13
+ }
14
+ return [...new Set(terms)];
15
+ };
16
+ interface Record {
17
+ id: string;
18
+ title: string;
19
+ content: string;
20
+ }
21
+ interface RankOptions {
22
+ database: Database;
23
+ excludedId?: string | null;
24
+ limit: number;
25
+ terms: string[];
26
+ }
27
+
28
+ const rank = function rank({ database, excludedId = null, limit, terms }: RankOptions) {
29
+ const expression = terms.map((term) => `"${term.replaceAll('"', '""')}"`).join(' OR ');
30
+ if (!expression) {
31
+ return [];
32
+ }
33
+ return database
34
+ .prepare(
35
+ 'SELECT id, bm25(sources) AS score FROM sources WHERE sources MATCH ? AND (? IS NULL OR id != ?) ORDER BY score, id LIMIT ?'
36
+ )
37
+ .all(expression, excludedId, excludedId, limit)
38
+ .map((value) => row.parse(value));
39
+ };
40
+
41
+ const prepareVocabulary = function prepareVocabulary(database: Database) {
42
+ database.run("CREATE VIRTUAL TABLE vocabulary USING fts5vocab(sources, 'row')");
43
+ database.run("CREATE VIRTUAL TABLE instances USING fts5vocab(sources, 'instance')");
44
+ database.run('CREATE TABLE document_terms AS SELECT DISTINCT doc, term FROM instances');
45
+ database.run('CREATE INDEX document_terms_by_doc ON document_terms(doc)');
46
+ };
15
47
 
16
48
  export class LexicalIndex {
17
49
  private readonly db = new Database(':memory:');
@@ -23,10 +55,10 @@ export class LexicalIndex {
23
55
  this.db.run('PRAGMA page_size=4096');
24
56
  this.db.run('PRAGMA max_page_count=32768');
25
57
  this.db.run(
26
- 'CREATE VIRTUAL TABLE sources USING fts5(id UNINDEXED, title, content, tokenize=unicode61)',
58
+ 'CREATE VIRTUAL TABLE sources USING fts5(id UNINDEXED, title, content, tokenize=unicode61)'
27
59
  );
28
60
  const insert = this.db.prepare(
29
- 'INSERT INTO sources(rowid, id, title, content) VALUES (?, ?, ?, ?)',
61
+ 'INSERT INTO sources(rowid, id, title, content) VALUES (?, ?, ?, ?)'
30
62
  );
31
63
  this.db.transaction(() => {
32
64
  for (const [index, record] of records.entries()) {
@@ -41,44 +73,31 @@ export class LexicalIndex {
41
73
  }
42
74
 
43
75
  search(query: string, limit = 32) {
44
- return this.rank(searchTerms(query), limit);
45
- }
46
-
47
- private rank(terms: string[], limit: number, excludedId: string | null = null) {
48
- const expression = terms.map((term) => `"${term.replaceAll('"', '""')}"`).join(' OR ');
49
- if (!expression) return [];
50
- return this.db
51
- .prepare(
52
- 'SELECT id, bm25(sources) AS score FROM sources WHERE sources MATCH ? AND (? IS NULL OR id != ?) ORDER BY score, id LIMIT ?',
53
- )
54
- .all(expression, excludedId, excludedId, limit)
55
- .map((value) => row.parse(value));
56
- }
57
-
58
- private prepareVocabulary() {
59
- if (this.vocabularyReady) return;
60
- this.db.run("CREATE VIRTUAL TABLE vocabulary USING fts5vocab(sources, 'row')");
61
- this.db.run("CREATE VIRTUAL TABLE instances USING fts5vocab(sources, 'instance')");
62
- this.db.run('CREATE TABLE document_terms AS SELECT DISTINCT doc, term FROM instances');
63
- this.db.run('CREATE INDEX document_terms_by_doc ON document_terms(doc)');
64
- this.vocabularyReady = true;
76
+ return rank({ database: this.db, limit, terms: searchTerms(query) });
65
77
  }
66
78
 
67
79
  neighbors(id: string, limit: number) {
68
- const doc = this.ids.get(id);
69
- if (doc === undefined)
80
+ const documentId = this.ids.get(id);
81
+ if (documentId === undefined) {
70
82
  throw new HivexError({
71
83
  code: 'LEXICAL_SOURCE_NOT_FOUND',
72
84
  message: 'The source is not in this lexical index',
73
85
  });
74
- this.prepareVocabulary();
86
+ }
87
+ if (!this.vocabularyReady) {
88
+ prepareVocabulary(this.db);
89
+ this.vocabularyReady = true;
90
+ }
75
91
  const terms = this.db
76
92
  .query<{ term: string }, [number]>(
77
- 'SELECT t.term FROM document_terms t JOIN vocabulary v ON v.term=t.term WHERE t.doc=? AND v.doc>=2 ORDER BY v.doc, t.term LIMIT 32',
93
+ 'SELECT t.term FROM document_terms t JOIN vocabulary v ON v.term=t.term WHERE t.doc=? AND v.doc>=2 ORDER BY v.doc, t.term LIMIT 32'
78
94
  )
79
- .all(doc)
80
- .map((row) => row.term);
81
- return { terms, matches: this.rank(terms, limit, id) };
95
+ .all(documentId)
96
+ .map((entry) => entry.term);
97
+ return {
98
+ matches: rank({ database: this.db, excludedId: id, limit, terms }),
99
+ terms,
100
+ };
82
101
  }
83
102
 
84
103
  [Symbol.dispose]() {
@@ -86,7 +105,11 @@ export class LexicalIndex {
86
105
  }
87
106
  }
88
107
 
89
- export function rankLexically(records: readonly Record[], query: string, limit = 32) {
108
+ export const rankLexically = function rankLexically(
109
+ records: readonly Record[],
110
+ query: string,
111
+ limit = 32
112
+ ) {
90
113
  using index = new LexicalIndex(records);
91
114
  return index.search(query, limit);
92
- }
115
+ };
package/src/review.ts CHANGED
@@ -1,32 +1,30 @@
1
1
  import { readFileSync, statSync } from 'node:fs';
2
- import { resolve } from 'node:path';
2
+ import pathModule from 'node:path';
3
3
  import { parseArgs } from 'node:util';
4
4
  import { z } from 'zod';
5
- import { loadProject, type Project } from './documents.ts';
6
- import { captureImplementation, type Implementation } from './implementation.ts';
7
- import {
8
- citationSchema,
9
- sourceEvidence,
10
- suppliedCitation,
11
- type SuppliedDocument,
12
- } from './knowledge-model.ts';
5
+ import { loadProject } from './documents.ts';
6
+ import { captureImplementation } from './implementation.ts';
7
+ import { citationSchema, sourceEvidence, suppliedCitation } from './knowledge-model.ts';
13
8
  import { HivexError } from './errors.ts';
9
+ import type { Project } from './documents.ts';
10
+ import type { Implementation } from './implementation.ts';
11
+ import type { SuppliedDocument } from './knowledge-model.ts';
14
12
 
15
13
  const codeCitation = z.object({
14
+ lineEnd: z.number().int().positive(),
15
+ lineStart: z.number().int().positive(),
16
16
  path: z.string().min(1),
17
17
  side: z.enum(['before', 'after']),
18
- lineStart: z.number().int().positive(),
19
- lineEnd: z.number().int().positive(),
20
18
  });
21
19
  export const reviewSchema = z.object({
22
20
  findings: z
23
21
  .array(
24
22
  z.object({
25
23
  assessment: z.enum(['conflict', 'exception', 'uncertain']),
26
- explanation: z.string().min(1).max(4096),
27
- documents: z.array(citationSchema).max(8),
28
24
  code: z.array(codeCitation).max(8),
29
- }),
25
+ documents: z.array(citationSchema).max(8),
26
+ explanation: z.string().min(1).max(4096),
27
+ })
30
28
  )
31
29
  .max(12),
32
30
  uncertainties: z.array(z.string().min(1).max(2048)).max(24),
@@ -34,96 +32,126 @@ export const reviewSchema = z.object({
34
32
  export const reviewInstructions =
35
33
  'Assist the principal reviewer with the task and implementation diff. Discover possible conflicts without requiring suspicions. Explain how documentary rules, direct/indirect dependencies, conditions and exceptions apply. Findings may identify a conflict, a valid exception, or uncertainty; do not turn missing context into approval or reject the entire change. Cite the supplied Markdown ranges and before/after code lines supporting each finding. Distinguish a rule violated by the change from behavior merely seen in context. The reviewer must verify each finding. This is knowledge assistance, not general code review, lint, tests or implementation approval.';
36
34
 
37
- function codeEvidence(citation: z.infer<typeof codeCitation>, implementation: Implementation) {
35
+ const codeEvidence = function codeEvidence(
36
+ citation: z.infer<typeof codeCitation>,
37
+ implementation: Implementation
38
+ ) {
38
39
  const file = implementation.files.find((entry) => entry.path === citation.path)?.[citation.side];
39
- if (!file || citation.lineEnd < citation.lineStart) return null;
40
+ if (!file || citation.lineEnd < citation.lineStart) {
41
+ return null;
42
+ }
40
43
  const lines = file.lines.filter(
41
- ([number]) => number >= citation.lineStart && number <= citation.lineEnd,
44
+ ([number]) => number >= citation.lineStart && number <= citation.lineEnd
42
45
  );
43
- if (lines.length !== citation.lineEnd - citation.lineStart + 1) return null;
44
- return { ...citation, version: file.version, text: lines.map(([, text]) => text).join('\n') };
45
- }
46
- export function materializeReview(
46
+ if (lines.length !== citation.lineEnd - citation.lineStart + 1) {
47
+ return null;
48
+ }
49
+ return {
50
+ ...citation,
51
+ text: lines.map(([, text]) => text).join('\n'),
52
+ version: file.version,
53
+ };
54
+ };
55
+
56
+ export const materializeReview = function materializeReview(
47
57
  project: Project,
48
58
  implementation: Implementation,
49
- value: unknown,
50
- supplied: SuppliedDocument[],
59
+ { supplied, value }: { supplied: SuppliedDocument[]; value: unknown }
51
60
  ) {
52
61
  const response = reviewSchema.parse(value);
53
62
  const findings = response.findings.map((finding) => {
54
63
  const documents = finding.documents
55
- .map((citation) =>
56
- suppliedCitation(citation, supplied) ? sourceEvidence(citation, project) : null,
57
- )
64
+ .map((citation) => {
65
+ const isSupplied = suppliedCitation(citation, supplied);
66
+ return isSupplied ? sourceEvidence(citation, project) : null;
67
+ })
58
68
  .filter((entry) => entry !== null);
59
69
  const code = finding.code
60
70
  .map((citation) => codeEvidence(citation, implementation))
61
71
  .filter((entry) => entry !== null);
62
- const referencesVerified =
72
+ const areReferencesVerified =
63
73
  documents.length > 0 &&
64
74
  code.length > 0 &&
65
75
  documents.length === finding.documents.length &&
66
76
  code.length === finding.code.length;
67
77
  return {
68
78
  ...finding,
69
- assessment: referencesVerified ? finding.assessment : 'uncertain',
70
- documents,
79
+ assessment: areReferencesVerified ? finding.assessment : 'uncertain',
71
80
  code,
72
- referencesVerified,
81
+ documents,
82
+ referencesVerified: areReferencesVerified,
73
83
  };
74
84
  });
75
85
  return {
76
86
  findings,
77
- uncertainties: response.uncertainties,
78
87
  invalidReferences: findings.some((finding) => !finding.referencesVerified),
88
+ uncertainties: response.uncertainties,
79
89
  };
80
- }
90
+ };
81
91
 
82
92
  const bindingSchema = z.object({
83
- baseCommit: z.string().regex(/^[a-f0-9]{40,64}$/),
84
- implementation: z.string().regex(/^[a-f0-9]{64}$/),
85
- documents: z.string().regex(/^[a-f0-9]{64}$/),
93
+ baseCommit: z.string().regex(/^[a-f\d]{40,64}$/u),
94
+ documents: z.string().regex(/^[a-f\d]{64}$/u),
95
+ implementation: z.string().regex(/^[a-f\d]{64}$/u),
86
96
  });
87
- export function reviewBinding(project: Project, implementation: Implementation) {
97
+ export const reviewBinding = function reviewBinding(
98
+ project: Project,
99
+ implementation: Implementation
100
+ ) {
88
101
  return {
89
102
  baseCommit: implementation.baseCommit,
90
- implementation: implementation.fingerprint,
91
103
  documents: project.snapshot,
104
+ implementation: implementation.fingerprint,
92
105
  };
93
- }
94
- export function reviewFreshness(root: string, binding: z.infer<typeof bindingSchema>) {
106
+ };
107
+
108
+ export const reviewFreshness = function reviewFreshness(
109
+ root: string,
110
+ binding: z.infer<typeof bindingSchema>
111
+ ) {
95
112
  const project = loadProject(root);
96
113
  const implementation = captureImplementation(root, binding.baseCommit);
97
- const documentsChanged = project.snapshot !== binding.documents;
98
- const implementationChanged = implementation.fingerprint !== binding.implementation;
114
+ const areDocumentsChanged = project.snapshot !== binding.documents;
115
+ const isImplementationChanged = implementation.fingerprint !== binding.implementation;
99
116
  return {
100
- status: documentsChanged || implementationChanged ? 'stale' : 'current',
101
- documentsChanged,
102
- implementationChanged,
117
+ documentsChanged: areDocumentsChanged,
118
+ implementationChanged: isImplementationChanged,
119
+ status: areDocumentsChanged || isImplementationChanged ? 'stale' : 'current',
103
120
  };
104
- }
105
- export function checkReview(args: string[]) {
121
+ };
122
+
123
+ export const checkReview = function checkReview(reviewArguments: string[]) {
106
124
  const parsed = parseArgs({
107
- args,
108
125
  allowPositionals: true,
126
+ args: reviewArguments,
127
+ options: { check: { type: 'string' }, root: { type: 'string' } },
109
128
  strict: true,
110
- options: { root: { type: 'string' }, check: { type: 'string' } },
111
129
  });
112
- if (parsed.positionals.length !== 1 || parsed.positionals[0] !== 'review' || !parsed.values.check)
130
+ if (
131
+ parsed.positionals.length !== 1 ||
132
+ parsed.positionals[0] !== 'review' ||
133
+ typeof parsed.values.check !== 'string' ||
134
+ parsed.values.check.length === 0
135
+ ) {
113
136
  throw new HivexError({
114
137
  code: 'INVALID_ARGUMENT',
115
138
  message: 'Use review --check <saved-report.json> [--root <project>].',
116
139
  });
140
+ }
117
141
  const root = parsed.values.root ?? process.cwd();
118
- const path = resolve(root, parsed.values.check);
119
- if (statSync(path).size > 1048576)
120
- throw new HivexError({ code: 'INVALID_REVIEW', message: 'Saved review exceeds 1 MiB.' });
142
+ const reportPath = pathModule.resolve(root, parsed.values.check);
143
+ if (statSync(reportPath).size > 1_048_576) {
144
+ throw new HivexError({
145
+ code: 'INVALID_REVIEW',
146
+ message: 'Saved review exceeds 1 MiB.',
147
+ });
148
+ }
121
149
  const report = z
122
- .object({ command: z.literal('review'), binding: bindingSchema })
123
- .parse(JSON.parse(readFileSync(path, 'utf8')));
150
+ .object({ binding: bindingSchema, command: z.literal('review') })
151
+ .parse(JSON.parse(readFileSync(reportPath, 'utf-8')));
124
152
  return {
125
153
  command: 'review-check',
126
154
  ...reviewFreshness(root, report.binding),
127
155
  guidance: 'Current means the versions still match, not that the implementation is approved.',
128
156
  };
129
- }
157
+ };
@@ -0,0 +1,4 @@
1
+ // Bun 1.4 implements Iterator.concat before TypeScript includes its declaration.
2
+ interface IteratorConstructor {
3
+ concat: <T>(...iterables: Iterable<T>[]) => IteratorObject<T, undefined>;
4
+ }
@@ -0,0 +1,105 @@
1
+ import { parseArgs } from 'node:util';
2
+ import { compareSerializedStrings } from './ordering.ts';
3
+ import { HivexError } from './errors.ts';
4
+ import { ingestionUnits } from './ingestion-units.ts';
5
+ import { KnowledgeStore } from './knowledge-store.ts';
6
+ import { readKnowledgeSnapshot, writeKnowledgeSnapshot } from './knowledge-snapshot.ts';
7
+ import { loadProject } from './documents.ts';
8
+ import type { Graph } from './knowledge-model.ts';
9
+ import type { Project } from './documents.ts';
10
+
11
+ const sourceVersion = function sourceVersion([document, version]: [string, string]) {
12
+ return { document, version };
13
+ };
14
+
15
+ const sourceVersions = function sourceVersions(project: Project, graph: Graph) {
16
+ const references = [
17
+ ...Object.entries(graph.documents).map(sourceVersion),
18
+ ...Object.values(graph.units),
19
+ ...graph.decisions,
20
+ ...graph.relationships.flatMap((edge) => edge.evidence),
21
+ ];
22
+ const current = new Set<string>();
23
+ const stale = new Set<string>();
24
+ const unavailable = new Set<string>();
25
+ for (const reference of references) {
26
+ const source = project.documents.find((document) => document.id === reference.document);
27
+ if (source === undefined) {
28
+ unavailable.add(reference.document);
29
+ } else if (source.hash === reference.version) {
30
+ current.add(reference.document);
31
+ } else {
32
+ stale.add(reference.document);
33
+ }
34
+ }
35
+ return {
36
+ current: [...current].filter((id) => !stale.has(id)).toSorted(compareSerializedStrings),
37
+ stale: [...stale].toSorted(compareSerializedStrings),
38
+ unavailable: [...unavailable].toSorted(compareSerializedStrings),
39
+ };
40
+ };
41
+
42
+ const snapshotReport = function snapshotReport(project: Project, graph: Graph, operation: string) {
43
+ const plan = ingestionUnits(project.currentDocuments);
44
+ const pending = plan.units.filter((unit) => {
45
+ const source = project.documents.find((document) => document.id === unit.document);
46
+ return graph.units[unit.id]?.version !== source?.hash;
47
+ });
48
+ const sources = sourceVersions(project, graph);
49
+ const warnings = [...graph.warnings, ...project.warnings, ...plan.warnings];
50
+ const isPartial = [
51
+ pending.length > 0,
52
+ warnings.length > 0,
53
+ sources.stale.length > 0,
54
+ sources.unavailable.length > 0,
55
+ [...graph.decisions, ...graph.relationships].some((entry) => entry.quality !== 'checked'),
56
+ ].includes(true);
57
+ return {
58
+ command: 'snapshot',
59
+ decisions: graph.decisions.length,
60
+ modelCalls: 0,
61
+ operation,
62
+ path: '.hivex/graph.json',
63
+ pendingUnits: pending.map((unit) => unit.id),
64
+ relationships: graph.relationships.length,
65
+ sources,
66
+ status: isPartial ? 'partial' : 'ready',
67
+ warnings,
68
+ };
69
+ };
70
+
71
+ export const snapshotCommand = function snapshotCommand(argumentsList: string[]) {
72
+ const { positionals, values } = parseArgs({
73
+ allowPositionals: true,
74
+ args: argumentsList,
75
+ options: { root: { type: 'string' } },
76
+ strict: true,
77
+ });
78
+ const [, operation] = positionals;
79
+ if (
80
+ positionals.length !== 2 ||
81
+ positionals[0] !== 'snapshot' ||
82
+ (operation !== 'export' && operation !== 'import')
83
+ ) {
84
+ throw new HivexError({
85
+ code: 'INVALID_ARGUMENT',
86
+ message: 'Use snapshot export | import [--root <project>]',
87
+ });
88
+ }
89
+ const project = loadProject(values.root ?? process.cwd());
90
+ const incoming = operation === 'import' ? readKnowledgeSnapshot(project.root) : null;
91
+ if (operation === 'import' && incoming === null) {
92
+ throw new HivexError({
93
+ code: 'SNAPSHOT_NOT_FOUND',
94
+ message: 'No .hivex/graph.json snapshot is available.',
95
+ });
96
+ }
97
+ using store = new KnowledgeStore(project.root, { update: true });
98
+ const graph = incoming ?? store.graph();
99
+ if (incoming === null) {
100
+ writeKnowledgeSnapshot(project.root, graph);
101
+ } else {
102
+ store.importGraph(incoming);
103
+ }
104
+ return snapshotReport(project, graph, operation);
105
+ };