@githolon/dsl 0.101.2 → 0.101.4
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/package.json +1 -1
- package/src/compile_package_main.ts +8 -2
- package/src/directive.ts +43 -0
- package/src/flow_gate.ts +9 -0
- package/src/framework/workspace_invariant.ts +2 -0
- package/src/law_check.ts +13 -2
- package/src/precondition.ts +2 -0
- package/src/zod_lazy.ts +14 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@githolon/dsl",
|
|
3
|
-
"version": "0.101.
|
|
3
|
+
"version": "0.101.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Nomos 2 domain-authoring DSL: aggregates + directives in TS, executed and encoded to the Rust kernel's wire shapes.",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
@@ -2365,6 +2365,7 @@ async function main(): Promise<void> {
|
|
|
2365
2365
|
// Exec the bin DIRECTLY (not via `node`): esbuild's postinstall replaces the
|
|
2366
2366
|
// JS shim with the platform-native binary, which only an exec can run.
|
|
2367
2367
|
const esbuildBin = path.join(path.dirname(esbuildPkgJson), esbuildBinRel ?? "bin/esbuild");
|
|
2368
|
+
const esbuildMod = (await import(pathToFileURL(path.join(path.dirname(esbuildPkgJson), "lib", "main.js")).href)) as typeof import("esbuild");
|
|
2368
2369
|
|
|
2369
2370
|
const canonicalizeEngineJavascript = (source: string): string => {
|
|
2370
2371
|
// esbuild includes module labels derived from the package's install topology in
|
|
@@ -2391,7 +2392,6 @@ async function main(): Promise<void> {
|
|
|
2391
2392
|
// package as the binary) so a resolve-plugin can stub zod's locale barrel to en-only —
|
|
2392
2393
|
// the CLI binary has no plugin surface. Same bundle options + minify; deterministic
|
|
2393
2394
|
// (pinned esbuild version, deterministic minifier).
|
|
2394
|
-
const esbuildMod = (await import(pathToFileURL(path.join(path.dirname(esbuildPkgJson), "lib", "main.js")).href)) as typeof import("esbuild");
|
|
2395
2395
|
const localeStripToEn = {
|
|
2396
2396
|
name: "nomos-zod-locales-en-only",
|
|
2397
2397
|
setup(build: import("esbuild").PluginBuild) {
|
|
@@ -2778,7 +2778,13 @@ async function main(): Promise<void> {
|
|
|
2778
2778
|
// ── 5. the typed TS client (#10 — the web sibling of codegen_dart), with the
|
|
2779
2779
|
// deployed law's content hash baked in (emitted AFTER the package is hashed) ──
|
|
2780
2780
|
const clientPath = path.join(outDir, `${cfg.name}.client.ts`);
|
|
2781
|
-
|
|
2781
|
+
const clientSource = generateTsClient(presentationDomainModules, { packageName: cfg.name, domainHash, interface: packageInterface, compat: nomosCompat });
|
|
2782
|
+
try {
|
|
2783
|
+
await esbuildMod.transform(clientSource, { loader: "ts", format: "esm", target: "es2020", logLevel: "silent" });
|
|
2784
|
+
} catch (e) {
|
|
2785
|
+
fail(`generated TypeScript client is not syntactically valid — this is a Nomos codegen bug: ${(e as Error).message}`);
|
|
2786
|
+
}
|
|
2787
|
+
writeFileSync(clientPath, clientSource, "utf8");
|
|
2782
2788
|
|
|
2783
2789
|
// ── 6. the typed Dart frontend (opt-in; the Flutter sibling of step 5, emitted
|
|
2784
2790
|
// after the package hash like the TS client) ──
|
package/src/directive.ts
CHANGED
|
@@ -64,6 +64,12 @@ export interface PreconditionDescriptor {
|
|
|
64
64
|
readonly predicate: (row: Record<string, unknown> | undefined) => boolean;
|
|
65
65
|
/** The typed refusal code emitted when the predicate is false. */
|
|
66
66
|
readonly rejectCode: string;
|
|
67
|
+
/**
|
|
68
|
+
* Why this precondition may intentionally make a fact about private state observable through its
|
|
69
|
+
* public refusal. Set by chaining `.declassify(reason)` directly after the precondition marker;
|
|
70
|
+
* carried onto the synthesized workspace invariant and into the replay-verified flow-gate law.
|
|
71
|
+
*/
|
|
72
|
+
readonly declassify?: string;
|
|
67
73
|
}
|
|
68
74
|
|
|
69
75
|
/**
|
|
@@ -662,6 +668,43 @@ class DirectivePlanStep<Id extends string, P> {
|
|
|
662
668
|
});
|
|
663
669
|
}
|
|
664
670
|
|
|
671
|
+
/**
|
|
672
|
+
* Acknowledge that the MOST RECENT write-path precondition may intentionally reveal one fact about
|
|
673
|
+
* private state through its typed refusal. This is business policy, so it stays beside the condition
|
|
674
|
+
* it explains; the compiler carries it onto the generated invariant without exposing that internal
|
|
675
|
+
* invariant to tenant code.
|
|
676
|
+
*/
|
|
677
|
+
declassify(reason: string): DirectivePlanStep<Id, P> {
|
|
678
|
+
const trimmed = reason.trim();
|
|
679
|
+
if (trimmed.length === 0) {
|
|
680
|
+
throw new Error(
|
|
681
|
+
`directive('${this.id}').declassify(reason) needs a non-empty human reason — it is the audit record for an intended private-state refusal.`,
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
if (this.preconditions.length === 0) {
|
|
685
|
+
throw new Error(
|
|
686
|
+
`directive('${this.id}').declassify(reason) must follow .requiresExisting(...), .where(...), or .withId(...).`,
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
const last = this.preconditions[this.preconditions.length - 1]!;
|
|
690
|
+
if (last.declassify !== undefined) {
|
|
691
|
+
throw new Error(
|
|
692
|
+
`directive('${this.id}') already declassifies its most recent precondition — declare each policy once, beside its condition.`,
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
return new DirectivePlanStep(
|
|
696
|
+
this.id,
|
|
697
|
+
this.marker,
|
|
698
|
+
this.aggregate,
|
|
699
|
+
this.payloadSchema,
|
|
700
|
+
this.createOnceIdField,
|
|
701
|
+
[
|
|
702
|
+
...this.preconditions.slice(0, -1),
|
|
703
|
+
{ ...last, declassify: trimmed },
|
|
704
|
+
],
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
|
|
665
708
|
plan(fn: (payload: P, ctx: Ports) => PlannedOp[]): RequirableDirective<P> {
|
|
666
709
|
return makeRequirable({
|
|
667
710
|
id: this.id,
|
package/src/flow_gate.ts
CHANGED
|
@@ -50,6 +50,7 @@ type FlowModule = {
|
|
|
50
50
|
readonly id: string;
|
|
51
51
|
readonly on: string;
|
|
52
52
|
readonly declassify?: string;
|
|
53
|
+
readonly __nomosGenerated?: "directive-precondition";
|
|
53
54
|
readonly reads: (ctx: { intent: Record<string, unknown>; principal?: string }) => readonly { readonly aggregate: string }[];
|
|
54
55
|
}[];
|
|
55
56
|
readonly impureCapabilities?: readonly {
|
|
@@ -190,6 +191,14 @@ export function assertFlowGate(m: CanonicalManifest, mod?: FlowModule): void {
|
|
|
190
191
|
for (const r of refs) {
|
|
191
192
|
const short = r.aggregate.includes(".") ? r.aggregate.slice(r.aggregate.lastIndexOf(".") + 1) : r.aggregate;
|
|
192
193
|
if (privateTypes.has(short)) {
|
|
194
|
+
if (wi.__nomosGenerated === "directive-precondition") {
|
|
195
|
+
throw new FlowGateError(
|
|
196
|
+
`[nomosFlowGate rule 4 — no secret-correlated refusal] directive '${wi.on}' checks the PRIVATE aggregate ` +
|
|
197
|
+
`'${r.aggregate}' before committing. Its refusal can reveal a fact about that record. If that is intended, ` +
|
|
198
|
+
`keep the policy beside the condition: chain .declassify("<why this refusal may reveal private state>") ` +
|
|
199
|
+
`immediately after .requiresExisting(...), .where(...), or .withId(...).`,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
193
202
|
throw new FlowGateError(
|
|
194
203
|
`[nomosFlowGate rule 4 — no secret-correlated refusal] workspaceInvariant '${wi.id}' (on '${wi.on}') reads the ` +
|
|
195
204
|
`PRIVATE aggregate '${r.aggregate}' — its refusal is a public ref transition that can correlate with that secret. ` +
|
|
@@ -150,6 +150,8 @@ export interface WorkspaceInvariantDecl {
|
|
|
150
150
|
* a secret-correlated refusal.
|
|
151
151
|
*/
|
|
152
152
|
readonly declassify?: string;
|
|
153
|
+
/** Framework-only provenance used to keep compiler errors on the tenant-authored surface. */
|
|
154
|
+
readonly __nomosGenerated?: "directive-precondition";
|
|
153
155
|
/** Brand so a plain object is never mistaken for a declaration. */
|
|
154
156
|
readonly __isWorkspaceInvariant: true;
|
|
155
157
|
}
|
package/src/law_check.ts
CHANGED
|
@@ -907,6 +907,12 @@ function invariantFindings(mod: DomainModule, aggById: ReadonlyMap<string, Aggre
|
|
|
907
907
|
let intent: Record<string, unknown> = {};
|
|
908
908
|
if (dir !== undefined) {
|
|
909
909
|
const probe = probeDirective(dir, `${domain}/${wi.id}#intent`);
|
|
910
|
+
// A generated precondition's id selector is fully typed and only runs after payload validation.
|
|
911
|
+
// If Nomos cannot synthesize this directive's otherwise-valid exotic payload (for example a
|
|
912
|
+
// branded value object), invoking that selector with our invented `{}` would manufacture a
|
|
913
|
+
// framework false positive. Hand-authored invariants still get the empty-intent probe because
|
|
914
|
+
// their reads function may genuinely need to be total independently of a directive schema.
|
|
915
|
+
if (probe === undefined && wi.__nomosGenerated === "directive-precondition") continue;
|
|
910
916
|
if (probe !== undefined) intent = probe.probePayload;
|
|
911
917
|
}
|
|
912
918
|
const ctx = { intent, principal: "principal:probe", principalVerified: true } as const;
|
|
@@ -935,12 +941,17 @@ function invariantFindings(mod: DomainModule, aggById: ReadonlyMap<string, Aggre
|
|
|
935
941
|
}
|
|
936
942
|
});
|
|
937
943
|
if (threwAt !== undefined) {
|
|
944
|
+
const generatedPrecondition = wi.__nomosGenerated === "directive-precondition";
|
|
938
945
|
out.push({
|
|
939
946
|
domain,
|
|
940
947
|
directiveId: wi.on,
|
|
941
948
|
level: "warn",
|
|
942
|
-
what:
|
|
943
|
-
|
|
949
|
+
what: generatedPrecondition
|
|
950
|
+
? `directive '${wi.on}' precondition THREW on the synthesized payload (${(err as Error)?.message ?? String(err)}) — its id selector or predicate may not handle every valid payload`
|
|
951
|
+
: `workspace invariant '${wi.id}' ${threwAt === "reads" ? "reads()" : "assert()"} THREW on the synthesized sample (${(err as Error)?.message ?? String(err)}) — a predicate bug, or a payload-dependent path the sample hit`,
|
|
952
|
+
fix: generatedPrecondition
|
|
953
|
+
? `run the directive's .requiresExisting(...), .where(...), or .withId(...) condition on ${samplePayloadText(intent)} — if that payload is valid, make the condition total for it`
|
|
954
|
+
: `run it on intent=${samplePayloadText(intent)} snapshots=${samplePayloadText(snapshots)} — if the throw is real, the gate refuses on '${wi.on}'; if the sample's shape never occurs, guard the predicate`,
|
|
944
955
|
});
|
|
945
956
|
}
|
|
946
957
|
}
|
package/src/precondition.ts
CHANGED
|
@@ -86,6 +86,8 @@ export function synthesizePreconditionInvariants<P>(directive: Directive<P>): Wo
|
|
|
86
86
|
const row = snapshots[REF_NAME] as Record<string, unknown> | undefined;
|
|
87
87
|
return desc.predicate(row) ? { accept: true } : { reject: desc.rejectCode };
|
|
88
88
|
},
|
|
89
|
+
...(desc.declassify !== undefined ? { declassify: desc.declassify } : {}),
|
|
90
|
+
__nomosGenerated: "directive-precondition" as const,
|
|
89
91
|
__isWorkspaceInvariant: true as const,
|
|
90
92
|
};
|
|
91
93
|
});
|
package/src/zod_lazy.ts
CHANGED
|
@@ -26,12 +26,22 @@ export function zodUnwrapLazy(zt: z.ZodTypeAny): z.ZodTypeAny {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
const capFirst = (s: string): string => (s.length === 0 ? s : s[0]!.toUpperCase() + s.slice(1));
|
|
29
|
-
|
|
29
|
+
|
|
30
|
+
/** Turn one structural path component into a valid TS/Dart identifier component.
|
|
31
|
+
* `*` is the record-value marker, not tenant vocabulary, so name it `Value`. */
|
|
32
|
+
function typeNamePart(segment: string): string {
|
|
33
|
+
if (segment === "*") return "Value";
|
|
34
|
+
const words = segment.split(/[^A-Za-z0-9]+/).filter(Boolean);
|
|
35
|
+
const joined = words.map(capFirst).join("") || "Field";
|
|
36
|
+
return /^[A-Za-z]/.test(joined) ? joined : `N${joined}`;
|
|
37
|
+
}
|
|
30
38
|
|
|
31
39
|
/**
|
|
32
40
|
* A PascalCase type name from a payload path (`saveTree.root` → `SaveTreeRoot`), array markers
|
|
33
|
-
* dropped
|
|
34
|
-
* TS
|
|
41
|
+
* dropped and record wildcards named `Value` (`saveJson.data.*` → `SaveJsonDataValue`). The SAME
|
|
42
|
+
* base the TS and Dart recursive-type synthesizers use so a recursive type's TS alias and Dart
|
|
43
|
+
* class agree in the common case. Every segment is sanitized here because schema object keys may
|
|
44
|
+
* contain punctuation even though generated language identifiers may not.
|
|
35
45
|
*/
|
|
36
46
|
export function recursiveTypeBaseName(path: string): string {
|
|
37
47
|
return (
|
|
@@ -39,7 +49,7 @@ export function recursiveTypeBaseName(path: string): string {
|
|
|
39
49
|
.replace(/\[\]/g, "")
|
|
40
50
|
.split(/[.]/)
|
|
41
51
|
.filter(Boolean)
|
|
42
|
-
.map(
|
|
52
|
+
.map(typeNamePart)
|
|
43
53
|
.join("") || "Recursive"
|
|
44
54
|
);
|
|
45
55
|
}
|