llmnav 0.6.6 → 0.7.0

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/CHANGELOG.md CHANGED
@@ -6,6 +6,13 @@ The npm package follows Semantic Versioning. The `llmnav/N` source protocol is v
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.7.0] — 2026-08-13
10
+
11
+ ### Added
12
+
13
+ * Added `llmnav migrate --check/--write` and `migrateProject` to report generated-format compatibility and apply full transactional upgrades from canonical source.
14
+ * Added migration regression coverage for read-only planning, idempotent writes, validation blocking, and transaction rollback.
15
+
9
16
  ## [0.6.6] — 2026-08-12
10
17
 
11
18
  ### Added
package/README.md CHANGED
@@ -179,6 +179,7 @@ Line-comment cards require an explicit terminator and work with `//`, `#`, and `
179
179
  | `llmnav context` | Build a bounded context bundle around one ID |
180
180
  | `llmnav eval` | Run repository-specific search regression queries |
181
181
  | `llmnav doctor` | Verify installation, cache integrity, transaction recovery, and drift |
182
+ | `llmnav migrate` | Check or transactionally apply generated-format upgrades from canonical source |
182
183
  | `llmnav spec` | Print source-spec vocabularies and key order |
183
184
  | `llmnav tools` | Print stable provider-neutral agent tool schemas |
184
185
  | `llmnav bundle` | Inspect the generated prompt-prefix cache partitions |
@@ -291,7 +292,7 @@ The project uses the Node.js standard library and built-in test runner. There is
291
292
 
292
293
  ## Status
293
294
 
294
- LLMNav is an experimental protocol and a usable v0.6 CLI. The source format remains `llmnav/1`; npm package changes and source-grammar changes are versioned independently.
295
+ LLMNav is an experimental protocol and a usable v0.7 CLI. The source format remains `llmnav/1`; npm package changes and source-grammar changes are versioned independently.
295
296
 
296
297
  ## License
297
298
 
package/ROADMAP.md CHANGED
@@ -73,12 +73,23 @@ Implemented:
73
73
  * path-specific coverage-rule suggestions without automatic source annotation
74
74
  * audit guidance in generated agent instructions and package scripts
75
75
 
76
+ ## 0.7 — generated-format migration
77
+
78
+ Implemented:
79
+
80
+ * read-only compatibility reports for the primary index and generated accelerators
81
+ * full migration plans reconstructed from canonical repository source
82
+ * explicit transactional writes with staging verification, rollback, and interrupted-process recovery
83
+ * validation blocking before mutation and idempotent repeated writes
84
+ * public CLI, ESM API, TypeScript declarations, and migration documentation
85
+
76
86
  ## 1.0 criteria
77
87
 
78
88
  The source grammar and generated formats will be declared stable only after use across multiple TypeScript, Go, Rust, Python, and mixed-language repositories. A 1.0 release requires migration tooling, documented compatibility guarantees, benchmark fixtures with published methodology, sustained Windows and Linux verification, and no unresolved high-severity parser or transaction ambiguity.
79
89
 
80
90
  In progress:
81
91
 
92
+ * Generated-format migration tooling is implemented for the current public formats; compatibility guarantees still need a documented support and deprecation policy.
82
93
  * A read-only cross-repository conformance matrix measures validation, retrieval, audit, repeatability, and cache freshness without averaging weak repositories away.
83
94
  * LLMNav, Workduck, Sairon, and AI BOM Generator currently pass repository-isolated conformance checks across JavaScript, TypeScript, Rust, Go, and Python.
84
95
  * The current evidence covers 4 repositories and 5 required languages with no held or failed repository, while 1.0 still requires sustained Windows and Linux verification and published benchmark methodology.
package/docs/api.md CHANGED
@@ -229,6 +229,7 @@ The package exports versioned generated-format constants:
229
229
  import {
230
230
  CONTRACT_FINGERPRINT_SCHEMA_VERSION,
231
231
  FILE_STATE_SCHEMA_VERSION,
232
+ MIGRATION_REPORT_SCHEMA_VERSION,
232
233
  SEARCH_INDEX_ENCODING,
233
234
  SEARCH_INDEX_SCHEMA_VERSION,
234
235
  SEARCH_SHARD_ENCODING,
@@ -240,6 +241,8 @@ import {
240
241
  } from "llmnav";
241
242
  ```
242
243
 
244
+ `migrateProject(root, { write: false })` returns a schemaVersion 1 compatibility report and a full-regeneration plan without committing generated artifacts. With `{ write: true }`, it applies a required upgrade through the same generation lock and recoverable transaction used by `generateProject`. Source validation errors stop the operation before mutation, and successful repeated writes are no-ops.
245
+
243
246
  `diagnosticsToSarif(diagnostics)` maps existing diagnostics to a deterministic SARIF 2.1.0 object without discovering or mutating diagnostics.
244
247
 
245
248
  `diagnosticsToEditor(diagnostics)` groups repository-relative diagnostics into schemaVersion 1 documents with zero-based ranges and stable severity mappings. `renderEditorDiagnostics` serializes the report deterministically. `getEditorIntegration("vscode")` returns a VS Code task and custom problem matcher without modifying editor files.
package/docs/cli.md CHANGED
@@ -212,6 +212,19 @@ Checks:
212
212
 
213
213
  `doctor` may perform transaction recovery, but it does not regenerate stale cache content.
214
214
 
215
+ ## `llmnav migrate`
216
+
217
+ ```sh
218
+ llmnav migrate [--check] [--json]
219
+ llmnav migrate --write [--json]
220
+ ```
221
+
222
+ The default and `--check` modes inspect the primary index, search accelerator, file state, graph, graph state, prompt-prefix bundle, and manifest against the formats supported by the installed LLMNav version. They also perform a full source reconstruction in check mode. No generated artifacts are committed. Exit status 1 means migration is required or source validation blocks a safe migration.
223
+
224
+ `--write` first builds the same plan from canonical source. If source validation succeeds and migration is required, it performs a full regeneration through the normal generation lock, staging verification, directory transaction, rollback, and interrupted-process recovery boundaries. It never rewrites source cards or reassigns semantic IDs. Repeating `--write` after a successful migration makes no changes and exits successfully.
225
+
226
+ `--check` and `--write` are mutually exclusive. Review JSON `formats`, `changedFiles`, and `diagnostics` before applying an upgrade in automation.
227
+
215
228
  ## `llmnav spec`
216
229
 
217
230
  ```sh
package/docs/migration.md CHANGED
@@ -1,23 +1,24 @@
1
1
  # Gradual migration
2
2
 
3
- ## Upgrade to 0.5
3
+ ## Upgrade to 0.7
4
4
 
5
5
  No source-card migration is required. Keep every `llmnav/1` comment and the existing `.llmnav/cache/index.json` consumer contract.
6
6
 
7
7
  Upgrade and regenerate:
8
8
 
9
9
  ```sh
10
- npm install --save-dev llmnav@^0.6.0
10
+ npm install --save-dev llmnav@^0.7.0
11
11
  npx llmnav init --agents all
12
- npx llmnav generate
12
+ npx llmnav migrate --check
13
+ npx llmnav migrate --write
13
14
  npx llmnav doctor
14
15
  ```
15
16
 
16
- The regeneration preserves the schemaVersion 1 primary index and existing graph artifacts, then adds deterministic `prompt-prefix.json`. Initialization refreshes the managed agent protocol so structured-tool hosts learn the trusted-root boundary. Volatile `state/`, `.transactions/`, and `generation-transaction.json` remain ignored.
17
+ The migration planner preserves the `llmnav/1` source comments and schemaVersion 1 primary-index contract. It reports incompatible or missing generated formats before applying a full transactional regeneration from canonical source. Initialization refreshes the managed agent protocol so structured-tool hosts learn the trusted-root boundary. Volatile `state/`, `.transactions/`, and `generation-transaction.json` remain ignored.
17
18
 
18
- Review `generate --json` during the first upgrade. Existing cards are reported as added only when no previous compatible primary index exists. The prompt-prefix artifact appears in affected catalogs when its bytes change. `LNV009` remains a non-failing contract-fingerprint review signal.
19
+ Review `migrate --check --json` during the first upgrade. Exit status 1 means generated formats need migration or source validation prevents a safe write. `migrate --write` stages and verifies a complete replacement before swapping the cache, and a repeated write is a no-op. Existing cards are reported as added only when no previous compatible primary index exists. `LNV009` remains a non-failing contract-fingerprint review signal.
19
20
 
20
- Do not delete `index.json`, rewrite semantic IDs, or copy generated paths or graph edges into comments. v0.5 tool schemas, prompt partitions, editor diagnostics, and host examples are additive; no source-card migration is required.
21
+ Do not delete `index.json`, rewrite semantic IDs, or copy generated paths or graph edges into comments. The migration command never edits source cards or the semantic ID registry; it upgrades disposable generated formats from their canonical inputs.
21
22
 
22
23
  ## Do not annotate the whole repository
23
24
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "llmnav",
3
- "version": "0.6.6",
3
+ "version": "0.7.0",
4
4
  "description": "A deterministic semantic navigation layer for LLM coding agents.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -39,6 +39,7 @@ import { getAgentToolDefinitions } from "./agent-protocol.js";
39
39
  import { loadPromptPrefixBundle } from "./prompt-bundle.js";
40
40
  import { diagnosticsToEditor, getEditorIntegration } from "./editor.js";
41
41
  import { AUDIT_PRIORITIES, auditHasFindings, auditProject } from "./audit.js";
42
+ import { migrateProject } from "./migration.js";
42
43
 
43
44
  const VALUE_OPTIONS = new Set(["--root", "--format", "--top", "--depth", "--budget", "--max-edges", "--agents", "--file", "--fail-on", "--output"]);
44
45
 
@@ -53,6 +54,7 @@ const COMMAND_OPTIONS = Object.freeze({
53
54
  context: new Set(["--depth", "--budget", "--max-edges", "--root", "--json"]),
54
55
  eval: new Set(["--file", "--top", "--root", "--json"]),
55
56
  doctor: new Set(["--root", "--json"]),
57
+ migrate: new Set(["--check", "--write", "--root", "--json"]),
56
58
  audit: new Set(["--root", "--json", "--summary", "--fail-on", "--output"]),
57
59
  spec: new Set(["--root", "--json"]),
58
60
  tools: new Set(["--json"]),
@@ -100,6 +102,8 @@ export async function runCli(argv) {
100
102
  return runEval(root, args, json);
101
103
  case "doctor":
102
104
  return runDoctor(root, json);
105
+ case "migrate":
106
+ return runMigrate(root, args, json);
103
107
  case "audit":
104
108
  return runAudit(root, args, json);
105
109
  case "spec":
@@ -295,6 +299,26 @@ async function runDoctor(root, json) {
295
299
  return result.ok ? 0 : 1;
296
300
  }
297
301
 
302
+ async function runMigrate(root, args, json) {
303
+ if (hasFlag(args, "--check") && hasFlag(args, "--write")) {
304
+ throw usageError("migrate accepts either --check or --write, not both.");
305
+ }
306
+ const result = await migrateProject(root, { write: hasFlag(args, "--write") });
307
+ if (json) {
308
+ console.log(JSON.stringify(result, null, 2));
309
+ } else {
310
+ for (const format of result.formats) {
311
+ console.log(`${format.status} ${format.id}: ${format.path}${format.detail ? ` (${format.detail})` : ""}`);
312
+ }
313
+ if (result.diagnostics.length > 0) printDiagnostics(result.diagnostics, "text");
314
+ if (result.ok && result.applied) console.log(`Migrated ${result.changedFiles.length} generated file(s) transactionally.`);
315
+ else if (result.ok) console.log("Generated LLMNav formats are current.");
316
+ else if (result.required && result.mode === "check") console.log("Migration is required. Run llmnav migrate --write after reviewing this plan.");
317
+ else if (result.required) console.error("Migration was not applied because source validation failed.");
318
+ }
319
+ return result.ok ? 0 : 1;
320
+ }
321
+
298
322
  async function runAudit(root, args, json) {
299
323
  const failOn = getOption(args, "--fail-on") ?? "none";
300
324
  if (failOn !== "none" && !AUDIT_PRIORITIES.includes(failOn)) {
@@ -492,6 +516,7 @@ Usage
492
516
  llmnav context <semantic-id> [--depth 1] [--budget 2500] [--max-edges 24]
493
517
  llmnav eval [--file path] [--top 5]
494
518
  llmnav doctor
519
+ llmnav migrate [--check|--write]
495
520
  llmnav audit [--summary] [--output path] [--fail-on none|high|medium|low]
496
521
  llmnav spec
497
522
  llmnav tools [--json]
package/src/index.d.ts CHANGED
@@ -561,6 +561,27 @@ export interface GenerationResult {
561
561
  transaction: TransactionResult;
562
562
  }
563
563
 
564
+ export interface MigrationFormatRecord {
565
+ id: string;
566
+ path: string;
567
+ status: "current" | "missing" | "incompatible";
568
+ expected: string;
569
+ detail: string | null;
570
+ }
571
+
572
+ export interface MigrationResult {
573
+ schemaVersion: 1;
574
+ ok: boolean;
575
+ mode: "check" | "write";
576
+ required: boolean;
577
+ applied: boolean;
578
+ changedFiles: string[];
579
+ formats: MigrationFormatRecord[];
580
+ previousFormats?: MigrationFormatRecord[];
581
+ diagnostics: Diagnostic[];
582
+ transaction: TransactionResult;
583
+ }
584
+
564
585
  export interface EvaluationResult {
565
586
  ok: boolean;
566
587
  errors: string[];
@@ -588,6 +609,7 @@ export const AGENT_OPERATION_SCHEMA_VERSION: 1;
588
609
  export const AGENT_TOOL_SCHEMA_VERSION: 1;
589
610
  export const BOUNDARY_KINDS: readonly DetectedBoundary["kind"][];
590
611
  export const GRAPH_INPUT_SCHEMA_VERSION: 1;
612
+ export const MIGRATION_REPORT_SCHEMA_VERSION: 1;
591
613
  export const EDITOR_DIAGNOSTIC_SCHEMA_VERSION: 1;
592
614
  export const EDITOR_INTEGRATION_SCHEMA_VERSION: 1;
593
615
  export const GRAPH_SCHEMA_VERSION: 1;
@@ -663,6 +685,7 @@ export function auditHasFindings(result: AuditResult, minimumPriority?: AuditPri
663
685
  export function findAttachedDeclaration(source: string, block: LlmnavBlock, filePath: string): Declaration | null;
664
686
  export function extractImports(source: string, filePath: string): string[];
665
687
  export function doctorProject(root: string): Promise<{ ok: boolean; checks: Array<{ name: string; ok: boolean; message: string }> }>;
688
+ export function migrateProject(root: string, options?: { write?: boolean; failpoint?: string; renameOptions?: Record<string, unknown>; lockOptions?: Record<string, unknown>; onTransactionPhase?: (phase: string) => void | Promise<void> }): Promise<MigrationResult>;
666
689
  export function evaluateProject(root: string, options?: { top?: number; file?: string }): Promise<EvaluationResult>;
667
690
  export function collectSourceFiles(root: string, config: LlmnavConfig, requestedPaths?: string[]): Promise<string[]>;
668
691
  export function findProjectRoot(start?: string): Promise<string>;
package/src/index.js CHANGED
@@ -49,6 +49,7 @@ export {
49
49
  } from "./contracts.js";
50
50
  export { findAttachedDeclaration, extractImports } from "./declaration.js";
51
51
  export { doctorProject } from "./doctor.js";
52
+ export { MIGRATION_REPORT_SCHEMA_VERSION, migrateProject } from "./migration.js";
52
53
  export { evaluateProject } from "./evaluation.js";
53
54
  export { collectSourceFiles, findProjectRoot } from "./files.js";
54
55
  export { formatProject } from "./formatter.js";
@@ -0,0 +1,194 @@
1
+ /* llmnav/1 module
2
+ id=llmnav.project.migrate
3
+ role=Plan and apply safe generated-format upgrades from canonical repository source.
4
+ owns=migration planning|format compatibility reporting|full transactional regeneration
5
+ excludes=source-card rewriting|semantic ID reassignment
6
+ search=llmnav migrate|cache format upgrade|migration dry run
7
+ invariant=Check mode never commits generated artifacts.
8
+ invariant=Write mode stops before mutation when canonical source validation fails.
9
+ rel=workflow>llmnav.index.generate
10
+ rel=workflow>llmnav.index.transaction
11
+ stability=contract
12
+ */
13
+
14
+ import path from "node:path";
15
+ import { loadConfig } from "./config.js";
16
+ import { generateProject } from "./generator.js";
17
+ import { compatibleGraphState, GRAPH_SCHEMA_VERSION, GRAPH_STATE_SCHEMA_VERSION, isCompatibleRepositoryGraph } from "./graph.js";
18
+ import { FILE_STATE_SCHEMA_VERSION, SOURCE_INDEXER_VERSION, usableFileState } from "./incremental.js";
19
+ import { isCompatibleSearchIndex, SEARCH_INDEX_ENCODING, SEARCH_INDEX_SCHEMA_VERSION } from "./inverted-index.js";
20
+ import { isCompatiblePromptPrefixBundle, PROMPT_BUNDLE_SCHEMA_VERSION } from "./prompt-bundle.js";
21
+ import { SEARCH_SHARD_SCHEMA_VERSION } from "./search-shards.js";
22
+ import { assertNoSymlinkTraversal, readText, toPosix } from "./util.js";
23
+
24
+ export const MIGRATION_REPORT_SCHEMA_VERSION = 1;
25
+
26
+ export async function migrateProject(root, options = {}) {
27
+ const write = options.write === true;
28
+ const { config } = await loadConfig(root);
29
+ const before = await inspectGeneratedFormats(root, config);
30
+ const plan = await generateProject(root, {
31
+ check: true,
32
+ incremental: false,
33
+ lockOptions: options.lockOptions,
34
+ renameOptions: options.renameOptions,
35
+ });
36
+ const required = plan.changedFiles.length > 0 || before.some((format) => format.status !== "current");
37
+ const blocked = plan.counts.error > 0;
38
+
39
+ if (!write || blocked || !required) {
40
+ return {
41
+ schemaVersion: MIGRATION_REPORT_SCHEMA_VERSION,
42
+ ok: !blocked && !required,
43
+ mode: write ? "write" : "check",
44
+ required,
45
+ applied: false,
46
+ changedFiles: plan.changedFiles,
47
+ formats: before,
48
+ diagnostics: plan.diagnostics,
49
+ transaction: plan.transaction,
50
+ };
51
+ }
52
+
53
+ const applied = await generateProject(root, {
54
+ check: false,
55
+ incremental: false,
56
+ failpoint: options.failpoint,
57
+ lockOptions: options.lockOptions,
58
+ renameOptions: options.renameOptions,
59
+ onTransactionPhase: options.onTransactionPhase,
60
+ });
61
+ const after = applied.ok ? await inspectGeneratedFormats(root, config) : before;
62
+ return {
63
+ schemaVersion: MIGRATION_REPORT_SCHEMA_VERSION,
64
+ ok: applied.ok && after.every((format) => format.status === "current"),
65
+ mode: "write",
66
+ required,
67
+ applied: applied.ok,
68
+ changedFiles: plan.changedFiles,
69
+ formats: after,
70
+ previousFormats: before,
71
+ diagnostics: applied.diagnostics,
72
+ transaction: applied.transaction,
73
+ };
74
+ }
75
+
76
+ async function inspectGeneratedFormats(root, config) {
77
+ const cacheDirectory = toPosix(config.generation.cacheDirectory).replace(/\/+$/u, "");
78
+ const cacheRoot = path.join(root, cacheDirectory);
79
+ await assertNoSymlinkTraversal(root, cacheRoot, cacheDirectory);
80
+ const repositoryId = config.repositoryId;
81
+ const records = [];
82
+
83
+ const index = await inspectJson(root, path.join(cacheRoot, "index.json"), `${cacheDirectory}/index.json`);
84
+ records.push(formatRecord(
85
+ "primary-index",
86
+ `${cacheDirectory}/index.json`,
87
+ index,
88
+ (value) => value?.schemaVersion === 1 && value.repositoryId === repositoryId && Array.isArray(value.cards),
89
+ "schemaVersion 1",
90
+ ));
91
+
92
+ const search = await inspectJson(root, path.join(cacheRoot, "search-index.json"), `${cacheDirectory}/search-index.json`);
93
+ records.push(formatRecord(
94
+ "search-index",
95
+ `${cacheDirectory}/search-index.json`,
96
+ search,
97
+ (value) => isCompatibleSearchIndex(value, repositoryId),
98
+ `schemaVersion ${SEARCH_INDEX_SCHEMA_VERSION} ${SEARCH_INDEX_ENCODING}`,
99
+ ));
100
+
101
+ const fileState = await inspectJson(root, path.join(cacheRoot, "file-state.json"), `${cacheDirectory}/file-state.json`);
102
+ records.push(formatRecord(
103
+ "file-state",
104
+ `${cacheDirectory}/file-state.json`,
105
+ fileState,
106
+ usableFileState,
107
+ `schemaVersion ${FILE_STATE_SCHEMA_VERSION} indexerVersion ${SOURCE_INDEXER_VERSION}`,
108
+ ));
109
+
110
+ const graph = await inspectJson(root, path.join(cacheRoot, "graph.json"), `${cacheDirectory}/graph.json`);
111
+ records.push(formatRecord(
112
+ "repository-graph",
113
+ `${cacheDirectory}/graph.json`,
114
+ graph,
115
+ (value) => isCompatibleRepositoryGraph(value, repositoryId),
116
+ `schemaVersion ${GRAPH_SCHEMA_VERSION}`,
117
+ ));
118
+
119
+ const graphState = await inspectJson(root, path.join(cacheRoot, "graph-state.json"), `${cacheDirectory}/graph-state.json`);
120
+ records.push(formatRecord(
121
+ "graph-state",
122
+ `${cacheDirectory}/graph-state.json`,
123
+ graphState,
124
+ (value) => compatibleGraphState(value, repositoryId),
125
+ `schemaVersion ${GRAPH_STATE_SCHEMA_VERSION}`,
126
+ ));
127
+
128
+ const prompt = await inspectJson(root, path.join(cacheRoot, "prompt-prefix.json"), `${cacheDirectory}/prompt-prefix.json`);
129
+ records.push(formatRecord(
130
+ "prompt-prefix",
131
+ `${cacheDirectory}/prompt-prefix.json`,
132
+ prompt,
133
+ (value) => isCompatiblePromptPrefixBundle(value, repositoryId),
134
+ `schemaVersion ${PROMPT_BUNDLE_SCHEMA_VERSION}`,
135
+ ));
136
+
137
+ const manifest = await inspectJson(root, path.join(cacheRoot, "manifest.json"), `${cacheDirectory}/manifest.json`);
138
+ records.push(formatRecord(
139
+ "manifest",
140
+ `${cacheDirectory}/manifest.json`,
141
+ manifest,
142
+ (value) => compatibleManifest(value, repositoryId),
143
+ "schemaVersion 1 with current generated format versions",
144
+ ));
145
+
146
+ return records;
147
+ }
148
+
149
+ function compatibleManifest(value, repositoryId) {
150
+ return Boolean(
151
+ value &&
152
+ value.schemaVersion === 1 &&
153
+ value.repositoryId === repositoryId &&
154
+ value.fileStateSchemaVersion === FILE_STATE_SCHEMA_VERSION &&
155
+ value.searchIndexSchemaVersion === SEARCH_INDEX_SCHEMA_VERSION &&
156
+ value.graphSchemaVersion === GRAPH_SCHEMA_VERSION &&
157
+ value.graphStateSchemaVersion === GRAPH_STATE_SCHEMA_VERSION &&
158
+ value.promptBundleSchemaVersion === PROMPT_BUNDLE_SCHEMA_VERSION &&
159
+ (value.searchShardSchemaVersion === null || value.searchShardSchemaVersion === SEARCH_SHARD_SCHEMA_VERSION) &&
160
+ value.files && typeof value.files === "object",
161
+ );
162
+ }
163
+
164
+ async function inspectJson(root, filePath, label) {
165
+ await assertNoSymlinkTraversal(root, filePath, label);
166
+ const text = await readText(filePath, null);
167
+ if (text === null) return { state: "missing", value: null, detail: "file is missing" };
168
+ try {
169
+ return { state: "present", value: JSON.parse(text), detail: null };
170
+ } catch (error) {
171
+ return { state: "malformed", value: null, detail: error instanceof Error ? error.message : String(error) };
172
+ }
173
+ }
174
+
175
+ function formatRecord(id, relativePath, inspected, compatible, expected) {
176
+ if (inspected.state === "missing") return { id, path: relativePath, status: "missing", expected, detail: inspected.detail };
177
+ if (inspected.state === "malformed") return { id, path: relativePath, status: "incompatible", expected, detail: inspected.detail };
178
+ const current = compatible(inspected.value);
179
+ return {
180
+ id,
181
+ path: relativePath,
182
+ status: current ? "current" : "incompatible",
183
+ expected,
184
+ detail: current ? null : describeVersion(inspected.value),
185
+ };
186
+ }
187
+
188
+ function describeVersion(value) {
189
+ if (!value || typeof value !== "object") return "expected a JSON object";
190
+ const fields = ["schemaVersion", "indexerVersion", "encoding", "tokenizerVersion"]
191
+ .filter((name) => value[name] !== undefined)
192
+ .map((name) => `${name}=${JSON.stringify(value[name])}`);
193
+ return fields.length > 0 ? `found ${fields.join(" ")}` : "version fields are missing";
194
+ }
package/src/spec.js CHANGED
@@ -10,7 +10,7 @@ rel=workflow>llmnav.rules.validate
10
10
  stability=contract
11
11
  */
12
12
 
13
- export const PACKAGE_VERSION = "0.6.6";
13
+ export const PACKAGE_VERSION = "0.7.0";
14
14
  export const SPEC_VERSION = "1";
15
15
 
16
16
  export const SCOPES = Object.freeze(["file", "module", "symbol"]);