@hyperscale0/udl 1.0.0-alpha.1

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 (97) hide show
  1. package/AUTHORS +8 -0
  2. package/CHANGELOG.md +36 -0
  3. package/LICENSE +661 -0
  4. package/LICENSING.md +51 -0
  5. package/README.md +163 -0
  6. package/SECURITY.md +42 -0
  7. package/TRADEMARKS.md +35 -0
  8. package/conformance/README.md +79 -0
  9. package/conformance/invalid/blank-title.expected.json +10 -0
  10. package/conformance/invalid/blank-title.udl +50 -0
  11. package/conformance/invalid/depth-budget.expected.json +10 -0
  12. package/conformance/invalid/depth-budget.udl +49 -0
  13. package/conformance/invalid/format-version.expected.json +10 -0
  14. package/conformance/invalid/format-version.udl +50 -0
  15. package/conformance/invalid/invalid-utf8.expected.json +10 -0
  16. package/conformance/invalid/invalid-utf8.udl +1 -0
  17. package/conformance/invalid/malformed-json.expected.json +10 -0
  18. package/conformance/invalid/malformed-json.udl +1 -0
  19. package/conformance/invalid/missing-create-verb.expected.json +10 -0
  20. package/conformance/invalid/missing-create-verb.udl +45 -0
  21. package/conformance/invalid/not-an-object.expected.json +10 -0
  22. package/conformance/invalid/not-an-object.udl +1 -0
  23. package/conformance/invalid/noun-id-not-snake-case.expected.json +10 -0
  24. package/conformance/invalid/noun-id-not-snake-case.udl +50 -0
  25. package/conformance/invalid/unknown-key.expected.json +10 -0
  26. package/conformance/invalid/unknown-key.udl +51 -0
  27. package/conformance/invalid/unreachable-state.expected.json +10 -0
  28. package/conformance/invalid/unreachable-state.udl +51 -0
  29. package/conformance/invalid/verb-without-transition.expected.json +10 -0
  30. package/conformance/invalid/verb-without-transition.udl +55 -0
  31. package/conformance/valid/cards.expected.json +5 -0
  32. package/conformance/valid/cards.udl +1092 -0
  33. package/conformance/valid/commerce-escrow.expected.json +5 -0
  34. package/conformance/valid/commerce-escrow.udl +1011 -0
  35. package/conformance/valid/hand-edited.expected.json +5 -0
  36. package/conformance/valid/hand-edited.udl +1 -0
  37. package/conformance/valid/insured-car-marketplace.expected.json +5 -0
  38. package/conformance/valid/insured-car-marketplace.udl +831 -0
  39. package/conformance/valid/insured-travel.expected.json +5 -0
  40. package/conformance/valid/insured-travel.udl +2621 -0
  41. package/conformance/valid/minimal.expected.json +5 -0
  42. package/conformance/valid/minimal.udl +50 -0
  43. package/conformance/valid/protection.expected.json +5 -0
  44. package/conformance/valid/protection.udl +1136 -0
  45. package/dist/canonical.d.ts +2 -0
  46. package/dist/canonical.d.ts.map +1 -0
  47. package/dist/canonical.js +35 -0
  48. package/dist/canonical.js.map +1 -0
  49. package/dist/cli.d.ts +3 -0
  50. package/dist/cli.d.ts.map +1 -0
  51. package/dist/cli.js +138 -0
  52. package/dist/cli.js.map +1 -0
  53. package/dist/duration.d.ts +3 -0
  54. package/dist/duration.d.ts.map +1 -0
  55. package/dist/duration.js +23 -0
  56. package/dist/duration.js.map +1 -0
  57. package/dist/evolution.d.ts +63 -0
  58. package/dist/evolution.d.ts.map +1 -0
  59. package/dist/evolution.js +345 -0
  60. package/dist/evolution.js.map +1 -0
  61. package/dist/finance.d.ts +53 -0
  62. package/dist/finance.d.ts.map +1 -0
  63. package/dist/finance.js +511 -0
  64. package/dist/finance.js.map +1 -0
  65. package/dist/index.d.ts +11 -0
  66. package/dist/index.d.ts.map +1 -0
  67. package/dist/index.js +8 -0
  68. package/dist/index.js.map +1 -0
  69. package/dist/limits.d.ts +33 -0
  70. package/dist/limits.d.ts.map +1 -0
  71. package/dist/limits.js +33 -0
  72. package/dist/limits.js.map +1 -0
  73. package/dist/parser.d.ts +4 -0
  74. package/dist/parser.d.ts.map +1 -0
  75. package/dist/parser.js +54 -0
  76. package/dist/parser.js.map +1 -0
  77. package/dist/schema.d.ts +757 -0
  78. package/dist/schema.d.ts.map +1 -0
  79. package/dist/schema.js +281 -0
  80. package/dist/schema.js.map +1 -0
  81. package/dist/validation.d.ts +64 -0
  82. package/dist/validation.d.ts.map +1 -0
  83. package/dist/validation.js +1323 -0
  84. package/dist/validation.js.map +1 -0
  85. package/package.json +84 -0
  86. package/spec/README.md +137 -0
  87. package/spec/udl.schema.json +1019 -0
  88. package/src/canonical.ts +41 -0
  89. package/src/cli.ts +148 -0
  90. package/src/duration.ts +26 -0
  91. package/src/evolution.ts +587 -0
  92. package/src/finance.ts +775 -0
  93. package/src/index.ts +59 -0
  94. package/src/limits.ts +32 -0
  95. package/src/parser.ts +61 -0
  96. package/src/schema.ts +339 -0
  97. package/src/validation.ts +2053 -0
@@ -0,0 +1,41 @@
1
+ import { assertValidUdl } from "./validation.js";
2
+
3
+ const INDENT = " ";
4
+
5
+ export function serializeUdl(value: unknown): string {
6
+ const document = assertValidUdl(value);
7
+ return `${writeJson(document, 0)}\n`;
8
+ }
9
+
10
+ function writeJson(value: unknown, depth: number): string {
11
+ if (value === null || typeof value !== "object") {
12
+ const encoded = JSON.stringify(value);
13
+ if (encoded === undefined) {
14
+ throw new TypeError("UDL canonical JSON received a non-JSON value");
15
+ }
16
+ return encoded;
17
+ }
18
+
19
+ const indent = INDENT.repeat(depth);
20
+ const childIndent = INDENT.repeat(depth + 1);
21
+ if (Array.isArray(value)) {
22
+ if (value.length === 0) return "[]";
23
+ return `[\n${value
24
+ .map((item) => `${childIndent}${writeJson(item, depth + 1)}`)
25
+ .join(",\n")}\n${indent}]`;
26
+ }
27
+
28
+ const record = value as Record<string, unknown>;
29
+ const keys = Object.keys(record).sort(compareCodeUnits);
30
+ if (keys.length === 0) return "{}";
31
+ return `{\n${keys
32
+ .map(
33
+ (key) =>
34
+ `${childIndent}${JSON.stringify(key)}: ${writeJson(record[key], depth + 1)}`,
35
+ )
36
+ .join(",\n")}\n${indent}}`;
37
+ }
38
+
39
+ function compareCodeUnits(left: string, right: string): number {
40
+ return left < right ? -1 : left > right ? 1 : 0;
41
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,148 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * The `udl` command. Three verbs over the same library the compiler uses:
4
+ * `validate` answers whether a document is admissible, `fmt` writes the
5
+ * canonical bytes, and `diff` answers whether a change is legal against a
6
+ * document that already has live instances.
7
+ */
8
+ import { readFile, writeFile } from "node:fs/promises";
9
+
10
+ import {
11
+ canonicalizeUdl,
12
+ diffUdlEvolution,
13
+ parseUdl,
14
+ UdlError,
15
+ type UdlDocument,
16
+ } from "./index.js";
17
+
18
+ const USAGE = `udl - the Universal Domain Language toolchain
19
+
20
+ usage:
21
+ udl validate <file> parse <file> and report every issue found
22
+ udl fmt <file> [--write] print the canonical form, or rewrite the file
23
+ udl diff <live> <next> check <next> against the append-only law
24
+ udl help print this
25
+
26
+ exit codes:
27
+ 0 the document is admissible, or the change is additive
28
+ 1 the document was refused, or the change breaks the append-only law
29
+ 2 the command line was wrong, or a file could not be read`;
30
+
31
+ async function main(argv: readonly string[]): Promise<number> {
32
+ const [command, ...rest] = argv;
33
+ switch (command) {
34
+ case "help":
35
+ case "-h":
36
+ case "--help":
37
+ console.log(USAGE);
38
+ return 0;
39
+ case "validate":
40
+ return validateCommand(rest);
41
+ case "fmt":
42
+ return formatCommand(rest);
43
+ case "diff":
44
+ return diffCommand(rest);
45
+ case undefined:
46
+ return usageError("udl: no command given");
47
+ default:
48
+ return usageError(`udl: unknown command ${command}`);
49
+ }
50
+ }
51
+
52
+ async function validateCommand(args: readonly string[]): Promise<number> {
53
+ const [file, ...extra] = args;
54
+ if (file === undefined || extra.length > 0) {
55
+ return usageError("usage: udl validate <file>");
56
+ }
57
+ const bytes = await readDocument(file);
58
+ if (bytes === null) return 2;
59
+ if (parseDocument(file, bytes) === null) return 1;
60
+ console.log(`${file}: ok`);
61
+ return 0;
62
+ }
63
+
64
+ async function formatCommand(args: readonly string[]): Promise<number> {
65
+ const write = args.includes("--write");
66
+ const [file, ...extra] = args.filter((argument) => argument !== "--write");
67
+ if (file === undefined || extra.length > 0) {
68
+ return usageError("usage: udl fmt <file> [--write]");
69
+ }
70
+ const bytes = await readDocument(file);
71
+ if (bytes === null) return 2;
72
+
73
+ let canonical: string;
74
+ try {
75
+ canonical = canonicalizeUdl(bytes);
76
+ } catch (error) {
77
+ return reportIssues(file, error);
78
+ }
79
+
80
+ if (!write) {
81
+ process.stdout.write(canonical);
82
+ return 0;
83
+ }
84
+ // The bytes decoded cleanly already: canonicalizeUdl refuses anything else.
85
+ if (new TextDecoder().decode(bytes) === canonical) return 0;
86
+ await writeFile(file, canonical);
87
+ console.log(`${file}: formatted`);
88
+ return 0;
89
+ }
90
+
91
+ async function diffCommand(args: readonly string[]): Promise<number> {
92
+ const [livePath, nextPath, ...extra] = args;
93
+ if (livePath === undefined || nextPath === undefined || extra.length > 0) {
94
+ return usageError("usage: udl diff <live> <next>");
95
+ }
96
+ const liveBytes = await readDocument(livePath);
97
+ const nextBytes = await readDocument(nextPath);
98
+ if (liveBytes === null || nextBytes === null) return 2;
99
+
100
+ const live = parseDocument(livePath, liveBytes);
101
+ const next = parseDocument(nextPath, nextBytes);
102
+ if (live === null || next === null) return 1;
103
+
104
+ const violations = diffUdlEvolution(live, next);
105
+ if (violations.length === 0) {
106
+ console.log(`${nextPath}: additive`);
107
+ return 0;
108
+ }
109
+ for (const violation of violations)
110
+ console.error(`${nextPath}: ${violation}`);
111
+ return 1;
112
+ }
113
+
114
+ async function readDocument(file: string): Promise<Uint8Array | null> {
115
+ try {
116
+ return await readFile(file);
117
+ } catch (error) {
118
+ const reason = error instanceof Error ? error.message : String(error);
119
+ console.error(`udl: cannot read ${file}: ${reason}`);
120
+ return null;
121
+ }
122
+ }
123
+
124
+ /** Parses one document, printing its issues. Null means the document was refused. */
125
+ function parseDocument(file: string, bytes: Uint8Array): UdlDocument | null {
126
+ try {
127
+ return parseUdl(bytes);
128
+ } catch (error) {
129
+ reportIssues(file, error);
130
+ return null;
131
+ }
132
+ }
133
+
134
+ function reportIssues(file: string, error: unknown): number {
135
+ if (!(error instanceof UdlError)) throw error;
136
+ for (const issue of error.issues) {
137
+ console.error(`${file}: ${issue.code} ${issue.path}: ${issue.message}`);
138
+ }
139
+ return 1;
140
+ }
141
+
142
+ function usageError(message: string): number {
143
+ console.error(message);
144
+ console.error(USAGE);
145
+ return 2;
146
+ }
147
+
148
+ process.exitCode = await main(process.argv.slice(2));
@@ -0,0 +1,26 @@
1
+ const fixedIsoDurationPattern =
2
+ /^P(?!$)(\d{1,7}W)?(\d{1,7}D)?(T(?!$)(\d{1,7}H)?(\d{1,7}M)?(\d{1,7}S)?)?$/;
3
+
4
+ // Four-digit ISO date-time anchors plus this bound stay inside both the JS Date
5
+ // range and PostgreSQL's timestamp range. The parser is the admission owner;
6
+ // downstream runtimes never approximate or clamp an accepted duration.
7
+ const MAX_FIXED_DURATION_MS = 8_000_000_000_000_000;
8
+
9
+ /** Milliseconds in a fixed ISO-8601 duration, or null for invalid/calendar units. */
10
+ export function fixedIsoDurationMs(duration: string): number | null {
11
+ const match = fixedIsoDurationPattern.exec(duration);
12
+ if (!match) return null;
13
+ const [, weeks, days, , hours, minutes, seconds] = match;
14
+ const integer = (part: string | undefined): number =>
15
+ part ? Number.parseInt(part, 10) : 0;
16
+ const milliseconds =
17
+ integer(weeks) * 604_800_000 +
18
+ integer(days) * 86_400_000 +
19
+ integer(hours) * 3_600_000 +
20
+ integer(minutes) * 60_000 +
21
+ integer(seconds) * 1_000;
22
+ return Number.isSafeInteger(milliseconds) &&
23
+ milliseconds <= MAX_FIXED_DURATION_MS
24
+ ? milliseconds
25
+ : null;
26
+ }