@deterministic-code/datasources-cli 0.1.13 → 0.1.15

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 (62) hide show
  1. package/dist/commands.js +9 -4
  2. package/dist/detect.js +21 -7
  3. package/dist/env.js +7 -0
  4. package/dist/generate/CSharpRepositories.js +18 -14
  5. package/dist/generate/DeterministicProvider.js +25 -16
  6. package/dist/generate/RepositoryModel.js +10 -4
  7. package/dist/generate/RepositoryPlan.js +12 -5
  8. package/dist/generate/RustRepositories.js +42 -31
  9. package/dist/generate/TypeScriptRepositories.js +22 -18
  10. package/dist/generate/applyGenerate.js +11 -2
  11. package/dist/generate/applyMigrations.js +14 -17
  12. package/dist/generate/casing.js +28 -0
  13. package/dist/generate/destEnv.js +12 -0
  14. package/dist/generate/migrateUp.js +8 -41
  15. package/dist/generate/sqlDialects.js +29 -0
  16. package/dist/generate/sqlFlags.js +27 -0
  17. package/dist/migrateInit.js +4 -3
  18. package/dist/parse.js +102 -25
  19. package/dist/run.js +42 -67
  20. package/dist/setup.js +7 -9
  21. package/dist/version/applyVersion.js +11 -14
  22. package/dist/version/delta.js +0 -50
  23. package/dist/version/render.js +1 -11
  24. package/dist/wireRegister.js +29 -11
  25. package/dist/wireRustProviders.js +42 -0
  26. package/node_modules/@deterministic-code/csharp-generators/dist/import-generator.js +16 -12
  27. package/node_modules/@deterministic-code/csharp-generators/src/import-generator.test.ts +22 -11
  28. package/node_modules/@deterministic-code/csharp-generators/src/import-generator.ts +21 -13
  29. package/node_modules/@deterministic-code/datasources-code-generators/dist/nestedSelectTypes.js +2 -6
  30. package/node_modules/@deterministic-code/datasources-code-generators/dist/sql/common/infer-reference-types.d.ts +2 -0
  31. package/node_modules/@deterministic-code/datasources-code-generators/dist/sql/common/infer-reference-types.js +37 -0
  32. package/node_modules/@deterministic-code/datasources-code-generators/dist/sql/common/reference-target.d.ts +2 -0
  33. package/node_modules/@deterministic-code/datasources-code-generators/dist/sql/common/reference-target.js +6 -0
  34. package/node_modules/@deterministic-code/datasources-code-generators/dist/sql/common/sql-schema.d.ts +1 -1
  35. package/node_modules/@deterministic-code/datasources-code-generators/dist/sql/common/sql-schema.js +3 -6
  36. package/node_modules/@deterministic-code/datasources-code-generators/dist/sql/common/version-migrations.js +25 -14
  37. package/node_modules/@deterministic-code/datasources-code-generators/package.json +10 -8
  38. package/node_modules/@deterministic-code/deterministic-generators-common/README.md +61 -1
  39. package/node_modules/@deterministic-code/deterministic-generators-common/typescript/parser/dist/specification-parser.js +128 -1
  40. package/node_modules/@deterministic-code/deterministic-generators-common/typescript/parser/specification-parser.ts +1 -0
  41. package/node_modules/@deterministic-code/deterministic-generators-common/typescript/parser/version-snapshot.ts +154 -0
  42. package/node_modules/@deterministic-code/deterministic-generators-common/typescript/parser/yaml-node.ts +2 -2
  43. package/node_modules/@deterministic-code/deterministic-specifications-typescript/README.md +61 -1
  44. package/node_modules/@deterministic-code/deterministic-specifications-typescript/typescript/parser/specification-parser.ts +1 -0
  45. package/node_modules/@deterministic-code/deterministic-specifications-typescript/typescript/parser/version-snapshot.ts +154 -0
  46. package/node_modules/@deterministic-code/deterministic-specifications-typescript/typescript/parser/yaml-node.ts +2 -2
  47. package/node_modules/@deterministic-code/rust-generators/dist/import-generator.js +25 -15
  48. package/node_modules/@deterministic-code/rust-generators/src/import-generator.test.ts +11 -0
  49. package/node_modules/@deterministic-code/rust-generators/src/import-generator.ts +36 -18
  50. package/node_modules/@deterministic-code/typescript-generators/dist/generate-frontend-types.js +6 -1
  51. package/node_modules/@deterministic-code/typescript-generators/dist/generate-frontend-validators.js +6 -1
  52. package/node_modules/@deterministic-code/typescript-generators/dist/generate-routes.js +1 -1
  53. package/node_modules/@deterministic-code/typescript-generators/dist/generate-services.js +1 -1
  54. package/node_modules/@deterministic-code/typescript-generators/dist/import-generator.js +29 -21
  55. package/node_modules/@deterministic-code/typescript-generators/src/emit.ts +2 -5
  56. package/node_modules/@deterministic-code/typescript-generators/src/generate-frontend-types.ts +7 -1
  57. package/node_modules/@deterministic-code/typescript-generators/src/generate-frontend-validators.ts +7 -1
  58. package/node_modules/@deterministic-code/typescript-generators/src/generate-routes.ts +1 -1
  59. package/node_modules/@deterministic-code/typescript-generators/src/generate-services.ts +1 -1
  60. package/node_modules/@deterministic-code/typescript-generators/src/import-generator.test.ts +16 -0
  61. package/node_modules/@deterministic-code/typescript-generators/src/import-generator.ts +37 -22
  62. package/package.json +8 -8
@@ -1,4 +1,5 @@
1
1
  import type { DatasourceFieldOverlay, DatasourceIndex, DatasourceTable, IDeterministic, Type, TypeField } from "@deterministic-code/deterministic-generators-common/parser";
2
+ export { referenceTarget } from "./reference-target.js";
2
3
  /** Flattened `datasource.*` flags. Stored procedures are opt-in `"true"`. */
3
4
  export declare const datasourceSettings: (settings: Record<string, string>) => {
4
5
  useStoredProcedures: boolean;
@@ -28,7 +29,6 @@ export type SqlFile = {
28
29
  path: string;
29
30
  content: string;
30
31
  };
31
- export declare const referenceTarget: (references: NonNullable<TypeField["references"]>) => [string, string];
32
32
  /** Attach physical names and order parent tables before children. */
33
33
  export declare const buildLiveTables: (types: SqlTable[], mapping: {
34
34
  tableName: (table: SqlTable) => string;
@@ -1,4 +1,6 @@
1
1
  import { datasourceTypesOf, dictionaryUniqueColumns, identityColumns, persistedColumnFields, tableByName, } from "@deterministic-code/generators-common/spec-types";
2
+ import { inferReferenceFieldTypes } from "./infer-reference-types.js";
3
+ export { referenceTarget } from "./reference-target.js";
2
4
  /** Flattened `datasource.*` flags. Stored procedures are opt-in `"true"`. */
3
5
  export const datasourceSettings = (settings) => ({
4
6
  useStoredProcedures: String(settings["datasource.use_stored_procedures"]) === "true",
@@ -44,6 +46,7 @@ export const isGeneratedIdentity = (table) => {
44
46
  export const usesGeneratedIdColumn = (table, field) => field.isId === true && isGeneratedIdentity(table);
45
47
  export const sqlTablesFrom = (spec) => {
46
48
  requireIdIsId(spec);
49
+ spec = { ...spec, expandedTypes: inferReferenceFieldTypes(spec.expandedTypes) };
47
50
  const overlays = tableByName(spec);
48
51
  const byName = new Map(spec.expandedTypes.map((t) => [t.name, t]));
49
52
  return datasourceTypesOf(spec).map((type) => {
@@ -80,12 +83,6 @@ const referenceParents = (references) => {
80
83
  return [references[0]];
81
84
  return [references.split(".")[0]];
82
85
  };
83
- export const referenceTarget = (references) => {
84
- if (Array.isArray(references))
85
- return [references[0], references[1]];
86
- const [table, col] = references.split(".");
87
- return [table ?? "", col ?? ""];
88
- };
89
86
  const topoSort = (tables) => {
90
87
  const names = new Set(tables.map((t) => t.name));
91
88
  const deps = new Map(tables.map((t) => [
@@ -1,8 +1,7 @@
1
- import { parse, stringify } from "yaml";
1
+ import { parse } from "yaml";
2
2
  import { CreateTableStatement } from "@deterministic-code/datasources";
3
3
  import { memoryReader } from "@deterministic-code/generators-common/deterministic-reader";
4
- import { DeterministicParser } from "@deterministic-code/deterministic-generators-common/parser";
5
- import { applyDocument, isObject, snapshotsEqual } from "./version-apply.js";
4
+ import { DeterministicParser, VersionSnapshot } from "@deterministic-code/deterministic-generators-common/parser";
6
5
  import { createCasing } from "./default-casing.js";
7
6
  import { SqlMapping } from "./sql-mapping.js";
8
7
  import { buildLiveTables, sqlTablesFrom } from "./sql-schema.js";
@@ -23,11 +22,26 @@ export const versionDiffPaths = (settings) => (settings["version.diffs"] ?? "")
23
22
  .split("\n")
24
23
  .map((path) => path.trim())
25
24
  .filter((path) => versionDiff.test(path));
26
- const asDocument = (value, source) => isObject(value) ? value : (() => { throw new Error(`${source}: expected a mapping`); })();
25
+ const own = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key);
26
+ const parseDiff = (text, source) => {
27
+ const value = parse(text);
28
+ if (value == null || typeof value !== "object" || Array.isArray(value))
29
+ throw new Error(`${source}: expected a mapping`);
30
+ return value;
31
+ };
32
+ const fold = (path, text) => VersionSnapshot.combine([{ [path]: { added: true, content: parse(text) } }]).yaml(path);
33
+ const matches = (snapshot, path, live, whenMissing) => {
34
+ const has = own(snapshot.documents, path);
35
+ const text = live === undefined || live === "" ? whenMissing : live;
36
+ if (text === undefined || text === "")
37
+ return !has;
38
+ const snapshotYaml = has ? snapshot.yaml(path) : whenMissing === undefined ? undefined : fold(path, whenMissing);
39
+ return snapshotYaml !== undefined && snapshotYaml === fold(path, text);
40
+ };
27
41
  const filesOf = (snapshot) => {
28
42
  const files = {};
29
- for (const [path, value] of Object.entries(snapshot))
30
- files[path] = stringify(value);
43
+ for (const path of Object.keys(snapshot.documents))
44
+ files[path] = snapshot.yaml(path);
31
45
  files["datasource.yaml"] ??= defaultDatasourceYaml;
32
46
  return files;
33
47
  };
@@ -37,7 +51,7 @@ const compile = async (snapshot, settings) => {
37
51
  const mapping = new SqlMapping(createCasing(settings), types);
38
52
  return { tables: buildLiveTables(types, mapping), mapping, seeds: spec.datasourceSeeds };
39
53
  };
40
- const emptySnapshot = (settings) => compile({}, settings);
54
+ const emptySnapshot = (settings) => compile(VersionSnapshot.combine([]), settings);
41
55
  const pad = (n) => String(n).padStart(4, "0");
42
56
  const renderAll = (dialect, statements) => statements
43
57
  .map((statement) => renderSql(dialect, statement))
@@ -48,22 +62,19 @@ export const versionMigrationFiles = async (dialect, ctx, casing, live) => {
48
62
  const paths = versionDiffPaths(ctx.settings);
49
63
  if (paths.length === 0)
50
64
  return { files: [], count: 0 };
51
- let before = {};
65
+ const diffs = [];
52
66
  const snapshots = [];
53
67
  for (const path of paths) {
54
68
  const text = await ctx.reader.read(path);
55
69
  if (text === "")
56
70
  continue;
57
- before = applyDocument(before, asDocument(parse(text), path));
58
- snapshots.push(before);
71
+ diffs.push(parseDiff(text, path));
72
+ snapshots.push(VersionSnapshot.combine(diffs));
59
73
  }
60
74
  if (snapshots.length === 0)
61
75
  return { files: [], count: 0 };
62
76
  const last = snapshots[snapshots.length - 1];
63
- const liveTypes = live.typesYaml === undefined ? undefined : parse(live.typesYaml);
64
- const liveDatasource = parse(live.datasourceYaml ?? defaultDatasourceYaml);
65
- const snapshotDatasource = last["datasource.yaml"] ?? parse(defaultDatasourceYaml);
66
- if (!snapshotsEqual(last["types.yaml"], liveTypes) || !snapshotsEqual(snapshotDatasource, liveDatasource)) {
77
+ if (!matches(last, "types.yaml", live.typesYaml, undefined) || !matches(last, "datasource.yaml", live.datasourceYaml, defaultDatasourceYaml)) {
67
78
  throw new Error("types.yaml or datasource.yaml differs from the frozen version diffs; run datasources version first");
68
79
  }
69
80
  const files = [];
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deterministic-code/datasources-code-generators",
3
- "version": "0.1.2",
3
+ "version": "0.1.15",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./dist/generateSql.js",
@@ -9,15 +9,17 @@
9
9
  "./generate-stored-procedures": "./dist/generate-stored-procedures.js",
10
10
  "./sql/generate-stored-procedures": "./dist/sql/generate-stored-procedures.js",
11
11
  "./common/sql-dialect": "./dist/sql/common/sql-dialect.js",
12
- "./sql/common/write-inclusion": "./dist/sql/common/write-inclusion.js"
12
+ "./sql/common/write-inclusion": "./dist/sql/common/write-inclusion.js",
13
+ "./sql/common/default-casing": "./dist/sql/common/default-casing.js",
14
+ "./sql/common/infer-reference-types": "./dist/sql/common/infer-reference-types.js"
13
15
  },
14
16
  "dependencies": {
15
- "@deterministic-code/datasources": "^0.1.12",
16
- "@deterministic-code/datasources-mysql": "^0.1.12",
17
- "@deterministic-code/datasources-oracle": "^0.1.12",
18
- "@deterministic-code/datasources-postgres": "^0.1.12",
19
- "@deterministic-code/datasources-sqlite": "^0.1.12",
20
- "@deterministic-code/datasources-sqlserver": "^0.1.12",
17
+ "@deterministic-code/datasources": "^0.1.14",
18
+ "@deterministic-code/datasources-mysql": "^0.1.14",
19
+ "@deterministic-code/datasources-oracle": "^0.1.14",
20
+ "@deterministic-code/datasources-postgres": "^0.1.14",
21
+ "@deterministic-code/datasources-sqlite": "^0.1.14",
22
+ "@deterministic-code/datasources-sqlserver": "^0.1.14",
21
23
  "@deterministic-code/deterministic-generators-common": "0.1.0",
22
24
  "@deterministic-code/generators-common": "0.0.1",
23
25
  "pluralize": "^8.0.0",
@@ -2,7 +2,7 @@
2
2
 
3
3
  Parser and validators for the [deterministic-specifications](https://github.com/deterministic-code/deterministic-specifications) contract. Each language is its own library. Specs and samples stay in the specifications repo; these libraries only read them.
4
4
 
5
- - [`typescript/`](typescript) — npm package `@deterministic-code/deterministic-generators-common` (`DeterministicParser` on the `./parser` export, validators on the package root)
5
+ - [`typescript/`](typescript) — npm package `@deterministic-code/deterministic-generators-common` (`DeterministicParser` and `VersionSnapshot` on the `./parser` export, validators on the package root)
6
6
  - [`rust/`](rust) — crate `deterministic_generators_common` (path/git dependency)
7
7
  - [`csharp/`](csharp) — `Deterministic.Generators.Common` (`net10.0` class library)
8
8
 
@@ -15,8 +15,68 @@ deterministic_generators_common = { git = "https://github.com/deterministic-code
15
15
  <ProjectReference Include="path/to/deterministic-generators-common/csharp/Deterministic.Generators.Common/Deterministic.Generators.Common.csproj" />
16
16
  ```
17
17
 
18
+ ```xml
19
+ <PackageReference Include="Deterministic.Generators.Common" Version="0.1.1" />
20
+ ```
21
+
22
+ TypeScript packs pin `#main`:
23
+
24
+ ```json
25
+ "@deterministic-code/deterministic-generators-common": "github:deterministic-code/deterministic-generators-common#main"
26
+ ```
27
+
28
+ C# first-party consumers use the `ProjectReference`. Out-of-repo C# can take the NuGet package.
29
+
30
+ ```text
31
+ npm run publish:nuget # pack and smoke-test
32
+ npm run publish:nuget -- --publish # push to nuget.org (NUGET_API_KEY)
33
+ ```
34
+
18
35
  ```text
19
36
  npm test # TypeScript parser + validator (100% coverage)
20
37
  sh rust/coverage.sh # cargo llvm-cov, 100% lines + functions
21
38
  dotnet test csharp/Deterministic.Generators.Common.slnx --settings csharp/coverlet.runsettings
22
39
  ```
40
+
41
+ ## VersionSnapshot
42
+
43
+ `VersionSnapshot` reconstructs specification YAML by folding every `deterministic/version-*/diff.yaml` in name order. Documents are the library YAML AST (`YamlValue` / `YamlMap` / `YamlSeq` / `YamlScalar`), not compiled `IDeterministic` and not `DeterministicParser` output.
44
+
45
+ | API | Role |
46
+ | --- | --- |
47
+ | `Combine` / `combine` | Fold already-parsed diff trees (mappings), in order |
48
+ | `Read` / `read` | Scan a `deterministic/` directory and fold matching version dirs |
49
+ | `Documents` / `documents` | Reconstructed file path → tree |
50
+ | `Yaml` / `yaml(path)` | Serialize one reconstructed file with the library serializer |
51
+
52
+ Apply helpers are private instance methods. Version directories must match `^version-\d{8}T\d{6}Z-\d{3}$`. Missing or empty `diff.yaml` is skipped. A non-mapping diff throws `expected a mapping`. TypeScript `read` is async; C# and Rust `Read` are sync.
53
+
54
+ ```csharp
55
+ var snapshot = VersionSnapshot.Read(deterministicRoot);
56
+ var types = snapshot.Yaml("types.yaml");
57
+ var datasource = snapshot.Yaml("datasource.yaml");
58
+ ```
59
+
60
+ ```typescript
61
+ import { VersionSnapshot } from "@deterministic-code/deterministic-generators-common/parser";
62
+
63
+ const snapshot = await VersionSnapshot.read(deterministicRoot);
64
+ const types = snapshot.yaml("types.yaml");
65
+ ```
66
+
67
+ ```rust
68
+ let snapshot = VersionSnapshot::read(deterministic_root)?;
69
+ let types = snapshot.yaml("types.yaml")?;
70
+ ```
71
+
72
+ `Combine` takes an ordered list of parsed mappings, not YAML strings. To compare a fold to a live file, wrap the live tree as `{ added: true, content }` and compare `Yaml` strings from the same serializer — do not string-compare authored YAML (comments and flow sequences differ).
73
+
74
+ Per file key in a diff:
75
+
76
+ - `{ added: true, content }` inserts `content` (`true` or the strings `"true"` / `"True"`).
77
+ - `{ removed: true }` deletes the file.
78
+ - Object merge: nested apply, `removed:` string list deletes keys, new keys are appended.
79
+ - Keyed collections (sequence of single-key maps): `{ added, changed, removed }`. Added entries are prepended.
80
+ - Otherwise the next value replaces.
81
+
82
+ `Read` does not grow `IDeterministicReader`.
@@ -1355,6 +1355,133 @@ var Parser = class {
1355
1355
  return nested;
1356
1356
  }
1357
1357
  };
1358
+
1359
+ // node_modules/@deterministic-code/deterministic-generators-common/typescript/parser/version-snapshot.ts
1360
+ import { readdir, readFile } from "node:fs/promises";
1361
+ import { join } from "node:path";
1362
+ import { parse as parse2, stringify as stringify2 } from "yaml";
1363
+ var versionDir = /^version-\d{8}T\d{6}Z-\d{3}$/;
1364
+ var own = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key);
1365
+ var VersionSnapshot = class _VersionSnapshot {
1366
+ documents;
1367
+ constructor(diffsInOrder) {
1368
+ const docs = {};
1369
+ for (const diff of diffsInOrder) {
1370
+ if (diff == null) continue;
1371
+ if (!this.isMap(diff)) throw new Error("expected a mapping");
1372
+ for (const [path, envelope] of Object.entries(diff)) {
1373
+ const next = this.applyFile(docs[path], envelope);
1374
+ if (this.isNull(next)) delete docs[path];
1375
+ else docs[path] = next;
1376
+ }
1377
+ }
1378
+ this.documents = docs;
1379
+ }
1380
+ static combine(diffsInOrder) {
1381
+ return new _VersionSnapshot(diffsInOrder);
1382
+ }
1383
+ static async read(deterministicRoot) {
1384
+ const names = (await readdir(deterministicRoot, { withFileTypes: true })).filter((entry) => entry.isDirectory() && versionDir.test(entry.name)).map((entry) => entry.name).sort();
1385
+ const snapshot = new _VersionSnapshot([]);
1386
+ return _VersionSnapshot.combine(await Promise.all(names.map((name) => snapshot.diffOf(deterministicRoot, name))));
1387
+ }
1388
+ yaml(path) {
1389
+ if (!own(this.documents, path)) throw new Error(`missing ${path}`);
1390
+ return stringify2(this.documents[path]);
1391
+ }
1392
+ async diffOf(root, name) {
1393
+ let text;
1394
+ try {
1395
+ text = await readFile(join(root, name, "diff.yaml"), "utf8");
1396
+ } catch {
1397
+ return void 0;
1398
+ }
1399
+ if (text === "") return void 0;
1400
+ let rootValue;
1401
+ try {
1402
+ rootValue = parse2(text);
1403
+ } catch (error) {
1404
+ throw new Error(`${name}/diff.yaml: ${String(error)}`);
1405
+ }
1406
+ if (!this.isMap(rootValue)) throw new Error(`${name}/diff.yaml: expected a mapping`);
1407
+ return rootValue;
1408
+ }
1409
+ applyFile(before, envelope) {
1410
+ if (this.isNull(envelope)) return before;
1411
+ if (this.isMap(envelope) && this.isTrue(envelope, "removed") && Object.keys(envelope).length === 1)
1412
+ return void 0;
1413
+ if (this.isMap(envelope) && this.isTrue(envelope, "added")) return envelope.content;
1414
+ return this.apply(before, envelope);
1415
+ }
1416
+ apply(before, next) {
1417
+ if (this.isNull(next)) return before;
1418
+ if (this.isKeyed(before) && this.isMap(next) && this.isSplit(next)) return this.applyKeyed(before, next);
1419
+ if (this.isMap(before) && this.isMap(next)) return this.applyObject(before, next);
1420
+ return next;
1421
+ }
1422
+ applyKeyed(beforeVal, split) {
1423
+ const removed = new Set(this.strings(split, "removed"));
1424
+ const changed = new Map(
1425
+ this.seq(split.changed).flatMap((entry) => {
1426
+ const pair = this.firstPair(entry);
1427
+ return pair ? [[pair.key, pair.value]] : [];
1428
+ })
1429
+ );
1430
+ return [
1431
+ ...this.seq(split.added),
1432
+ ...beforeVal.flatMap((entry) => {
1433
+ const pair = this.firstPair(entry);
1434
+ if (!pair || removed.has(pair.key)) return [];
1435
+ return [
1436
+ changed.has(pair.key) ? { [pair.key]: this.apply(pair.value, changed.get(pair.key)) } : entry
1437
+ ];
1438
+ })
1439
+ ];
1440
+ }
1441
+ applyObject(beforeVal, next) {
1442
+ const removed = new Set(this.strings(next, "removed"));
1443
+ const output = {};
1444
+ for (const [key, value] of Object.entries(beforeVal)) {
1445
+ if (removed.has(key)) continue;
1446
+ output[key] = own(next, key) ? this.apply(value, next[key]) : value;
1447
+ }
1448
+ for (const [key, value] of Object.entries(next)) {
1449
+ if (key === "removed" && Array.isArray(value)) continue;
1450
+ if (!own(beforeVal, key)) output[key] = value;
1451
+ }
1452
+ return output;
1453
+ }
1454
+ isSplit(map) {
1455
+ return Object.keys(map).every((key) => key === "added" || key === "changed" || key === "removed");
1456
+ }
1457
+ isKeyed(value) {
1458
+ return Array.isArray(value) && value.length > 0 && value.every((item) => this.isMap(item) && Object.keys(item).length === 1);
1459
+ }
1460
+ seq(value) {
1461
+ return Array.isArray(value) ? value : [];
1462
+ }
1463
+ strings(map, key) {
1464
+ return this.seq(map[key]).map((item) => this.isScalar(item) ? String(item ?? "") : "");
1465
+ }
1466
+ firstPair(entry) {
1467
+ if (!this.isMap(entry)) return void 0;
1468
+ const key = Object.keys(entry)[0];
1469
+ return key === void 0 ? void 0 : { key, value: entry[key] };
1470
+ }
1471
+ isMap(value) {
1472
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1473
+ }
1474
+ isTrue(map, key) {
1475
+ return map[key] === true || map[key] === "true" || map[key] === "True";
1476
+ }
1477
+ isNull(value) {
1478
+ return value == null;
1479
+ }
1480
+ isScalar(value) {
1481
+ return value == null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
1482
+ }
1483
+ };
1358
1484
  export {
1359
- DeterministicParser
1485
+ DeterministicParser,
1486
+ VersionSnapshot
1360
1487
  };
@@ -1,5 +1,6 @@
1
1
  export type { IDeterministic } from "./deterministic.ts";
2
2
  export { DeterministicParser } from "./parser/specification-parser.ts";
3
+ export { VersionSnapshot } from "./version-snapshot.ts";
3
4
  export type {
4
5
  CustomRouteEntry,
5
6
  CustomServiceEntry,
@@ -0,0 +1,154 @@
1
+ import { readdir, readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { parse, stringify } from "yaml";
4
+ import type { YamlMap, YamlValue } from "./yaml-node.ts";
5
+
6
+ const versionDir = /^version-\d{8}T\d{6}Z-\d{3}$/;
7
+ const own = (obj: object, key: string) => Object.prototype.hasOwnProperty.call(obj, key);
8
+
9
+ export class VersionSnapshot {
10
+ readonly documents: Readonly<Record<string, YamlValue | undefined>>;
11
+
12
+ private constructor(diffsInOrder: Array<YamlValue | null | undefined>) {
13
+ const docs: Record<string, YamlValue | undefined> = {};
14
+ for (const diff of diffsInOrder) {
15
+ if (diff == null) continue;
16
+ if (!this.isMap(diff)) throw new Error("expected a mapping");
17
+ for (const [path, envelope] of Object.entries(diff)) {
18
+ const next = this.applyFile(docs[path], envelope);
19
+ if (this.isNull(next)) delete docs[path];
20
+ else docs[path] = next;
21
+ }
22
+ }
23
+ this.documents = docs;
24
+ }
25
+
26
+ static combine(diffsInOrder: Array<YamlValue | null | undefined>): VersionSnapshot {
27
+ return new VersionSnapshot(diffsInOrder);
28
+ }
29
+
30
+ static async read(deterministicRoot: string): Promise<VersionSnapshot> {
31
+ const names = (await readdir(deterministicRoot, { withFileTypes: true }))
32
+ .filter((entry) => entry.isDirectory() && versionDir.test(entry.name))
33
+ .map((entry) => entry.name)
34
+ .sort();
35
+ const snapshot = new VersionSnapshot([]);
36
+ return VersionSnapshot.combine(await Promise.all(names.map((name) => snapshot.diffOf(deterministicRoot, name))));
37
+ }
38
+
39
+ yaml(path: string): string {
40
+ if (!own(this.documents, path)) throw new Error(`missing ${path}`);
41
+ return stringify(this.documents[path]);
42
+ }
43
+
44
+ private async diffOf(root: string, name: string): Promise<YamlValue | undefined> {
45
+ let text: string;
46
+ try {
47
+ text = await readFile(join(root, name, "diff.yaml"), "utf8");
48
+ } catch {
49
+ return undefined;
50
+ }
51
+ if (text === "") return undefined;
52
+ let rootValue: YamlValue;
53
+ try {
54
+ rootValue = parse(text) as YamlValue;
55
+ } catch (error) {
56
+ throw new Error(`${name}/diff.yaml: ${String(error)}`);
57
+ }
58
+ if (!this.isMap(rootValue)) throw new Error(`${name}/diff.yaml: expected a mapping`);
59
+ return rootValue;
60
+ }
61
+
62
+ private applyFile(before: YamlValue | undefined, envelope: YamlValue | undefined): YamlValue | undefined {
63
+ if (this.isNull(envelope)) return before;
64
+ if (this.isMap(envelope) && this.isTrue(envelope, "removed") && Object.keys(envelope).length === 1)
65
+ return undefined;
66
+ if (this.isMap(envelope) && this.isTrue(envelope, "added")) return envelope.content;
67
+ return this.apply(before, envelope);
68
+ }
69
+
70
+ private apply(before: YamlValue | undefined, next: YamlValue | undefined): YamlValue | undefined {
71
+ if (this.isNull(next)) return before;
72
+ if (this.isKeyed(before) && this.isMap(next) && this.isSplit(next)) return this.applyKeyed(before, next);
73
+ if (this.isMap(before) && this.isMap(next)) return this.applyObject(before, next);
74
+ return next;
75
+ }
76
+
77
+ private applyKeyed(beforeVal: YamlMap[], split: YamlMap): YamlValue {
78
+ const removed = new Set(this.strings(split, "removed"));
79
+ const changed = new Map(
80
+ this.seq(split.changed).flatMap((entry) => {
81
+ const pair = this.firstPair(entry);
82
+ return pair ? [[pair.key, pair.value] as const] : [];
83
+ }),
84
+ );
85
+ return [
86
+ ...this.seq(split.added),
87
+ ...beforeVal.flatMap((entry) => {
88
+ const pair = this.firstPair(entry);
89
+ if (!pair || removed.has(pair.key)) return [];
90
+ return [
91
+ changed.has(pair.key)
92
+ ? { [pair.key]: this.apply(pair.value, changed.get(pair.key)) as YamlValue }
93
+ : entry,
94
+ ];
95
+ }),
96
+ ];
97
+ }
98
+
99
+ private applyObject(beforeVal: YamlMap, next: YamlMap): YamlMap {
100
+ const removed = new Set(this.strings(next, "removed"));
101
+ const output: Record<string, YamlValue> = {};
102
+ for (const [key, value] of Object.entries(beforeVal)) {
103
+ if (removed.has(key)) continue;
104
+ output[key] = (own(next, key) ? this.apply(value, next[key]) : value) as YamlValue;
105
+ }
106
+ for (const [key, value] of Object.entries(next)) {
107
+ if (key === "removed" && Array.isArray(value)) continue;
108
+ if (!own(beforeVal, key)) output[key] = value;
109
+ }
110
+ return output;
111
+ }
112
+
113
+ private isSplit(map: YamlMap) {
114
+ return Object.keys(map).every((key) => key === "added" || key === "changed" || key === "removed");
115
+ }
116
+
117
+ private isKeyed(value: YamlValue | undefined): value is YamlMap[] {
118
+ return (
119
+ Array.isArray(value) &&
120
+ value.length > 0 &&
121
+ value.every((item) => this.isMap(item) && Object.keys(item).length === 1)
122
+ );
123
+ }
124
+
125
+ private seq(value: YamlValue | undefined): YamlValue[] {
126
+ return Array.isArray(value) ? value : [];
127
+ }
128
+
129
+ private strings(map: YamlMap, key: string) {
130
+ return this.seq(map[key]).map((item) => (this.isScalar(item) ? String(item ?? "") : ""));
131
+ }
132
+
133
+ private firstPair(entry: YamlValue | undefined) {
134
+ if (!this.isMap(entry)) return undefined;
135
+ const key = Object.keys(entry)[0];
136
+ return key === undefined ? undefined : { key, value: entry[key] };
137
+ }
138
+
139
+ private isMap(value: YamlValue | undefined | null): value is YamlMap {
140
+ return typeof value === "object" && value !== null && !Array.isArray(value);
141
+ }
142
+
143
+ private isTrue(map: YamlMap, key: string) {
144
+ return map[key] === true || map[key] === "true" || map[key] === "True";
145
+ }
146
+
147
+ private isNull(value: YamlValue | undefined | null) {
148
+ return value == null;
149
+ }
150
+
151
+ private isScalar(value: YamlValue | undefined) {
152
+ return value == null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
153
+ }
154
+ }
@@ -1,8 +1,8 @@
1
1
  import { parse } from "yaml";
2
2
 
3
3
  type YamlLiteral = string | number | boolean | null;
4
- type YamlMap = { readonly [key: string]: YamlValue };
5
- type YamlValue = YamlLiteral | YamlValue[] | YamlMap;
4
+ export type YamlMap = { readonly [key: string]: YamlValue };
5
+ export type YamlValue = YamlLiteral | YamlValue[] | YamlMap;
6
6
  type FieldDefault = YamlLiteral | YamlValue[] | YamlMap;
7
7
  type Named = { name: string; node: YamlNode };
8
8
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  Parser and validators for the [deterministic-specifications](https://github.com/deterministic-code/deterministic-specifications) contract. Each language is its own library. Specs and samples stay in the specifications repo; these libraries only read them.
4
4
 
5
- - [`typescript/`](typescript) — npm package `@deterministic-code/deterministic-generators-common` (`DeterministicParser` on the `./parser` export, validators on the package root)
5
+ - [`typescript/`](typescript) — npm package `@deterministic-code/deterministic-generators-common` (`DeterministicParser` and `VersionSnapshot` on the `./parser` export, validators on the package root)
6
6
  - [`rust/`](rust) — crate `deterministic_generators_common` (path/git dependency)
7
7
  - [`csharp/`](csharp) — `Deterministic.Generators.Common` (`net10.0` class library)
8
8
 
@@ -15,8 +15,68 @@ deterministic_generators_common = { git = "https://github.com/deterministic-code
15
15
  <ProjectReference Include="path/to/deterministic-generators-common/csharp/Deterministic.Generators.Common/Deterministic.Generators.Common.csproj" />
16
16
  ```
17
17
 
18
+ ```xml
19
+ <PackageReference Include="Deterministic.Generators.Common" Version="0.1.1" />
20
+ ```
21
+
22
+ TypeScript packs pin `#main`:
23
+
24
+ ```json
25
+ "@deterministic-code/deterministic-generators-common": "github:deterministic-code/deterministic-generators-common#main"
26
+ ```
27
+
28
+ C# first-party consumers use the `ProjectReference`. Out-of-repo C# can take the NuGet package.
29
+
30
+ ```text
31
+ npm run publish:nuget # pack and smoke-test
32
+ npm run publish:nuget -- --publish # push to nuget.org (NUGET_API_KEY)
33
+ ```
34
+
18
35
  ```text
19
36
  npm test # TypeScript parser + validator (100% coverage)
20
37
  sh rust/coverage.sh # cargo llvm-cov, 100% lines + functions
21
38
  dotnet test csharp/Deterministic.Generators.Common.slnx --settings csharp/coverlet.runsettings
22
39
  ```
40
+
41
+ ## VersionSnapshot
42
+
43
+ `VersionSnapshot` reconstructs specification YAML by folding every `deterministic/version-*/diff.yaml` in name order. Documents are the library YAML AST (`YamlValue` / `YamlMap` / `YamlSeq` / `YamlScalar`), not compiled `IDeterministic` and not `DeterministicParser` output.
44
+
45
+ | API | Role |
46
+ | --- | --- |
47
+ | `Combine` / `combine` | Fold already-parsed diff trees (mappings), in order |
48
+ | `Read` / `read` | Scan a `deterministic/` directory and fold matching version dirs |
49
+ | `Documents` / `documents` | Reconstructed file path → tree |
50
+ | `Yaml` / `yaml(path)` | Serialize one reconstructed file with the library serializer |
51
+
52
+ Apply helpers are private instance methods. Version directories must match `^version-\d{8}T\d{6}Z-\d{3}$`. Missing or empty `diff.yaml` is skipped. A non-mapping diff throws `expected a mapping`. TypeScript `read` is async; C# and Rust `Read` are sync.
53
+
54
+ ```csharp
55
+ var snapshot = VersionSnapshot.Read(deterministicRoot);
56
+ var types = snapshot.Yaml("types.yaml");
57
+ var datasource = snapshot.Yaml("datasource.yaml");
58
+ ```
59
+
60
+ ```typescript
61
+ import { VersionSnapshot } from "@deterministic-code/deterministic-generators-common/parser";
62
+
63
+ const snapshot = await VersionSnapshot.read(deterministicRoot);
64
+ const types = snapshot.yaml("types.yaml");
65
+ ```
66
+
67
+ ```rust
68
+ let snapshot = VersionSnapshot::read(deterministic_root)?;
69
+ let types = snapshot.yaml("types.yaml")?;
70
+ ```
71
+
72
+ `Combine` takes an ordered list of parsed mappings, not YAML strings. To compare a fold to a live file, wrap the live tree as `{ added: true, content }` and compare `Yaml` strings from the same serializer — do not string-compare authored YAML (comments and flow sequences differ).
73
+
74
+ Per file key in a diff:
75
+
76
+ - `{ added: true, content }` inserts `content` (`true` or the strings `"true"` / `"True"`).
77
+ - `{ removed: true }` deletes the file.
78
+ - Object merge: nested apply, `removed:` string list deletes keys, new keys are appended.
79
+ - Keyed collections (sequence of single-key maps): `{ added, changed, removed }`. Added entries are prepended.
80
+ - Otherwise the next value replaces.
81
+
82
+ `Read` does not grow `IDeterministicReader`.
@@ -1,5 +1,6 @@
1
1
  export type { IDeterministic } from "./deterministic.ts";
2
2
  export { DeterministicParser } from "./parser/specification-parser.ts";
3
+ export { VersionSnapshot } from "./version-snapshot.ts";
3
4
  export type {
4
5
  CustomRouteEntry,
5
6
  CustomServiceEntry,