@h1v35/hivex 0.2.0 → 0.2.2

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.
Files changed (39) hide show
  1. package/README.md +55 -163
  2. package/docs/CONTEXT.md +20 -36
  3. package/docs/README.md +6 -12
  4. package/docs/adr/0003-independent-bun-installation.md +5 -19
  5. package/docs/adr/0010-practical-knowledge-assistance.md +28 -81
  6. package/docs/adr/0011-shared-knowledge-and-selective-history.md +16 -43
  7. package/docs/guidelines/engineering.md +74 -0
  8. package/docs/procedures/self-hosted-runner.md +7 -0
  9. package/package.json +32 -11
  10. package/skills/hivex/SKILL.md +28 -92
  11. package/skills/hivex/references/markdown.md +12 -42
  12. package/src/cli/diagnostic.ts +21 -11
  13. package/src/cli.ts +46 -36
  14. package/src/documents.ts +502 -320
  15. package/src/errors.ts +8 -6
  16. package/src/implementation.ts +185 -87
  17. package/src/ingestion-units.ts +107 -64
  18. package/src/knowledge-maintenance.ts +35 -22
  19. package/src/knowledge-model.ts +386 -268
  20. package/src/knowledge-serialization.ts +239 -0
  21. package/src/knowledge-snapshot.ts +100 -77
  22. package/src/knowledge-store.ts +634 -453
  23. package/src/knowledge.ts +1001 -758
  24. package/src/markdown.ts +107 -45
  25. package/src/model/connection.ts +134 -76
  26. package/src/model/failure.ts +46 -23
  27. package/src/model/invoke.ts +346 -166
  28. package/src/model/profile.ts +201 -103
  29. package/src/model/rpc-error.ts +21 -0
  30. package/src/model/server.ts +151 -82
  31. package/src/model/thread.ts +24 -14
  32. package/src/model/transcript.ts +87 -46
  33. package/src/ordering.ts +9 -0
  34. package/src/retrieval/lexical.ts +64 -41
  35. package/src/review.ts +83 -55
  36. package/src/runtime.d.ts +4 -0
  37. package/src/snapshot-command.ts +82 -43
  38. package/src/source-relocation.ts +222 -0
  39. package/docs/engineering.md +0 -174
@@ -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
+ }
@@ -1,91 +1,130 @@
1
1
  import { parseArgs } from 'node:util';
2
+ import { compareSerializedStrings } from './ordering.ts';
2
3
  import { HivexError } from './errors.ts';
3
- import { loadProject, type Project } from './documents.ts';
4
- import type { Graph } from './knowledge-model.ts';
5
4
  import { ingestionUnits } from './ingestion-units.ts';
6
5
  import { KnowledgeStore } from './knowledge-store.ts';
7
6
  import { readKnowledgeSnapshot, writeKnowledgeSnapshot } from './knowledge-snapshot.ts';
7
+ import { loadProject } from './documents.ts';
8
+ import { relocateSource } from './source-relocation.ts';
9
+ import type { Graph } from './knowledge-model.ts';
10
+ import type { Project } from './documents.ts';
11
+ import type { SourceRelocation } from './source-relocation.ts';
12
+
13
+ const sourceVersion = function sourceVersion([document, version]: [string, string]) {
14
+ return { document, version };
15
+ };
8
16
 
9
- function sourceVersions(project: Project, graph: Graph) {
17
+ const warningScopes = function warningScopes(warning: Graph['warnings'][number]) {
18
+ return typeof warning === 'string' ? [] : warning.scope;
19
+ };
20
+
21
+ const sourceVersions = function sourceVersions(project: Project, graph: Graph) {
10
22
  const references = [
11
- ...Object.entries(graph.documents).map(([document, version]) => ({ document, version })),
23
+ ...Object.entries(graph.documents).map(sourceVersion),
12
24
  ...Object.values(graph.units),
13
25
  ...graph.decisions,
14
26
  ...graph.relationships.flatMap((edge) => edge.evidence),
27
+ ...graph.warnings.flatMap(warningScopes),
15
28
  ];
16
29
  const current = new Set<string>();
17
30
  const stale = new Set<string>();
18
31
  const unavailable = new Set<string>();
19
32
  for (const reference of references) {
20
33
  const source = project.documents.find((document) => document.id === reference.document);
21
- if (!source) unavailable.add(reference.document);
22
- else if (source.hash !== reference.version) stale.add(reference.document);
23
- else current.add(reference.document);
34
+ if (source === undefined) {
35
+ unavailable.add(reference.document);
36
+ } else if (source.hash === reference.version) {
37
+ current.add(reference.document);
38
+ } else {
39
+ stale.add(reference.document);
40
+ }
24
41
  }
25
42
  return {
26
- current: [...current].filter((id) => !stale.has(id)).sort(),
27
- stale: [...stale].sort(),
28
- unavailable: [...unavailable].sort(),
43
+ current: [...current].filter((id) => !stale.has(id)).toSorted(compareSerializedStrings),
44
+ stale: [...stale].toSorted(compareSerializedStrings),
45
+ unavailable: [...unavailable].toSorted(compareSerializedStrings),
29
46
  };
30
- }
47
+ };
31
48
 
32
- function snapshotReport(project: Project, graph: Graph, operation: string) {
49
+ const snapshotReport = function snapshotReport(project: Project, graph: Graph, operation: string) {
33
50
  const plan = ingestionUnits(project.currentDocuments);
34
- const pending = plan.units.filter(
35
- (unit) =>
36
- graph.units[unit.id]?.version !==
37
- project.documents.find((document) => document.id === unit.document)?.hash,
38
- );
51
+ const pending = plan.units.filter((unit) => {
52
+ const source = project.documents.find((document) => document.id === unit.document);
53
+ return graph.units[unit.id]?.version !== source?.hash;
54
+ });
39
55
  const sources = sourceVersions(project, graph);
40
56
  const warnings = [...graph.warnings, ...project.warnings, ...plan.warnings];
41
- const partial =
42
- pending.length ||
43
- warnings.length ||
44
- sources.stale.length ||
45
- sources.unavailable.length ||
46
- [...graph.decisions, ...graph.relationships].some((entry) => entry.quality !== 'checked');
57
+ const isPartial = [
58
+ pending.length > 0,
59
+ warnings.length > 0,
60
+ sources.stale.length > 0,
61
+ sources.unavailable.length > 0,
62
+ [...graph.decisions, ...graph.relationships].some((entry) => entry.quality !== 'checked'),
63
+ ].includes(true);
47
64
  return {
48
65
  command: 'snapshot',
49
- operation,
66
+ decisions: graph.decisions.length,
50
67
  modelCalls: 0,
68
+ operation,
51
69
  path: '.hivex/graph.json',
52
- status: partial ? 'partial' : 'ready',
53
- decisions: graph.decisions.length,
70
+ pendingUnits: pending.map((unit) => unit.id),
54
71
  relationships: graph.relationships.length,
55
72
  sources,
56
- pendingUnits: pending.map((unit) => unit.id),
73
+ status: isPartial ? 'partial' : 'ready',
57
74
  warnings,
58
75
  };
59
- }
76
+ };
60
77
 
61
- export function snapshotCommand(args: string[]) {
78
+ const relocationReport = function relocationReport(project: Project, relocation: SourceRelocation) {
79
+ return {
80
+ ...snapshotReport(project, relocation.graph, 'relocate'),
81
+ from: { document: relocation.from, versions: relocation.fromVersions },
82
+ reused: relocation.reused,
83
+ to: { document: relocation.to, version: relocation.destinationVersion },
84
+ };
85
+ };
86
+
87
+ export const snapshotCommand = function snapshotCommand(argumentsList: string[]) {
62
88
  const { positionals, values } = parseArgs({
63
- args,
64
89
  allowPositionals: true,
65
- strict: true,
90
+ args: argumentsList,
66
91
  options: { root: { type: 'string' } },
92
+ strict: true,
67
93
  });
68
- const operation = positionals[1];
94
+ const [, operation, from, to] = positionals;
95
+ const isRelocate = operation === 'relocate';
96
+ const expectedPositionals = isRelocate ? 4 : 2;
97
+ const isValidOperation = operation === 'export' || operation === 'import' || isRelocate;
69
98
  if (
70
- positionals.length !== 2 ||
71
- positionals[0] !== 'snapshot' ||
72
- (operation !== 'export' && operation !== 'import')
73
- )
99
+ !isValidOperation ||
100
+ positionals.length !== expectedPositionals ||
101
+ positionals[0] !== 'snapshot'
102
+ ) {
74
103
  throw new HivexError({
75
104
  code: 'INVALID_ARGUMENT',
76
- message: 'Use snapshot export | import [--root <project>]',
105
+ message: 'Use snapshot export | import | relocate <from> <to> [--root <project>]',
77
106
  });
107
+ }
78
108
  const project = loadProject(values.root ?? process.cwd());
109
+ if (isRelocate) {
110
+ using store = new KnowledgeStore(project.root, { update: true });
111
+ const relocation = relocateSource(store.graph(), project, from ?? '', to ?? '');
112
+ store.importGraph(relocation.graph);
113
+ return relocationReport(project, relocation);
114
+ }
79
115
  const incoming = operation === 'import' ? readKnowledgeSnapshot(project.root) : null;
80
- if (operation === 'import' && !incoming)
116
+ if (operation === 'import' && incoming === null) {
81
117
  throw new HivexError({
82
118
  code: 'SNAPSHOT_NOT_FOUND',
83
119
  message: 'No .hivex/graph.json snapshot is available.',
84
120
  });
85
- using store = new KnowledgeStore(project.root);
86
- using _lease = store.updateLease();
121
+ }
122
+ using store = new KnowledgeStore(project.root, { update: true });
87
123
  const graph = incoming ?? store.graph();
88
- if (incoming) store.importGraph(incoming);
89
- else writeKnowledgeSnapshot(project.root, graph);
124
+ if (incoming === null) {
125
+ writeKnowledgeSnapshot(project.root, graph);
126
+ } else {
127
+ store.importGraph(incoming);
128
+ }
90
129
  return snapshotReport(project, graph, operation);
91
- }
130
+ };