@h1v35/hivex 0.2.1 → 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 +10 -2
- package/docs/CONTEXT.md +2 -0
- package/docs/README.md +2 -1
- package/docs/adr/0011-shared-knowledge-and-selective-history.md +4 -0
- package/docs/{engineering.md → guidelines/engineering.md} +3 -7
- package/docs/procedures/self-hosted-runner.md +7 -0
- package/package.json +3 -2
- package/skills/hivex/SKILL.md +4 -0
- package/skills/hivex/references/markdown.md +1 -1
- package/src/cli.ts +1 -1
- package/src/snapshot-command.ts +30 -5
- package/src/source-relocation.ts +222 -0
package/README.md
CHANGED
|
@@ -109,10 +109,18 @@ bun hivex snapshot export --root /path/to/project
|
|
|
109
109
|
bun hivex snapshot import --root /path/to/project
|
|
110
110
|
```
|
|
111
111
|
|
|
112
|
-
`snapshot export` writes `.hivex/graph.json` atomically as stable, readable JSON. Commit that file alongside the Markdown it describes to share decisions, relationships, source versions, evidence, available provenance and coverage. It exports the graph, not work records, process identities, budgets or cached model answers.
|
|
112
|
+
`snapshot export` writes `.hivex/graph.json` atomically as stable, readable JSON. Commit that file alongside the Markdown it describes to share decisions, relationships, source versions, evidence, available provenance and coverage. It exports the graph, not work records, process identities, budgets or cached model answers. Snapshot operations make zero model calls.
|
|
113
113
|
|
|
114
114
|
A fresh clone can use `search`, `neighbors` and `status` directly from the shared snapshot without creating a local database. Its first update reuses matching ingestion units and starts local work accounting. If a local graph already exists, it takes precedence: use `snapshot import` to adopt a new shared version. Import refuses while local work is unfinished and never resets attempts or budgets. Complete or recover that work through its normal lifecycle first.
|
|
115
115
|
|
|
116
|
+
When Markdown moves, explicitly relocate its knowledge before the next update:
|
|
117
|
+
|
|
118
|
+
```sh
|
|
119
|
+
bun hivex snapshot relocate docs/old-guide.md docs/guidelines/guide.md --root /path/to/project
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
The old source must no longer be selected, and the destination must be selected current Markdown. Relocation preserves IDs, relationships, source versions and uncertainty, leaving existing work, cached answers and budgets intact. It makes zero model calls and refuses unfinished local work. An identical move to a destination without prior knowledge reuses ingestion coverage when all retained source evidence has matching, known versions. If content changed, source versions are mixed or missing, or the destination already had knowledge, its coverage becomes pending so the usual update/check can validate the result. Mismatched evidence remains stale until then. Keep the relocation report with the change and export the final graph; do not use relocation to hide unrelated missing evidence.
|
|
123
|
+
|
|
116
124
|
The snapshot response identifies current, stale and unavailable source versions, pending units and warnings. A changed or absent source is not silently current; matching sources remain reusable. Partial and uncertain knowledge can be shared with those states retained. Freshness is not proof that a model interpretation is correct: the cited Markdown remains authority.
|
|
117
125
|
|
|
118
126
|
Keep only the shared graph under version control, for example:
|
|
@@ -139,7 +147,7 @@ bun run format:check
|
|
|
139
147
|
bun run test
|
|
140
148
|
```
|
|
141
149
|
|
|
142
|
-
Tests use the public CLI and a simulated native transport. Real Luna evaluations are bounded and reported separately; simulated token usage is not a consumption measurement. Development is issue-first, with coherent PRs, independent Standards/Spec review and CI on the final commit. See the [engineering workflow](docs/engineering.md).
|
|
150
|
+
Tests use the public CLI and a simulated native transport. Real Luna evaluations are bounded and reported separately; simulated token usage is not a consumption measurement. Development is issue-first, with coherent PRs, independent Standards/Spec review and CI on the final commit. See the [engineering workflow](docs/guidelines/engineering.md).
|
|
143
151
|
|
|
144
152
|
Earlier candidate/fidelity/comparison/admission protocols and their tests are retired from the active CLI. Their code remains in Git history and historical evidence keeps its original results. They do not impose a requirement to reproduce an Opus graph or exhaustively replay an old gold suite.
|
|
145
153
|
|
package/docs/CONTEXT.md
CHANGED
|
@@ -34,6 +34,8 @@ Hivex supplies project knowledge to the agents responsible for implementation an
|
|
|
34
34
|
|
|
35
35
|
**Interpretation repair**: Replacing a wrong derived interpretation by comparing it with unchanged Markdown. It preserves source authority and does not resolve a genuine policy conflict by itself.
|
|
36
36
|
|
|
37
|
+
**Source relocation**: Moving existing knowledge to a document's new location, retaining its decisions and relationships while distinguishing reusable evidence from content that needs a new check.
|
|
38
|
+
|
|
37
39
|
**Context**: The decisions, related evidence and remaining uncertainties relevant to a particular task.
|
|
38
40
|
|
|
39
41
|
**Implementation version**: The captured change against a particular base together with the exact contents of the affected files. Later code changes are outside that review.
|
package/docs/README.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# Documentation map
|
|
2
2
|
|
|
3
3
|
- [Domain language](CONTEXT.md): documents, decisions, relationships, evidence and bounded work.
|
|
4
|
-
- [Engineering
|
|
4
|
+
- [Engineering guidelines](guidelines/engineering.md): development, verification and knowledge maintenance.
|
|
5
|
+
- [Runner procedure](procedures/self-hosted-runner.md): install and move the repository's CI runner.
|
|
5
6
|
- [Practical knowledge assistance](adr/0010-practical-knowledge-assistance.md): the current approved contract, staged delivery, autonomy, semantic relationships, uncertainty and cost.
|
|
6
7
|
- [Shared knowledge and selective history](adr/0011-shared-knowledge-and-selective-history.md): accepted extension for compact ADRs, bounded historical retrieval and a Git-versioned knowledge snapshot.
|
|
7
8
|
- [Recommended Markdown convention](../skills/hivex/references/markdown.md): optional organization and writing practices for any adopting project.
|
|
@@ -25,3 +25,7 @@ Use deterministic structural and source-version checks to distinguish reusable k
|
|
|
25
25
|
This extends ADR 0010's portable second-brain contract and supersedes a blanket recommendation to ignore every artifact under `.hivex/`. The CLI supports explicit snapshot export/import and reuses the snapshot in a fresh clone. Configurable `history` globs keep historical sources available for focused retrieval while excluding them from ordinary ingestion. A saved snapshot alone does not complete a project's rollout; publication and process retirement follow the project's normal workflow.
|
|
26
26
|
|
|
27
27
|
Delivery is tracked by [#53](https://github.com/H1V35/hivex/issues/53) (shared graph snapshot) and [#54](https://github.com/H1V35/hivex/issues/54) (selective historical sources), under [the approved extension to #17](https://github.com/H1V35/hivex/issues/17#issuecomment-5631587186). The CLI remains the existing behavioral test boundary for both independent deliveries.
|
|
28
|
+
|
|
29
|
+
## Source relocation — 2026-09-13 (#64)
|
|
30
|
+
|
|
31
|
+
An explicit source relocation keeps knowledge usable when documentation changes location. It preserves decision and relationship identities, source versions, uncertainty and existing work history. An identical move can reuse ingestion coverage without a model call. Changed content or consolidation requires the normal update/check at the destination; relocation does not approve that interpretation or make mismatched evidence current. Refuse relocation while local work is unfinished, preserve original work and model answers, and record the source/destination and versions in the operation report. Commit the exported graph with the corresponding source changes so their history remains auditable.
|
|
@@ -19,13 +19,9 @@ Independent code reviewers use the coordinating agent's current model and reason
|
|
|
19
19
|
|
|
20
20
|
Choose verification for the affected surfaces. Code changes require typechecking, lint, formatting and relevant behavior tests; documentation-only changes need formatting and checks of affected references or declared sources. Record the exact revision and the checks actually completed. A later change invalidates the affected results. Do not claim an omitted, interrupted or truncated check passed.
|
|
21
21
|
|
|
22
|
-
GitHub Actions runs the quality workflow on the owner's Mac through an official self-hosted runner, using `[self-hosted, macOS, ARM64, hivex]`. GitHub retains secrets, logs and PR checks; no hosted runner fallback is configured. No speed benchmark is required. A queued, skipped or interrupted run is not a pass. Local verification remains required when the runner is unavailable.
|
|
22
|
+
GitHub Actions runs the quality workflow on the owner's Mac through an official self-hosted runner, using `[self-hosted, macOS, ARM64, hivex]`. GitHub retains secrets, logs and PR checks; no hosted runner fallback is configured. No speed benchmark is required. A queued, skipped or interrupted run is not a pass. Local verification remains required when the runner is unavailable. Installation and host migration follow the [runner procedure](../procedures/self-hosted-runner.md).
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
During a host migration, disable Actions, register and confirm the runner is online, and merge all workflow routes before re-enabling Actions. Then dispatch Quality on that exact revision and check the assigned runner and completed result. The route change alone is not functional verification.
|
|
27
|
-
|
|
28
|
-
Use `bun install` for development and `bun ci` for frozen installation. Bun owns dependency installation through its native configuration and lockfile; Hivex has no custom installer or registry verifier. See the [installation decision](adr/0003-independent-bun-installation.md).
|
|
24
|
+
Use `bun install` for development and `bun ci` for frozen installation. Bun owns dependency installation through its native configuration and lockfile; Hivex has no custom installer or registry verifier. See the [installation decision](../adr/0003-independent-bun-installation.md).
|
|
29
25
|
|
|
30
26
|
Lint uses the published Ultracite ESLint/Prettier core presets with the owner's selected preferences: maximum cyclomatic complexity 20, cognitive complexity 15, nesting depth 3 and four parameters per function; print width 100 and single quotes; unused variables, arguments and caught errors may have a leading `_`, and loose equality is allowed when comparing with `null`. Keep the rest of the presets unchanged. Naming and unused-variable checks allow the leading `_` for unused bindings. The overlapping SonarJS unused-variable rule, `no-eq-null` and optional-catch-binding preference are disabled because they cannot express those exceptions; the configured unused-variable and equality rules still check ordinary names and comparisons. Use `eslint-plugin-boundaries` to enforce actual module responsibilities and keep additional policy minimal and tied to domain behavior. Configuration and dependency versions belong in the executable project configuration. Refactor around meaningful responsibilities instead of adding wrappers to satisfy arbitrary thresholds.
|
|
31
27
|
|
|
@@ -61,7 +57,7 @@ Keep docs with the monorepo, workspace or module they describe. Link to common r
|
|
|
61
57
|
|
|
62
58
|
Use repository decisions and review evidence for durable knowledge, not private agent memory. Checkpoints identify the exact commit, verified work and remaining work. Choose a context handoff when the task needs it; Hivex does not impose the retired machinery's fixed token thresholds. Knowledge-model operations use the admitted Luna/max profile and record actual usage, including failed or interrupted attempts. Deterministic retrieval and maintenance do not require a model.
|
|
63
59
|
|
|
64
|
-
The replacement workflow follows [ADR 0010](adr/0010-practical-knowledge-assistance.md). It processes bounded document batches with one additional knowledge check, keeps partial knowledge usable and preserves a work budget across phases and resumption. A consultation maintains one pending batch before answering, and a source-based repair replaces interpretations without changing doctrine. Keep check warnings scoped and public evidence limited to source coordinates, version and text. Avoid a new abstraction or protocol unless it protects a concrete requirement. The owner-authorized implementation can replace the earlier cohort/admission pipeline; its historical evidence remains unchanged.
|
|
60
|
+
The replacement workflow follows [ADR 0010](../adr/0010-practical-knowledge-assistance.md). It processes bounded document batches with one additional knowledge check, keeps partial knowledge usable and preserves a work budget across phases and resumption. A consultation maintains one pending batch before answering, and a source-based repair replaces interpretations without changing doctrine. Keep check warnings scoped and public evidence limited to source coordinates, version and text. Avoid a new abstraction or protocol unless it protects a concrete requirement. The owner-authorized implementation can replace the earlier cohort/admission pipeline; its historical evidence remains unchanged.
|
|
65
61
|
|
|
66
62
|
Hivex assists the principal reviewer with decisions, dependencies, exceptions and possible conflicts. The reviewer verifies its findings. Missing context or uncertainty limits the conclusions it affects; a definitive finding must refer to the actual document and implementation versions reviewed.
|
|
67
63
|
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Self-hosted GitHub Actions runner
|
|
2
|
+
|
|
3
|
+
Follow the [engineering guidelines](../guidelines/engineering.md#development-and-verification) for runner labels, execution boundaries and verification requirements.
|
|
4
|
+
|
|
5
|
+
Runner installation is repository administration, outside Hivex's product. Register the admitted macOS ARM64 release from GitHub's runner settings in its own directory, verify the official checksum, and use the generated `svc.sh install/start/status/stop` commands. Keep automatic updates enabled, a stable Homebrew/system PATH and the Mac awake and connected under the logged-in user. The runner work directory must be separate from the developer checkout and other repository runners. Only trusted code may run on this persistent host; review that boundary before public contributions. See [GitHub's runner reference](https://docs.github.com/en/actions/reference/runners/self-hosted-runners).
|
|
6
|
+
|
|
7
|
+
During a host migration, disable Actions, register and confirm the runner is online, and merge all workflow routes before re-enabling Actions. Then dispatch Quality on that exact revision and check the assigned runner and completed result. The route change alone is not functional verification.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@h1v35/hivex",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Versioned project knowledge for people and agents.",
|
|
6
6
|
"scripts": {
|
|
@@ -63,7 +63,8 @@
|
|
|
63
63
|
"!src/**/*.test.ts",
|
|
64
64
|
"docs/README.md",
|
|
65
65
|
"docs/CONTEXT.md",
|
|
66
|
-
"docs/engineering.md",
|
|
66
|
+
"docs/guidelines/engineering.md",
|
|
67
|
+
"docs/procedures/self-hosted-runner.md",
|
|
67
68
|
"docs/adr/0003-independent-bun-installation.md",
|
|
68
69
|
"docs/adr/0010-practical-knowledge-assistance.md",
|
|
69
70
|
"docs/adr/0011-shared-knowledge-and-selective-history.md",
|
package/skills/hivex/SKILL.md
CHANGED
|
@@ -53,6 +53,10 @@ Repair a demonstrably wrong interpretation with `update --repair <document> --re
|
|
|
53
53
|
|
|
54
54
|
A clone without local knowledge can read the snapshot directly and reuse its matching units on the first update. Existing local knowledge takes precedence; `hivex snapshot import` explicitly adopts the shared graph and refuses if work is unfinished. Preserve that work and its accounting rather than deleting the store to force import. Stale or unavailable source versions remain explicit, and source freshness is not semantic approval. Export when knowledge changes; ordinary reads do not dirty the shared artifact.
|
|
55
55
|
|
|
56
|
+
When Markdown moves, use `hivex snapshot relocate <old-document> <new-document>` before updating knowledge. The old source must no longer be selected, and the destination must be selected current Markdown. This explicit operation preserves decision and relationship IDs and does not call the model or rewrite earlier work, answers, attempts or budgets. Unfinished work must be resolved through its normal lifecycle first.
|
|
57
|
+
|
|
58
|
+
An identical document at a previously unknown destination reuses its knowledge and ingestion coverage when retained evidence has matching, known source versions. Changed content, mixed or missing source versions, or consolidation into an existing destination leaves that destination pending for the normal update/check; it does not certify the old interpretation against new text. Retain the relocation report with the change and export the resulting graph alongside its Markdown. Do not relocate unrelated knowledge merely to hide unavailable evidence.
|
|
59
|
+
|
|
56
60
|
## Control consumption
|
|
57
61
|
|
|
58
62
|
Large Markdown is ingested in bounded rounds. Inspect pending units and coverage, and resume the same work; do not wipe the store or resend the whole corpus when only a later round remains.
|
|
@@ -18,7 +18,7 @@ A useful catalogue distinguishes:
|
|
|
18
18
|
- Procedures for concrete operational or recovery tasks.
|
|
19
19
|
- Research and evidence identified as dated support, not automatically current decisions.
|
|
20
20
|
|
|
21
|
-
For example, a project may use `docs/README.md`, `docs/CONTEXT.md`, `docs/adr/`, `docs/guidelines/`, `docs/
|
|
21
|
+
For example, a project may use `docs/README.md`, `docs/CONTEXT.md`, `docs/adr/`, `docs/guidelines/`, `docs/procedures/` and `docs/research/`. Keep standing rules and design guidance in `guidelines`, and instructions for carrying out tasks in `procedures`; avoid overlapping directories with indistinct purposes. These are examples, not required paths. Multiple bounded contexts may have their own glossary and decisions with a small context map linking them; a monorepo need not duplicate one shared product glossary.
|
|
22
22
|
|
|
23
23
|
## Write decisions for the next reader
|
|
24
24
|
|
package/src/cli.ts
CHANGED
|
@@ -67,7 +67,7 @@ const main = async (input: string[]) => {
|
|
|
67
67
|
{
|
|
68
68
|
modelCalls: 0,
|
|
69
69
|
name: 'snapshot',
|
|
70
|
-
usage: 'snapshot export | import [--root <project>]',
|
|
70
|
+
usage: 'snapshot export | import | relocate <from> <to> [--root <project>]',
|
|
71
71
|
},
|
|
72
72
|
{ modelCalls: 0, name: 'status', usage: 'status [--root <project>]' },
|
|
73
73
|
],
|
package/src/snapshot-command.ts
CHANGED
|
@@ -5,19 +5,26 @@ import { ingestionUnits } from './ingestion-units.ts';
|
|
|
5
5
|
import { KnowledgeStore } from './knowledge-store.ts';
|
|
6
6
|
import { readKnowledgeSnapshot, writeKnowledgeSnapshot } from './knowledge-snapshot.ts';
|
|
7
7
|
import { loadProject } from './documents.ts';
|
|
8
|
+
import { relocateSource } from './source-relocation.ts';
|
|
8
9
|
import type { Graph } from './knowledge-model.ts';
|
|
9
10
|
import type { Project } from './documents.ts';
|
|
11
|
+
import type { SourceRelocation } from './source-relocation.ts';
|
|
10
12
|
|
|
11
13
|
const sourceVersion = function sourceVersion([document, version]: [string, string]) {
|
|
12
14
|
return { document, version };
|
|
13
15
|
};
|
|
14
16
|
|
|
17
|
+
const warningScopes = function warningScopes(warning: Graph['warnings'][number]) {
|
|
18
|
+
return typeof warning === 'string' ? [] : warning.scope;
|
|
19
|
+
};
|
|
20
|
+
|
|
15
21
|
const sourceVersions = function sourceVersions(project: Project, graph: Graph) {
|
|
16
22
|
const references = [
|
|
17
23
|
...Object.entries(graph.documents).map(sourceVersion),
|
|
18
24
|
...Object.values(graph.units),
|
|
19
25
|
...graph.decisions,
|
|
20
26
|
...graph.relationships.flatMap((edge) => edge.evidence),
|
|
27
|
+
...graph.warnings.flatMap(warningScopes),
|
|
21
28
|
];
|
|
22
29
|
const current = new Set<string>();
|
|
23
30
|
const stale = new Set<string>();
|
|
@@ -68,6 +75,15 @@ const snapshotReport = function snapshotReport(project: Project, graph: Graph, o
|
|
|
68
75
|
};
|
|
69
76
|
};
|
|
70
77
|
|
|
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
|
+
|
|
71
87
|
export const snapshotCommand = function snapshotCommand(argumentsList: string[]) {
|
|
72
88
|
const { positionals, values } = parseArgs({
|
|
73
89
|
allowPositionals: true,
|
|
@@ -75,18 +91,27 @@ export const snapshotCommand = function snapshotCommand(argumentsList: string[])
|
|
|
75
91
|
options: { root: { type: 'string' } },
|
|
76
92
|
strict: true,
|
|
77
93
|
});
|
|
78
|
-
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;
|
|
79
98
|
if (
|
|
80
|
-
|
|
81
|
-
positionals
|
|
82
|
-
|
|
99
|
+
!isValidOperation ||
|
|
100
|
+
positionals.length !== expectedPositionals ||
|
|
101
|
+
positionals[0] !== 'snapshot'
|
|
83
102
|
) {
|
|
84
103
|
throw new HivexError({
|
|
85
104
|
code: 'INVALID_ARGUMENT',
|
|
86
|
-
message: 'Use snapshot export | import [--root <project>]',
|
|
105
|
+
message: 'Use snapshot export | import | relocate <from> <to> [--root <project>]',
|
|
87
106
|
});
|
|
88
107
|
}
|
|
89
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
|
+
}
|
|
90
115
|
const incoming = operation === 'import' ? readKnowledgeSnapshot(project.root) : null;
|
|
91
116
|
if (operation === 'import' && incoming === null) {
|
|
92
117
|
throw new HivexError({
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { compareSerializedStrings } from './ordering.ts';
|
|
2
|
+
import { HivexError } from './errors.ts';
|
|
3
|
+
import { isMarkdownPath } from './markdown.ts';
|
|
4
|
+
import type { Project } from './documents.ts';
|
|
5
|
+
import type { Graph } from './knowledge-model.ts';
|
|
6
|
+
|
|
7
|
+
const protectedParts = new Set(['', '.', '..', '.git', '.hivex', 'node_modules']);
|
|
8
|
+
|
|
9
|
+
const isPortablePath = function isPortablePath(value: string) {
|
|
10
|
+
if (!isMarkdownPath(value) || value.startsWith('/')) {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
if (value.includes('\\') || value.includes('\0')) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
return value.split('/').every((part) => !protectedParts.has(part));
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const fail = function fail(code: string, message: string): never {
|
|
20
|
+
throw new HivexError({ code, message });
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const relationshipVersions = function relationshipVersions(
|
|
24
|
+
relationship: Graph['relationships'][number],
|
|
25
|
+
document: string
|
|
26
|
+
) {
|
|
27
|
+
return relationship.evidence
|
|
28
|
+
.filter((evidence) => evidence.document === document)
|
|
29
|
+
.flatMap((evidence) => (evidence.version === undefined ? [] : [evidence.version]));
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const warningVersions = function warningVersions(
|
|
33
|
+
warning: Graph['warnings'][number],
|
|
34
|
+
document: string
|
|
35
|
+
) {
|
|
36
|
+
if (typeof warning === 'string') {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
return warning.scope.filter((scope) => scope.document === document).map((scope) => scope.version);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const sourceVersions = function sourceVersions(graph: Graph, document: string) {
|
|
43
|
+
const versions = [
|
|
44
|
+
graph.documents[document],
|
|
45
|
+
...Object.values(graph.units)
|
|
46
|
+
.filter((unit) => unit.document === document)
|
|
47
|
+
.map((unit) => unit.version),
|
|
48
|
+
...graph.decisions
|
|
49
|
+
.filter((decision) => decision.document === document)
|
|
50
|
+
.map((decision) => decision.version),
|
|
51
|
+
...graph.relationships.flatMap((relationship) => relationshipVersions(relationship, document)),
|
|
52
|
+
...graph.warnings.flatMap((warning) => warningVersions(warning, document)),
|
|
53
|
+
].filter((version): version is string => version !== undefined);
|
|
54
|
+
return [...new Set(versions)].toSorted(compareSerializedStrings);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const hasKnowledge = function hasKnowledge(graph: Graph, document: string) {
|
|
58
|
+
if (graph.documents[document] !== undefined) {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
if (Object.values(graph.units).some((unit) => unit.document === document)) {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
if (graph.decisions.some((decision) => decision.document === document)) {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
if (
|
|
68
|
+
graph.relationships.some((relationship) =>
|
|
69
|
+
relationship.evidence.some((evidence) => evidence.document === document)
|
|
70
|
+
)
|
|
71
|
+
) {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
return graph.warnings.some(
|
|
75
|
+
(warning) =>
|
|
76
|
+
typeof warning !== 'string' && warning.scope.some((scope) => scope.document === document)
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const selectedDocument = function selectedDocument(project: Project, id: string) {
|
|
81
|
+
return project.documents.find((document) => document.id === id);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const mapCitation = function mapCitation<T extends { document: string }>(
|
|
85
|
+
citation: T,
|
|
86
|
+
from: string,
|
|
87
|
+
to: string
|
|
88
|
+
) {
|
|
89
|
+
return citation.document === from ? { ...citation, document: to } : citation;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const mapUnitId = function mapUnitId(id: string, from: string, to: string) {
|
|
93
|
+
const prefix = `${from}:`;
|
|
94
|
+
return id.startsWith(prefix) ? `${to}${id.slice(from.length)}` : id;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const mapWarning = function mapWarning(
|
|
98
|
+
warning: Graph['warnings'][number],
|
|
99
|
+
from: string,
|
|
100
|
+
to: string
|
|
101
|
+
): Graph['warnings'][number] {
|
|
102
|
+
return typeof warning === 'string'
|
|
103
|
+
? warning
|
|
104
|
+
: { ...warning, scope: warning.scope.map((scope) => mapCitation(scope, from, to)) };
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const mapDecision = function mapDecision(
|
|
108
|
+
decision: Graph['decisions'][number],
|
|
109
|
+
from: string,
|
|
110
|
+
to: string
|
|
111
|
+
) {
|
|
112
|
+
return { ...decision, document: decision.document === from ? to : decision.document };
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const mapRelationship = function mapRelationship(
|
|
116
|
+
relationship: Graph['relationships'][number],
|
|
117
|
+
from: string,
|
|
118
|
+
to: string
|
|
119
|
+
) {
|
|
120
|
+
return {
|
|
121
|
+
...relationship,
|
|
122
|
+
evidence: relationship.evidence.map((evidence) => mapCitation(evidence, from, to)),
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const mapUnits = function mapUnits(
|
|
127
|
+
graph: Graph,
|
|
128
|
+
from: string,
|
|
129
|
+
to: string,
|
|
130
|
+
isCoverageRelocated: boolean
|
|
131
|
+
) {
|
|
132
|
+
const entries = Object.entries(graph.units).flatMap(([id, unit]) => {
|
|
133
|
+
if (!isCoverageRelocated && (unit.document === from || unit.document === to)) {
|
|
134
|
+
return [];
|
|
135
|
+
}
|
|
136
|
+
if (unit.document !== from) {
|
|
137
|
+
return [[id, unit] as const];
|
|
138
|
+
}
|
|
139
|
+
const relocatedId = mapUnitId(id, from, to);
|
|
140
|
+
return [[relocatedId, { ...unit, document: to }] as const];
|
|
141
|
+
});
|
|
142
|
+
return Object.fromEntries(entries);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const mapDocuments = function mapDocuments(
|
|
146
|
+
graph: Graph,
|
|
147
|
+
from: string,
|
|
148
|
+
to: string,
|
|
149
|
+
options: { isCoverageRelocated: boolean; sourceVersion: string | undefined }
|
|
150
|
+
) {
|
|
151
|
+
const entries = Object.entries(graph.documents).filter(([id]) => id !== from && id !== to);
|
|
152
|
+
if (options.isCoverageRelocated && options.sourceVersion !== undefined) {
|
|
153
|
+
entries.push([to, options.sourceVersion]);
|
|
154
|
+
}
|
|
155
|
+
return Object.fromEntries(entries);
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
export interface SourceRelocation {
|
|
159
|
+
destinationVersion: string;
|
|
160
|
+
from: string;
|
|
161
|
+
fromVersions: string[];
|
|
162
|
+
graph: Graph;
|
|
163
|
+
reused: boolean;
|
|
164
|
+
to: string;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export const relocateSource = function relocateSource(
|
|
168
|
+
graph: Graph,
|
|
169
|
+
project: Project,
|
|
170
|
+
from: string,
|
|
171
|
+
to: string
|
|
172
|
+
): SourceRelocation {
|
|
173
|
+
if (from === to || !isPortablePath(from) || !isPortablePath(to)) {
|
|
174
|
+
fail(
|
|
175
|
+
'INVALID_ARGUMENT',
|
|
176
|
+
'Source relocation paths must be distinct project-local Markdown files'
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
if (!hasKnowledge(graph, from)) {
|
|
180
|
+
fail('SOURCE_NOT_FOUND', `Source is not present in knowledge: ${from}`);
|
|
181
|
+
}
|
|
182
|
+
if (selectedDocument(project, from) !== undefined) {
|
|
183
|
+
fail('INVALID_ARGUMENT', `Source must no longer be selected: ${from}`);
|
|
184
|
+
}
|
|
185
|
+
const destination = project.currentDocuments.find((document) => document.id === to);
|
|
186
|
+
if (destination === undefined) {
|
|
187
|
+
return fail('SOURCE_NOT_FOUND', `Destination is not a selected current Markdown source: ${to}`);
|
|
188
|
+
}
|
|
189
|
+
const versions = sourceVersions(graph, from);
|
|
190
|
+
const hasDestinationKnowledge = hasKnowledge(graph, to);
|
|
191
|
+
const hasUnversionedEvidence = graph.relationships
|
|
192
|
+
.flatMap((relationship) => relationship.evidence)
|
|
193
|
+
.some((evidence) => evidence.document === from && evidence.version === undefined);
|
|
194
|
+
const isReused =
|
|
195
|
+
!hasDestinationKnowledge &&
|
|
196
|
+
!hasUnversionedEvidence &&
|
|
197
|
+
versions.length > 0 &&
|
|
198
|
+
versions.every((version) => version === destination.hash);
|
|
199
|
+
const documents = mapDocuments(graph, from, to, {
|
|
200
|
+
isCoverageRelocated: isReused,
|
|
201
|
+
sourceVersion: graph.documents[from],
|
|
202
|
+
});
|
|
203
|
+
const units = mapUnits(graph, from, to, isReused);
|
|
204
|
+
const relocated: Graph = {
|
|
205
|
+
...graph,
|
|
206
|
+
decisions: graph.decisions.map((decision) => mapDecision(decision, from, to)),
|
|
207
|
+
documents,
|
|
208
|
+
relationships: graph.relationships.map((relationship) =>
|
|
209
|
+
mapRelationship(relationship, from, to)
|
|
210
|
+
),
|
|
211
|
+
units,
|
|
212
|
+
warnings: graph.warnings.map((warning) => mapWarning(warning, from, to)),
|
|
213
|
+
};
|
|
214
|
+
return {
|
|
215
|
+
destinationVersion: destination.hash,
|
|
216
|
+
from,
|
|
217
|
+
fromVersions: versions,
|
|
218
|
+
graph: relocated,
|
|
219
|
+
reused: isReused,
|
|
220
|
+
to,
|
|
221
|
+
};
|
|
222
|
+
};
|