@githolon/dsl 0.100.6 → 0.101.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/dart/CHANGELOG.md +6 -0
- package/dart/lib/src/dev_cloud.dart +46 -15
- package/dart/lib/src/test_harness.dart +78 -16
- package/dart/lib/src/webview_bridge.dart +640 -126
- package/dart/pubspec.lock +1 -1
- package/dart/pubspec.yaml +2 -2
- package/dart/test/bundle_version_pairing_test.dart +11 -22
- package/dart/test/co2_product_gauntlet_test.dart +228 -0
- package/dart/test/errors_test.dart +63 -12
- package/dart/test/first_app_gauntlet_test.dart +29 -3
- package/dart/test/webview_bridge_test.dart +274 -0
- package/dart_types/CHANGELOG.md +6 -0
- package/dart_types/lib/src/errors.dart +136 -30
- package/dart_types/lib/src/surfaced.dart +48 -2
- package/dart_types/lib/src/wire.dart +3 -1
- package/dart_types/pubspec.yaml +1 -1
- package/dart_types/test/surfaced_test.dart +19 -0
- package/package.json +1 -1
- package/src/activity.ts +43 -4
- package/src/aggregate.ts +132 -22
- package/src/attested_core.ts +1 -1
- package/src/authoring.ts +119 -6
- package/src/birth_meter.ts +4 -4
- package/src/birth_meter_ensure.ts +9 -1
- package/src/births.ts +53 -22
- package/src/build_package.ts +55 -14
- package/src/business_birth.ts +48 -7
- package/src/codegen_dart.ts +267 -37
- package/src/codegen_dart_client.ts +921 -82
- package/src/codegen_openapi.ts +28 -5
- package/src/codegen_proof.ts +107 -11
- package/src/codegen_ts.ts +961 -135
- package/src/compile_package_main.ts +155 -45
- package/src/conflicts.ts +252 -0
- package/src/describe_model.ts +448 -0
- package/src/directive.ts +5 -38
- package/src/drivers.ts +12 -4
- package/src/engine_entry.ts +4 -0
- package/src/evolve_preview.ts +72 -11
- package/src/fields.ts +201 -55
- package/src/framework/application_home.ts +103 -0
- package/src/framework/application_home_domain.ts +2 -0
- package/src/framework/attested_receipts.ts +2 -2
- package/src/framework/auth_provider_law.ts +51 -24
- package/src/framework/business_corrections.ts +170 -0
- package/src/framework/disclosure.ts +1 -1
- package/src/framework/domain_lifecycle.ts +250 -16
- package/src/framework/encryption_identity.ts +1 -1
- package/src/framework/encryption_scope.ts +21 -13
- package/src/framework/era_certification.ts +1 -1
- package/src/framework/framework_aggregates.ts +2 -0
- package/src/framework/genesis.ts +1 -1
- package/src/framework/home.ts +22 -9
- package/src/framework/identity.ts +5 -5
- package/src/framework/impure_capability.ts +2 -2
- package/src/framework/owned_custody.ts +561 -0
- package/src/framework/platform_admin.ts +1 -1
- package/src/framework/relations.ts +9 -1
- package/src/framework/repair.ts +1 -1
- package/src/framework/signing_identity.ts +2 -2
- package/src/framework/source_roster.ts +1 -1
- package/src/framework/workspaces.ts +17 -10
- package/src/generated_client_names.ts +39 -0
- package/src/index.ts +20 -15
- package/src/kernel_capabilities.ts +25 -1
- package/src/law_check.ts +4 -1
- package/src/manifest.ts +356 -63
- package/src/read_projection_manifest.ts +8 -0
- package/src/read_projection_namespace.ts +4 -0
- package/src/relation.ts +1 -1
- package/src/shape_compat.ts +47 -2
- package/src/stable_ids.ts +38 -6
- package/src/stable_ids_types.ts +30 -0
- package/src/surfaced_lint.ts +0 -0
- package/src/usd.ts +62 -11
- package/src/usd_layers.ts +54 -3
- package/src/value_object.ts +23 -19
- package/src/wire.ts +1 -0
- package/src/wire_encode.ts +48 -4
- package/src/workspace_routing.ts +57 -0
- package/src/workspace_sharding.ts +9 -9
- package/src/certified_read.ts +0 -313
package/src/workspace_routing.ts
CHANGED
|
@@ -486,6 +486,63 @@ export function mintedCreateField(d: AnyDirective, agg: AggregateHandle): string
|
|
|
486
486
|
return undefined;
|
|
487
487
|
}
|
|
488
488
|
|
|
489
|
+
/** The record identity an encrypted owned directive targets. Generated clients need this before they
|
|
490
|
+
* seal a field so the ciphertext scope is `<qualified-type>:<id>#<owned-reader>`. This is derived by
|
|
491
|
+
* executing the real plan with sentinels, never by guessing that a field happens to be named `id`.
|
|
492
|
+
*
|
|
493
|
+
* - `payload`: the plan addresses the record from a public business payload field;
|
|
494
|
+
* - `literal`: the plan addresses one deterministic instance;
|
|
495
|
+
* - `premint`: `create(Aggregate)` mints internally, so the client must reserve a hidden typed id that
|
|
496
|
+
* the authoring surface consumes during the real/replay plan.
|
|
497
|
+
*/
|
|
498
|
+
export type DirectiveRecordTarget =
|
|
499
|
+
| { readonly kind: "payload"; readonly field: string }
|
|
500
|
+
| { readonly kind: "literal"; readonly id: string }
|
|
501
|
+
| { readonly kind: "premint" };
|
|
502
|
+
|
|
503
|
+
export function deriveDirectiveRecordTarget(
|
|
504
|
+
d: AnyDirective,
|
|
505
|
+
agg: AggregateHandle,
|
|
506
|
+
): DirectiveRecordTarget | undefined {
|
|
507
|
+
if (d.aggregateId !== agg.id) return undefined;
|
|
508
|
+
const schema = (d as unknown as { payloadSchema?: z.ZodTypeAny }).payloadSchema;
|
|
509
|
+
if (schema === undefined || zodKind(schema) !== "object") return undefined;
|
|
510
|
+
const sentinelOf = new Map<string, string>();
|
|
511
|
+
const fieldOfSentinel = new Map<string, string>();
|
|
512
|
+
for (const [name, raw] of Object.entries(zodObjectShape(schema))) {
|
|
513
|
+
let f = raw as z.ZodTypeAny;
|
|
514
|
+
while (zodKind(f) === "optional" || zodKind(f) === "default") f = zodDef(f).innerType!;
|
|
515
|
+
if (zodKind(f) !== "string") continue;
|
|
516
|
+
const sentinel = `__NOMOS_RECORD_PROBE_${name}__`;
|
|
517
|
+
sentinelOf.set(name, sentinel);
|
|
518
|
+
fieldOfSentinel.set(sentinel, name);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const classify = (events: readonly TracedEvent[]): DirectiveRecordTarget | undefined => {
|
|
522
|
+
const targets = events.filter((event) => event.type === agg.id || (event.type === undefined && event.aggregate === agg.id));
|
|
523
|
+
if (targets.length !== 1) return undefined;
|
|
524
|
+
const target = targets[0]!.aggregate;
|
|
525
|
+
const payloadField = fieldOfSentinel.get(target);
|
|
526
|
+
if (payloadField !== undefined) return { kind: "payload", field: payloadField };
|
|
527
|
+
if (target.startsWith(`${agg.id}_00000000-0000-7000-8000-`)) return { kind: "premint" };
|
|
528
|
+
return { kind: "literal", id: target };
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
const traced = tracePlan(d, agg, sentinelOf);
|
|
532
|
+
const direct = traced === undefined ? undefined : classify(traced);
|
|
533
|
+
if (direct !== undefined) return direct;
|
|
534
|
+
|
|
535
|
+
// Plans that JSON.parse another string field may reject the all-sentinel probe. Retry one possible
|
|
536
|
+
// identity field at a time with every sibling string JSON-safe, matching the routing derivation.
|
|
537
|
+
for (const [name, sentinel] of sentinelOf) {
|
|
538
|
+
const one = tracePlan(d, agg, new Map([[name, sentinel]]), true);
|
|
539
|
+
if (one === undefined) continue;
|
|
540
|
+
const classified = classify(one);
|
|
541
|
+
if (classified?.kind === "payload" && classified.field === name) return classified;
|
|
542
|
+
}
|
|
543
|
+
return undefined;
|
|
544
|
+
}
|
|
545
|
+
|
|
489
546
|
/** One generated-client mint instruction (the TS front-door, taxonomy packages only). */
|
|
490
547
|
export interface ClientMintPlanEntry {
|
|
491
548
|
/** The directive id. */
|
|
@@ -211,7 +211,7 @@ export const NomosShardAssignment = aggregate("NomosShardAssignment", {
|
|
|
211
211
|
mapVersion: t.int().merge(Lww),
|
|
212
212
|
status: t.string().merge(Lww), // active | moving | moved (slice 5 flips these)
|
|
213
213
|
placedAt: t.string().merge(Lww),
|
|
214
|
-
});
|
|
214
|
+
}).public();
|
|
215
215
|
|
|
216
216
|
/** The home's CURRENT assignment row, keyed by home key. Top-level so the §6 move directives can
|
|
217
217
|
* CAPTURED-READ it (P6 S3a — the map version is derived in law, not by the host). Also routed by
|
|
@@ -235,7 +235,7 @@ export const NomosShardIdentity = aggregate("NomosShardIdentity", {
|
|
|
235
235
|
label: t.string().merge(Lww), // "s0"…, or "" = the coordinator
|
|
236
236
|
coordinator: t.string().merge(Lww), // the coordinator workspace's name
|
|
237
237
|
declaredAt: t.string().merge(Lww),
|
|
238
|
-
});
|
|
238
|
+
}).public();
|
|
239
239
|
|
|
240
240
|
/**
|
|
241
241
|
* THE ASSIGNMENT RECEIPT (slice 3 — §3's receipt leg): the shard's own chain records
|
|
@@ -251,14 +251,14 @@ export const NomosHomeReceipt = aggregate("NomosHomeReceipt", {
|
|
|
251
251
|
mapVersion: t.int().merge(Lww),
|
|
252
252
|
status: t.string().merge(Lww), // active | moved (slice 5 — the tombstone IS a redirect: `shard` then names the target)
|
|
253
253
|
receivedAt: t.string().merge(Lww),
|
|
254
|
-
});
|
|
254
|
+
}).public();
|
|
255
255
|
|
|
256
256
|
/** The OPEN-SHARD registry row (coordinator law-state; `s0…s{pool-1}` are implicit). */
|
|
257
257
|
export const NomosShardRegistry = aggregate("NomosShardRegistry", {
|
|
258
258
|
label: t.string().merge(Lww),
|
|
259
259
|
status: t.string().merge(Lww), // open | retired (slice 5)
|
|
260
260
|
openedAt: t.string().merge(Lww),
|
|
261
|
-
});
|
|
261
|
+
}).public();
|
|
262
262
|
|
|
263
263
|
/**
|
|
264
264
|
* THE SPLIT POLICY singleton (coordinator law-state — ruling 5: 75% ships, the dial
|
|
@@ -274,7 +274,7 @@ export const NomosShardPolicy = aggregate("NomosShardPolicy", {
|
|
|
274
274
|
// Absent ⇒ the host default (64); 0 ⇒ cadence off (explicit seals only).
|
|
275
275
|
checkpointEveryDeltas: t.int().merge(Lww),
|
|
276
276
|
setAt: t.string().merge(Lww),
|
|
277
|
-
});
|
|
277
|
+
}).public();
|
|
278
278
|
|
|
279
279
|
/**
|
|
280
280
|
* THE CHECKPOINT SEAL (§5.5, slice 4 — ruling 1 made periodic): a committed
|
|
@@ -294,7 +294,7 @@ export const NomosCheckpointSeal = aggregate("NomosCheckpointSeal", {
|
|
|
294
294
|
frontiers: t.string().merge(Lww), // canonical JSON: [{shard, frontier}] sorted by shard
|
|
295
295
|
stateHash: t.string().merge(Lww), // sha256 over the coordinator's subtotal state at the seal
|
|
296
296
|
sealedAt: t.string().merge(Lww),
|
|
297
|
-
});
|
|
297
|
+
}).public();
|
|
298
298
|
|
|
299
299
|
/** The canonical (shard-sorted) frontier-set bytes a seal folds — ONE shape on every peer. */
|
|
300
300
|
export const canonicalSealFrontiers = (
|
|
@@ -330,7 +330,7 @@ export const NomosSummarySubtotal = aggregate("NomosSummarySubtotal", {
|
|
|
330
330
|
value: t.int().merge(Lww),
|
|
331
331
|
frontier: t.string().merge(Lww),
|
|
332
332
|
empty: t.int().merge(Lww).optional(),
|
|
333
|
-
});
|
|
333
|
+
}).public();
|
|
334
334
|
|
|
335
335
|
/** Per-(shard) committed subtotal rows, keyed by shard. Top-level so the derived placement directive can
|
|
336
336
|
* CAPTURED-READ each open shard's load (the `nomosShardRows` subtotal) to pick LEAST-LOADED IN THE PLAN
|
|
@@ -344,7 +344,7 @@ export const NomosSummaryFrontier = aggregate("NomosSummaryFrontier", {
|
|
|
344
344
|
shard: t.string().merge(Lww),
|
|
345
345
|
frontier: t.string().merge(Lww), // the shard main oid the subtotals are true at
|
|
346
346
|
updatedAt: t.string().merge(Lww),
|
|
347
|
-
});
|
|
347
|
+
}).public();
|
|
348
348
|
|
|
349
349
|
/** THE RECORDED DEEP-VERIFY VERDICT (§5.5 — replay-on-suspicion, made operational). */
|
|
350
350
|
export const NomosDeepVerify = aggregate("NomosDeepVerify", {
|
|
@@ -354,7 +354,7 @@ export const NomosDeepVerify = aggregate("NomosDeepVerify", {
|
|
|
354
354
|
verdict: t.string().merge(Lww), // verified | mismatch:<…> | invalid:<…>
|
|
355
355
|
detail: t.string().merge(Lww), // JSON: the per-(read,group) byte-compare table
|
|
356
356
|
checkedAt: t.string().merge(Lww),
|
|
357
|
-
});
|
|
357
|
+
}).public();
|
|
358
358
|
|
|
359
359
|
export const nomosDeclareShardIdentity = directive("nomosDeclareShardIdentity")
|
|
360
360
|
.ensures(NomosShardIdentity)
|
package/src/certified_read.ts
DELETED
|
@@ -1,313 +0,0 @@
|
|
|
1
|
-
// NOMOS — Nomos Sovereign: participants act · verify · remember LOCALLY; hosted
|
|
2
|
-
// remotes are replaceable custody/transport, not truth. ⇒ ONE Nomos GitHolon
|
|
3
|
-
// wasm32-wasip1 artifact {kernel · projection · embedded
|
|
4
|
-
// QuickJS engine} on V8 + WASI-shim, byte-identical everywhere. V8 = portability; the one
|
|
5
|
-
// wasm = determinism. No native, no wasmtime, no 2nd artifact, no domain-JS on bare V8.
|
|
6
|
-
// If a file isn't this / hosting this / authoring for this / proving this — it's gone.
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* **CertifiedRead — the DECLARED write-path read surface (survival Constraint 5;
|
|
10
|
-
* `certified_read.md`).** A tenant directive's `decide()` may READ the current world to
|
|
11
|
-
* decide whether a write is valid ("is this thing assigned? what's the balance? enough
|
|
12
|
-
* stock?"). That read RESULT is a DECISION INPUT, so it must be a deterministic, captured,
|
|
13
|
-
* byte-identical-across-peers fact ([[determinism-or-death]]). This module is the AUTHORING
|
|
14
|
-
* surface a tenant uses to DECLARE such reads — the `reads:{ name: q.someProfiledRead(args) }`
|
|
15
|
-
* shape from `certified_read.md` §"Tenant authoring".
|
|
16
|
-
*
|
|
17
|
-
* ## The two halves (mirroring `readmodel/src/certified_read.rs`)
|
|
18
|
-
* 1. {@link sqlProfileV0Check} — the TS port of the Rust `SqlProfileV0` enforcer. The villain
|
|
19
|
-
* is UNDER-SPECIFICATION, not SQLite: a declared write-path read MUST obey the restricted
|
|
20
|
-
* profile so EVERY valid execution plan produces the SAME canonical result (explicit
|
|
21
|
-
* `ORDER BY` ending in a UNIQUE tie-breaker on multi-row, NO clock/rng functions, BINARY
|
|
22
|
-
* collation only, NO floats, explicit columns, no implicit rowid, a single pure SELECT). An
|
|
23
|
-
* under-specified declared read is REFUSED at COMPILE (`certifiedRead(...).sql(...)` throws),
|
|
24
|
-
* the SAME refusal the Rust `CertifiedReadDef::compile` raises — so a tenant cannot author a
|
|
25
|
-
* non-deterministic write-path read.
|
|
26
|
-
* 2. {@link certifiedRead} — the declaration builder. `certifiedRead("stockAvailable")
|
|
27
|
-
* .sql("SELECT value_json FROM rows WHERE …", { multiRow:false })` yields a
|
|
28
|
-
* {@link CertifiedReadDecl} the directive's `.readsCertified({ available: decl })` attaches.
|
|
29
|
-
* The declared `query_id`s land in the canonical manifest (`manifest.ts`), so the
|
|
30
|
-
* ONE write-path gate (`executor::admit` step 5.6) can DERIVE the expected declared-read set
|
|
31
|
-
* and REFUSE any admit where a declared read produced no witness (the vacuous-pass close).
|
|
32
|
-
*
|
|
33
|
-
* THE BOUNDARY: a tenant declares the SQL recipe + the shape (single/multi-row + tie-breakers).
|
|
34
|
-
* Nomos owns the determinism (the profile, the canonical encoding, the witness, the admission
|
|
35
|
-
* re-run). The DSL author NEVER mints a witness or touches SQLite at decide time — the HOST
|
|
36
|
-
* resolves each declared read against the pinned projection snapshot and records the witness
|
|
37
|
-
* (the capture wire; the Rust `CertifiedReadPort::resolve_and_witness`).
|
|
38
|
-
*/
|
|
39
|
-
|
|
40
|
-
/** The SQL-profile-v0 version pin — frozen alongside the Rust `SQL_PROFILE_VERSION`. */
|
|
41
|
-
export const SQL_PROFILE_VERSION = "v0";
|
|
42
|
-
|
|
43
|
-
/** The NON-DETERMINISTIC SQL functions BANNED on the write/decide path — the clock family +
|
|
44
|
-
* the entropy family. Lower-cased (SQLite function names are case-insensitive). Byte-identical
|
|
45
|
-
* to the Rust `BANNED_FUNCTIONS` so the TS compile refusal and the Rust runtime guard agree. */
|
|
46
|
-
export const BANNED_FUNCTIONS = [
|
|
47
|
-
"random",
|
|
48
|
-
"randomblob",
|
|
49
|
-
"datetime",
|
|
50
|
-
"date",
|
|
51
|
-
"time",
|
|
52
|
-
"julianday",
|
|
53
|
-
"unixepoch",
|
|
54
|
-
"strftime",
|
|
55
|
-
"current_timestamp",
|
|
56
|
-
"current_time",
|
|
57
|
-
"current_date",
|
|
58
|
-
"localtime",
|
|
59
|
-
"localtimestamp",
|
|
60
|
-
] as const;
|
|
61
|
-
|
|
62
|
-
/** Why a declared write-path read was REFUSED by the SQL-profile-v0 enforcer — the same typed
|
|
63
|
-
* cause set as the Rust `ProfileViolation`. Carried in the thrown {@link ProfileError}. */
|
|
64
|
-
export type ProfileViolation =
|
|
65
|
-
| { kind: "SelectStar" }
|
|
66
|
-
| { kind: "MissingOrderBy" }
|
|
67
|
-
| { kind: "NoUniqueTieBreaker" }
|
|
68
|
-
| { kind: "BannedFunction"; name: string }
|
|
69
|
-
| { kind: "NonBinaryCollation"; collation: string }
|
|
70
|
-
| { kind: "FloatInDecision"; literal: string }
|
|
71
|
-
| { kind: "ImplicitRowid" }
|
|
72
|
-
| { kind: "NotASingleSelect" };
|
|
73
|
-
|
|
74
|
-
/** A compile-time refusal of an under-specified declared read (the villain is
|
|
75
|
-
* UNDER-SPECIFICATION). Thrown by {@link certifiedRead}'s `.sql(...)`. */
|
|
76
|
-
export class ProfileError extends Error {
|
|
77
|
-
constructor(public readonly violation: ProfileViolation) {
|
|
78
|
-
super(`SQL-profile-v0 violation: ${describe(violation)}`);
|
|
79
|
-
this.name = "ProfileError";
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function describe(v: ProfileViolation): string {
|
|
84
|
-
switch (v.kind) {
|
|
85
|
-
case "SelectStar":
|
|
86
|
-
return "SELECT * is banned on the write path — list explicit columns";
|
|
87
|
-
case "MissingOrderBy":
|
|
88
|
-
return "a multi-row query MUST have an explicit ORDER BY (row order is UNDEFINED without one)";
|
|
89
|
-
case "NoUniqueTieBreaker":
|
|
90
|
-
return "ORDER BY MUST end in a UNIQUE deterministic tie-breaker (e.g. `…, aggregate_id`)";
|
|
91
|
-
case "BannedFunction":
|
|
92
|
-
return `non-deterministic function \`${v.name}\` is banned (the clock/rng family)`;
|
|
93
|
-
case "NonBinaryCollation":
|
|
94
|
-
return `COLLATE \`${v.collation}\` is banned — only BINARY/canonical collation is byte-stable`;
|
|
95
|
-
case "FloatInDecision":
|
|
96
|
-
return `float literal \`${v.literal}\` in a decision is banned — use integers / decimals-as-strings`;
|
|
97
|
-
case "ImplicitRowid":
|
|
98
|
-
return "implicit rowid is banned unless modeled";
|
|
99
|
-
case "NotASingleSelect":
|
|
100
|
-
return "the write-path read must be a SINGLE pure SELECT (no chained statements / write verb)";
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// ── the profile parser (lower-cased input only; a faithful TS port of the Rust enforcer) ──
|
|
105
|
-
|
|
106
|
-
function collapseWs(s: string): string {
|
|
107
|
-
return ` ${s.replace(/\s+/g, " ").trim()} `;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function containsWord(hay: string, word: string): boolean {
|
|
111
|
-
if (word.length === 0) return false;
|
|
112
|
-
const re = new RegExp(`(?<![A-Za-z0-9_])${escapeRe(word)}(?![A-Za-z0-9_])`);
|
|
113
|
-
return re.test(hay);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function containsFunctionCall(hay: string, name: string): boolean {
|
|
117
|
-
// `name` followed (with optional spaces) by `(`, and NOT preceded by a word char.
|
|
118
|
-
const re = new RegExp(`(?<![A-Za-z0-9_])${escapeRe(name)}\\s*\\(`);
|
|
119
|
-
return re.test(hay);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function escapeRe(s: string): string {
|
|
123
|
-
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function selectListOf(trimmed: string): string {
|
|
127
|
-
const afterSelect = trimmed.startsWith("select ") ? trimmed.slice("select ".length) : trimmed;
|
|
128
|
-
const idx = afterSelect.indexOf(" from ");
|
|
129
|
-
return idx >= 0 ? afterSelect.slice(0, idx) : afterSelect;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function orderByClause(normalized: string): string | undefined {
|
|
133
|
-
const idx = normalized.lastIndexOf(" order by ");
|
|
134
|
-
if (idx < 0) return undefined;
|
|
135
|
-
const after = normalized.slice(idx + " order by ".length);
|
|
136
|
-
const lim = after.indexOf(" limit ");
|
|
137
|
-
return (lim >= 0 ? after.slice(0, lim) : after).trim();
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function collateOtherThanBinary(normalized: string): string | undefined {
|
|
141
|
-
const re = /collate\s+([a-z0-9_]+)/g;
|
|
142
|
-
let m: RegExpExecArray | null;
|
|
143
|
-
while ((m = re.exec(normalized)) !== null) {
|
|
144
|
-
const name = m[1]!;
|
|
145
|
-
if (name !== "binary") return name;
|
|
146
|
-
}
|
|
147
|
-
return undefined;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function floatLiteral(normalized: string): string | undefined {
|
|
151
|
-
// A numeric literal that is NOT a pure integer: a decimal point and/or scientific exponent,
|
|
152
|
-
// at a token boundary, outside single-quoted strings. Mirrors the Rust `float_literal`.
|
|
153
|
-
let inStr = false;
|
|
154
|
-
const isIdent = (c: string) => /[A-Za-z0-9_]/.test(c);
|
|
155
|
-
const n = normalized.length;
|
|
156
|
-
let i = 0;
|
|
157
|
-
while (i < n) {
|
|
158
|
-
const ch = normalized[i]!;
|
|
159
|
-
if (ch === "'") {
|
|
160
|
-
inStr = !inStr;
|
|
161
|
-
i += 1;
|
|
162
|
-
continue;
|
|
163
|
-
}
|
|
164
|
-
if (inStr) {
|
|
165
|
-
i += 1;
|
|
166
|
-
continue;
|
|
167
|
-
}
|
|
168
|
-
const prev = i > 0 ? normalized[i - 1]! : "";
|
|
169
|
-
const startsDigit = /[0-9]/.test(ch);
|
|
170
|
-
const startsDot = ch === "." && i + 1 < n && /[0-9]/.test(normalized[i + 1]!);
|
|
171
|
-
const boundary = i === 0 || (!isIdent(prev) && prev !== ".");
|
|
172
|
-
if (boundary && (startsDigit || startsDot)) {
|
|
173
|
-
const start = i;
|
|
174
|
-
let isFloat = startsDot;
|
|
175
|
-
while (i < n && /[0-9.]/.test(normalized[i]!)) {
|
|
176
|
-
if (normalized[i] === ".") isFloat = true;
|
|
177
|
-
i += 1;
|
|
178
|
-
}
|
|
179
|
-
if (i < n && (normalized[i] === "e" || normalized[i] === "E")) {
|
|
180
|
-
let j = i + 1;
|
|
181
|
-
if (j < n && (normalized[j] === "+" || normalized[j] === "-")) j += 1;
|
|
182
|
-
if (j < n && /[0-9]/.test(normalized[j]!)) {
|
|
183
|
-
isFloat = true;
|
|
184
|
-
i = j;
|
|
185
|
-
while (i < n && /[0-9]/.test(normalized[i]!)) i += 1;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
if (isFloat) return normalized.slice(start, i);
|
|
189
|
-
continue;
|
|
190
|
-
}
|
|
191
|
-
i += 1;
|
|
192
|
-
}
|
|
193
|
-
return undefined;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* Check `sql` against SQL-profile-v0 — the TS port of `SqlProfileV0::check`. `multiRow` is
|
|
198
|
-
* whether the query can return more than one row (a scalar COUNT/MAX is single-row, exempt from
|
|
199
|
-
* the ORDER BY rule). `uniqueTieBreakers` are the declared unique deterministic key columns the
|
|
200
|
-
* multi-row `ORDER BY` must end in. Returns the FIRST {@link ProfileViolation}, or `undefined`
|
|
201
|
-
* when the query obeys EVERY rule.
|
|
202
|
-
*/
|
|
203
|
-
export function sqlProfileV0Check(
|
|
204
|
-
sql: string,
|
|
205
|
-
multiRow: boolean,
|
|
206
|
-
uniqueTieBreakers: string[],
|
|
207
|
-
): ProfileViolation | undefined {
|
|
208
|
-
const lower = sql.toLowerCase();
|
|
209
|
-
const normalized = collapseWs(lower);
|
|
210
|
-
|
|
211
|
-
// single pure SELECT (no chained statements / write verbs)
|
|
212
|
-
const trimmed = normalized.replace(/;+$/, "").trim();
|
|
213
|
-
if (trimmed.includes(";")) return { kind: "NotASingleSelect" };
|
|
214
|
-
if (!trimmed.startsWith("select ") && trimmed !== "select") return { kind: "NotASingleSelect" };
|
|
215
|
-
for (const verb of [
|
|
216
|
-
" insert ", " update ", " delete ", " drop ", " create ", " alter ", " attach ",
|
|
217
|
-
" pragma ", " replace ", " reindex ", " vacuum ",
|
|
218
|
-
]) {
|
|
219
|
-
if (normalized.includes(verb)) return { kind: "NotASingleSelect" };
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// explicit columns (no SELECT *), allowing the one `count(*)`
|
|
223
|
-
const selectList = selectListOf(trimmed).replaceAll("count(*)", "count_star");
|
|
224
|
-
if (selectList.includes("*")) return { kind: "SelectStar" };
|
|
225
|
-
|
|
226
|
-
// banned non-deterministic functions (clock/rng family), anywhere
|
|
227
|
-
for (const name of BANNED_FUNCTIONS) {
|
|
228
|
-
if (containsFunctionCall(normalized, name)) return { kind: "BannedFunction", name };
|
|
229
|
-
if (name.startsWith("current_") && containsWord(normalized, name)) {
|
|
230
|
-
return { kind: "BannedFunction", name };
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// BINARY/canonical collation only
|
|
235
|
-
const coll = collateOtherThanBinary(normalized);
|
|
236
|
-
if (coll !== undefined) return { kind: "NonBinaryCollation", collation: coll };
|
|
237
|
-
|
|
238
|
-
// no implicit rowid (unless modeled as a column)
|
|
239
|
-
for (const kw of ["rowid", "_rowid_", "oid"]) {
|
|
240
|
-
if (containsWord(normalized, kw)) return { kind: "ImplicitRowid" };
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// no floats in decision-critical results
|
|
244
|
-
const lit = floatLiteral(normalized);
|
|
245
|
-
if (lit !== undefined) return { kind: "FloatInDecision", literal: lit };
|
|
246
|
-
|
|
247
|
-
// ORDER BY with a unique tie-breaker (multi-row only)
|
|
248
|
-
if (multiRow) {
|
|
249
|
-
const orderBy = orderByClause(normalized);
|
|
250
|
-
if (orderBy === undefined) return { kind: "MissingOrderBy" };
|
|
251
|
-
let lastTerm = orderBy.split(",").pop()!.trim();
|
|
252
|
-
const collIdx = lastTerm.indexOf(" collate ");
|
|
253
|
-
if (collIdx >= 0) lastTerm = lastTerm.slice(0, collIdx).trim();
|
|
254
|
-
lastTerm = lastTerm.replace(/ asc$/, "").replace(/ desc$/, "").trim();
|
|
255
|
-
const bare = lastTerm.includes(".") ? lastTerm.split(".").pop()! : lastTerm;
|
|
256
|
-
const ok = uniqueTieBreakers.some((t) => t === lastTerm || t === bare);
|
|
257
|
-
if (!ok) return { kind: "NoUniqueTieBreaker" };
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
return undefined;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* A FINISHED, profile-conforming CertifiedRead declaration — the write-path read API surface
|
|
265
|
-
* (the DSL analogue of the Rust `CertifiedReadDef`). Construction (`certifiedRead(id).sql(...)`)
|
|
266
|
-
* is the COMPILE-TIME refusal: a profile-violating SQL throws {@link ProfileError}, so a
|
|
267
|
-
* `CertifiedReadDecl` value is a profile-conformance WITNESS by construction. The `queryId` is
|
|
268
|
-
* which question; the `sql` is the validated recipe; `multiRow` + `uniqueTieBreakers` are the
|
|
269
|
-
* shape the profile + the admission re-run pin.
|
|
270
|
-
*/
|
|
271
|
-
export interface CertifiedReadDecl {
|
|
272
|
-
readonly queryId: string;
|
|
273
|
-
readonly sql: string;
|
|
274
|
-
readonly multiRow: boolean;
|
|
275
|
-
readonly uniqueTieBreakers: string[];
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/** Options for {@link CertifiedReadBuilder.sql}. */
|
|
279
|
-
export interface CertifiedReadOpts {
|
|
280
|
-
/** Whether the query can return more than one row (drives the ORDER BY requirement).
|
|
281
|
-
* Defaults to `false` (a single-row scalar read, e.g. a COUNT/MAX — exempt from ORDER BY). */
|
|
282
|
-
readonly multiRow?: boolean;
|
|
283
|
-
/** The declared unique deterministic tie-breaker column(s) the multi-row ORDER BY must end in
|
|
284
|
-
* (e.g. `["aggregate_id"]`). Required for a `multiRow` query; ignored for single-row. */
|
|
285
|
-
readonly uniqueTieBreakers?: string[];
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
/** Stage 2: attach the profiled SQL recipe, producing the finished {@link CertifiedReadDecl}. */
|
|
289
|
-
class CertifiedReadBuilder<Id extends string> {
|
|
290
|
-
constructor(private readonly id: Id) {}
|
|
291
|
-
/**
|
|
292
|
-
* Declare the read's SQL recipe. COMPILE-TIME refusal: the SQL is validated against
|
|
293
|
-
* SQL-profile-v0 here ({@link sqlProfileV0Check}) and THROWS {@link ProfileError} on any
|
|
294
|
-
* under-specification — the SAME refusal the Rust `CertifiedReadDef::compile` raises. So a
|
|
295
|
-
* non-conforming declared read can never become a {@link CertifiedReadDecl}.
|
|
296
|
-
*/
|
|
297
|
-
sql(text: string, opts: CertifiedReadOpts = {}): CertifiedReadDecl {
|
|
298
|
-
const multiRow = opts.multiRow ?? false;
|
|
299
|
-
const uniqueTieBreakers = [...(opts.uniqueTieBreakers ?? [])];
|
|
300
|
-
const violation = sqlProfileV0Check(text, multiRow, uniqueTieBreakers);
|
|
301
|
-
if (violation !== undefined) throw new ProfileError(violation);
|
|
302
|
-
return { queryId: this.id, sql: text, multiRow, uniqueTieBreakers };
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
/**
|
|
307
|
-
* Begin a CertifiedRead declaration: `certifiedRead("stockAvailable").sql("SELECT … FROM …",
|
|
308
|
-
* { multiRow:false })`. The id is the `query_id` frozen into the witness + the manifest's
|
|
309
|
-
* declared-read set; `.sql(...)` validates the recipe and produces the finished decl.
|
|
310
|
-
*/
|
|
311
|
-
export function certifiedRead<const Id extends string>(id: Id): CertifiedReadBuilder<Id> {
|
|
312
|
-
return new CertifiedReadBuilder(id);
|
|
313
|
-
}
|