@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.
- package/README.md +55 -163
- package/docs/CONTEXT.md +20 -36
- package/docs/README.md +6 -12
- package/docs/adr/0003-independent-bun-installation.md +5 -19
- package/docs/adr/0010-practical-knowledge-assistance.md +28 -81
- package/docs/adr/0011-shared-knowledge-and-selective-history.md +16 -43
- package/docs/guidelines/engineering.md +74 -0
- package/docs/procedures/self-hosted-runner.md +7 -0
- package/package.json +32 -11
- package/skills/hivex/SKILL.md +28 -92
- package/skills/hivex/references/markdown.md +12 -42
- package/src/cli/diagnostic.ts +21 -11
- package/src/cli.ts +46 -36
- package/src/documents.ts +502 -320
- package/src/errors.ts +8 -6
- package/src/implementation.ts +185 -87
- package/src/ingestion-units.ts +107 -64
- package/src/knowledge-maintenance.ts +35 -22
- package/src/knowledge-model.ts +386 -268
- package/src/knowledge-serialization.ts +239 -0
- package/src/knowledge-snapshot.ts +100 -77
- package/src/knowledge-store.ts +634 -453
- package/src/knowledge.ts +1001 -758
- package/src/markdown.ts +107 -45
- package/src/model/connection.ts +134 -76
- package/src/model/failure.ts +46 -23
- package/src/model/invoke.ts +346 -166
- package/src/model/profile.ts +201 -103
- package/src/model/rpc-error.ts +21 -0
- package/src/model/server.ts +151 -82
- package/src/model/thread.ts +24 -14
- package/src/model/transcript.ts +87 -46
- package/src/ordering.ts +9 -0
- package/src/retrieval/lexical.ts +64 -41
- package/src/review.ts +83 -55
- package/src/runtime.d.ts +4 -0
- package/src/snapshot-command.ts +82 -43
- package/src/source-relocation.ts +222 -0
- package/docs/engineering.md +0 -174
package/src/retrieval/lexical.ts
CHANGED
|
@@ -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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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.
|
|
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
|
|
69
|
-
if (
|
|
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
|
-
|
|
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(
|
|
80
|
-
.map((
|
|
81
|
-
return {
|
|
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
|
|
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
|
|
2
|
+
import pathModule from 'node:path';
|
|
3
3
|
import { parseArgs } from 'node:util';
|
|
4
4
|
import { z } from 'zod';
|
|
5
|
-
import { loadProject
|
|
6
|
-
import { captureImplementation
|
|
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
|
-
|
|
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)
|
|
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)
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
70
|
-
documents,
|
|
79
|
+
assessment: areReferencesVerified ? finding.assessment : 'uncertain',
|
|
71
80
|
code,
|
|
72
|
-
|
|
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-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
98
|
-
const
|
|
114
|
+
const areDocumentsChanged = project.snapshot !== binding.documents;
|
|
115
|
+
const isImplementationChanged = implementation.fingerprint !== binding.implementation;
|
|
99
116
|
return {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
117
|
+
documentsChanged: areDocumentsChanged,
|
|
118
|
+
implementationChanged: isImplementationChanged,
|
|
119
|
+
status: areDocumentsChanged || isImplementationChanged ? 'stale' : 'current',
|
|
103
120
|
};
|
|
104
|
-
}
|
|
105
|
-
|
|
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 (
|
|
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
|
|
119
|
-
if (statSync(
|
|
120
|
-
throw new HivexError({
|
|
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')
|
|
123
|
-
.parse(JSON.parse(readFileSync(
|
|
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
|
+
};
|
package/src/runtime.d.ts
ADDED
package/src/snapshot-command.ts
CHANGED
|
@@ -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
|
|
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(
|
|
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 (
|
|
22
|
-
|
|
23
|
-
else
|
|
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)).
|
|
27
|
-
stale: [...stale].
|
|
28
|
-
unavailable: [...unavailable].
|
|
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
|
-
(
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
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
|
-
|
|
66
|
+
decisions: graph.decisions.length,
|
|
50
67
|
modelCalls: 0,
|
|
68
|
+
operation,
|
|
51
69
|
path: '.hivex/graph.json',
|
|
52
|
-
|
|
53
|
-
decisions: graph.decisions.length,
|
|
70
|
+
pendingUnits: pending.map((unit) => unit.id),
|
|
54
71
|
relationships: graph.relationships.length,
|
|
55
72
|
sources,
|
|
56
|
-
|
|
73
|
+
status: isPartial ? 'partial' : 'ready',
|
|
57
74
|
warnings,
|
|
58
75
|
};
|
|
59
|
-
}
|
|
76
|
+
};
|
|
60
77
|
|
|
61
|
-
|
|
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
|
-
|
|
90
|
+
args: argumentsList,
|
|
66
91
|
options: { root: { type: 'string' } },
|
|
92
|
+
strict: true,
|
|
67
93
|
});
|
|
68
|
-
const operation = positionals
|
|
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
|
-
|
|
71
|
-
positionals
|
|
72
|
-
|
|
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' &&
|
|
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
|
-
|
|
86
|
-
using
|
|
121
|
+
}
|
|
122
|
+
using store = new KnowledgeStore(project.root, { update: true });
|
|
87
123
|
const graph = incoming ?? store.graph();
|
|
88
|
-
if (incoming)
|
|
89
|
-
|
|
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
|
+
};
|