@githolon/dsl 0.4.0 → 0.5.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.
- package/package.json +2 -1
- package/src/build_package.ts +4 -0
- package/src/capability_exports.ts +55 -0
- package/src/codegen_dart.ts +9 -0
- package/src/codegen_proof.ts +140 -12
- package/src/compile_package_main.ts +255 -14
- package/src/directive.ts +35 -10
- package/src/engine_entry.ts +5 -1
- package/src/framework/capability.ts +215 -0
- package/src/framework/impure_capability.ts +25 -3
- package/src/framework/workspaces.ts +129 -0
- package/src/index.ts +9 -0
- package/src/manifest.ts +103 -0
- package/src/read.ts +29 -0
- package/src/stable_ids.ts +226 -0
- package/src/stable_ids_types.ts +40 -0
- package/src/usd.ts +54 -0
- package/src/usd_layers.ts +65 -1
- package/src/wire_encode.ts +18 -0
- package/dart/.dart_tool/package_config.json +0 -328
- package/dart/.dart_tool/package_graph.json +0 -485
- package/dart/.dart_tool/pub/bin/test/test.dart-3.11.5.snapshot +0 -0
- package/dart/.dart_tool/test/incremental_kernel.Ly9AZGFydD0zLjU= +0 -0
- package/dart/.dart_tool/version +0 -1
- package/dart/build/native_assets/macos/native_assets.json +0 -1
- package/dart/build/test_cache/build/89a6598c8854ed031dfc25d83c80860e.cache.dill.track.dill +0 -0
- package/dart/build/unit_test_assets/AssetManifest.bin +0 -0
- package/dart/build/unit_test_assets/FontManifest.json +0 -1
- package/dart/build/unit_test_assets/NOTICES.Z +0 -0
- package/dart/build/unit_test_assets/NativeAssetsManifest.json +0 -1
- package/dart/build/unit_test_assets/shaders/ink_sparkle.frag +0 -0
- package/dart/build/unit_test_assets/shaders/stretch_effect.frag +0 -0
|
@@ -21,10 +21,10 @@
|
|
|
21
21
|
* Per domain: `key` is the engine dispatch key AND the identity-manifest name
|
|
22
22
|
* (`name` overrides the latter); `modules` is the ORDERED module list (later wins on
|
|
23
23
|
* a name collision — the framework `identity` union pattern). Read-side declarations
|
|
24
|
-
* (queries / counts / spatials / deriveds / combineds) are AUTO-DISCOVERED
|
|
25
|
-
* merged module exports by SHAPE (both individual decls and exported arrays
|
|
26
|
-
* deduped); anything undiscoverable can be passed explicitly as EXPORT
|
|
27
|
-
* `{
|
|
24
|
+
* (queries / counts / sums / spatials / deriveds / combineds) are AUTO-DISCOVERED
|
|
25
|
+
* from the merged module exports by SHAPE (both individual decls and exported arrays
|
|
26
|
+
* of them, deduped); anything undiscoverable can be passed explicitly as EXPORT
|
|
27
|
+
* NAMES, e.g. `{ mins: ["LOWEST_BIDS"], extraAggregates: ["SomeFrameworkHandle"] }`.
|
|
28
28
|
* Reports: `{ reports: { my_report_v1: "./report/my_report.ts#myReportV1" } }`.
|
|
29
29
|
*
|
|
30
30
|
* It emits into `outDir` (default `<config dir>/build`):
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
* `globalThis.plan`.
|
|
50
50
|
*/
|
|
51
51
|
import { execFileSync } from "node:child_process";
|
|
52
|
+
import { expandCapabilityExports, isCapabilityDecl } from "./capability_exports.js";
|
|
52
53
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
53
54
|
import { createRequire } from "node:module";
|
|
54
55
|
import path from "node:path";
|
|
@@ -93,9 +94,17 @@ import {
|
|
|
93
94
|
layeredRootUsda,
|
|
94
95
|
type LayeredModuleInput,
|
|
95
96
|
} from "./usd_layers.js";
|
|
97
|
+
import { domainManifest } from "./manifest.js";
|
|
98
|
+
import type { WireSchema } from "./wire.js";
|
|
99
|
+
import {
|
|
100
|
+
deriveStableIdContinuity,
|
|
101
|
+
stableIdsFromManifestJson,
|
|
102
|
+
type InferredRename,
|
|
103
|
+
type StableIds,
|
|
104
|
+
} from "./stable_ids.js";
|
|
96
105
|
import type { UsdLayer } from "./usd.js";
|
|
97
106
|
import { generateTsClient, tsClientFactoryName } from "./codegen_ts.js";
|
|
98
|
-
import { generateTsProof } from "./codegen_proof.js";
|
|
107
|
+
import { generateTsProof, proofLegsIndex } from "./codegen_proof.js";
|
|
99
108
|
|
|
100
109
|
const DSL_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
101
110
|
|
|
@@ -106,8 +115,9 @@ interface DomainConfig {
|
|
|
106
115
|
// Explicit EXPORT-NAME escape hatches (resolved on the merged module exports):
|
|
107
116
|
readonly queries?: readonly string[];
|
|
108
117
|
readonly counts?: readonly string[];
|
|
118
|
+
/** Escape hatch only since #57 — exported SumDecls auto-discover like queries/counts. */
|
|
109
119
|
readonly sums?: readonly string[];
|
|
110
|
-
/** Maintained MIN/MAX reads (#47) — export names
|
|
120
|
+
/** Maintained MIN/MAX reads (#47) — export names (config-named; not yet auto-discovered). */
|
|
111
121
|
readonly mins?: readonly string[];
|
|
112
122
|
readonly maxes?: readonly string[];
|
|
113
123
|
readonly spatials?: readonly string[];
|
|
@@ -232,6 +242,13 @@ function isCombinedDecl(v: unknown): v is CombinedDecl {
|
|
|
232
242
|
);
|
|
233
243
|
}
|
|
234
244
|
|
|
245
|
+
/** A sum decl/builder — the `{id, of, sumField}` mark (counts/extremums never carry `sumField`). */
|
|
246
|
+
function isSumDecl(v: unknown): v is AnySum {
|
|
247
|
+
if (!v || typeof v !== "object") return false;
|
|
248
|
+
const o = v as Record<string, unknown>;
|
|
249
|
+
return typeof o.id === "string" && typeof o.of === "string" && typeof o.sumField === "string";
|
|
250
|
+
}
|
|
251
|
+
|
|
235
252
|
/** A count decl/builder — `{id, of}` with NONE of the other decl families' marks. */
|
|
236
253
|
function isCountDecl(v: unknown): v is AnyCount {
|
|
237
254
|
if (!v || typeof v !== "object") return false;
|
|
@@ -470,6 +487,25 @@ async function main(): Promise<void> {
|
|
|
470
487
|
const layered = layeredFlag || cfg.layered === true;
|
|
471
488
|
const layeredDomains: { key: string; inputs: LayeredModuleInput[] }[] = [];
|
|
472
489
|
let importSeq = 0;
|
|
490
|
+
/**
|
|
491
|
+
* THE CAPABILITY LOWERING (capability_marketplace.md §4 — the deploy-warning
|
|
492
|
+
* prerequisite + the executor's task→capability→domain map). Rides the
|
|
493
|
+
* UNHASHED deploy sidecar only (a top-level deploy-body key, like
|
|
494
|
+
* `dispositions`) — never the read manifest the wasm parses, never any
|
|
495
|
+
* hash-bearing artifact: declaring a capability moves no identity hash, and
|
|
496
|
+
* capability-free deploy bodies stay byte-identical (omit-when-empty).
|
|
497
|
+
*/
|
|
498
|
+
const capabilityLowerings: {
|
|
499
|
+
capability: string;
|
|
500
|
+
domain: string;
|
|
501
|
+
aggregateId: string;
|
|
502
|
+
order: string;
|
|
503
|
+
complete: string;
|
|
504
|
+
fail: string;
|
|
505
|
+
block: string;
|
|
506
|
+
deadLetter: string;
|
|
507
|
+
tasksQueryId: string | null;
|
|
508
|
+
}[] = [];
|
|
473
509
|
|
|
474
510
|
for (const d of cfg.domains) {
|
|
475
511
|
if (typeof d.key !== "string" || !Array.isArray(d.modules) || d.modules.length === 0) {
|
|
@@ -492,6 +528,9 @@ async function main(): Promise<void> {
|
|
|
492
528
|
}
|
|
493
529
|
let merged: Mod = {};
|
|
494
530
|
for (const m of mods) merged = { ...merged, ...m };
|
|
531
|
+
// `capability()` bundles expand into discoverable entries (the CLI plane's
|
|
532
|
+
// merge point) — queries/sums/sid-minting/manifests all see the pieces.
|
|
533
|
+
merged = expandCapabilityExports(merged);
|
|
495
534
|
let shardingSums: AnySum[] = [];
|
|
496
535
|
let shardingMins: AnyExtremum[] = [];
|
|
497
536
|
let shardingMaxes: AnyExtremum[] = [];
|
|
@@ -555,9 +594,9 @@ async function main(): Promise<void> {
|
|
|
555
594
|
merged = { ...merged, ...shardingMod };
|
|
556
595
|
shardingModForDomain = shardingMod;
|
|
557
596
|
// The derived sharding law's MAINTAINED SUMS (`nomosEstateSummary` — the §5.1
|
|
558
|
-
// estate total over subtotal rows) must reach the read manifest + identity
|
|
559
|
-
//
|
|
560
|
-
//
|
|
597
|
+
// estate total over subtotal rows) must reach the read manifest + identity.
|
|
598
|
+
// (Sums auto-discover since #57; this explicit thread predates that and stays
|
|
599
|
+
// as belt-and-braces — the reference-dedupe below collapses the overlap.)
|
|
561
600
|
shardingSums = Object.values(shardingMod).filter(
|
|
562
601
|
(v): v is AnySum =>
|
|
563
602
|
!!v && typeof v === "object" &&
|
|
@@ -652,14 +691,47 @@ async function main(): Promise<void> {
|
|
|
652
691
|
...discover<WorkspaceInvariantDecl>(merged, isWorkspaceInvariantDecl),
|
|
653
692
|
...resolveNamed<WorkspaceInvariantDecl>(merged, d.workspaceInvariants, "workspaceInvariants"),
|
|
654
693
|
];
|
|
655
|
-
|
|
694
|
+
// Sums are AUTO-DISCOVERED like queries/counts (#57 — an exported SumDecl used to be
|
|
695
|
+
// SILENTLY DROPPED unless config-named; the config lane stays as the escape hatch).
|
|
696
|
+
// Reference-dedupe: a sum that is BOTH exported and config-named is the same object.
|
|
697
|
+
const sums = [
|
|
698
|
+
...new Set([
|
|
699
|
+
...discover<AnySum>(merged, isSumDecl),
|
|
700
|
+
...resolveNamed<AnySum>(merged, d.sums, "sums"),
|
|
701
|
+
...shardingSums,
|
|
702
|
+
]),
|
|
703
|
+
];
|
|
656
704
|
const mins = [...resolveNamed<AnyExtremum>(merged, d.mins, "mins"), ...shardingMins];
|
|
657
705
|
const maxes = [...resolveNamed<AnyExtremum>(merged, d.maxes, "maxes"), ...shardingMaxes];
|
|
658
|
-
|
|
706
|
+
// `capability()` declarations AUTO-DISCOVER (the marketplace lane, M2 —
|
|
707
|
+
// "declare ONE thing"); the config-named lane stays for hand-rolled
|
|
708
|
+
// impureCapability quartets. Deduped by task-aggregate id (a config-named
|
|
709
|
+
// capability() counts once).
|
|
710
|
+
const discoveredCapabilities = Object.values(merged).filter(isCapabilityDecl) as unknown as ImpureCapabilityDecl[];
|
|
711
|
+
const namedCapabilities = resolveNamed<ImpureCapabilityDecl>(
|
|
659
712
|
merged,
|
|
660
713
|
d.impureCapabilities,
|
|
661
714
|
"impureCapabilities",
|
|
662
715
|
);
|
|
716
|
+
const capByAggregate = new Map<string, ImpureCapabilityDecl>();
|
|
717
|
+
for (const c of [...discoveredCapabilities, ...namedCapabilities]) capByAggregate.set(c.aggregate.id, c);
|
|
718
|
+
const impureCapabilities = [...capByAggregate.values()];
|
|
719
|
+
for (const c of impureCapabilities) {
|
|
720
|
+
const meta = c as unknown as { name?: unknown; capability?: unknown; tasks?: { id?: unknown } };
|
|
721
|
+
const cap = typeof meta.name === "string" ? meta.name : typeof meta.capability === "string" ? meta.capability : null;
|
|
722
|
+
if (cap === null) continue; // a binding-free quartet lowers nothing (omit-when-absent)
|
|
723
|
+
capabilityLowerings.push({
|
|
724
|
+
capability: cap,
|
|
725
|
+
domain: d.key,
|
|
726
|
+
aggregateId: c.aggregate.id,
|
|
727
|
+
order: c.order.id,
|
|
728
|
+
complete: c.complete.id,
|
|
729
|
+
fail: c.fail.id,
|
|
730
|
+
block: c.block.id,
|
|
731
|
+
deadLetter: c.deadLetter.id,
|
|
732
|
+
tasksQueryId: typeof meta.tasks?.id === "string" ? meta.tasks.id : null,
|
|
733
|
+
});
|
|
734
|
+
}
|
|
663
735
|
const extraAggregates = resolveNamed<AggregateHandle>(merged, d.extraAggregates, "extraAggregates");
|
|
664
736
|
|
|
665
737
|
// Dart-codegen hints (frontend-only — never law; see DomainConfig):
|
|
@@ -804,6 +876,96 @@ async function main(): Promise<void> {
|
|
|
804
876
|
}
|
|
805
877
|
}
|
|
806
878
|
|
|
879
|
+
// ── STABLE-ID CONTINUITY (#58 — names are labels, identity is minted) ──
|
|
880
|
+
// Derive each composed domain's stable ids from the PRIOR compile: the committed
|
|
881
|
+
// `nomos.stable-ids.json` lockfile beside the config (the durable source — survives
|
|
882
|
+
// a clean clone), else the previous build's identity manifests. Match by name;
|
|
883
|
+
// exactly ONE disappeared + ONE appeared name of the same kind/shape is an inferred
|
|
884
|
+
// RENAME (sid carried, lineage recorded, printed below); anything else mints fresh
|
|
885
|
+
// at first appearance (a leftover disappearance is THE EVOLVE GATE's typed question
|
|
886
|
+
// at deploy — answered in the upgrade intent's disposition). The resolved continuity
|
|
887
|
+
// is attached to the module so the canonical manifest + USD-IR carry it.
|
|
888
|
+
interface StableIdLock {
|
|
889
|
+
v: 1;
|
|
890
|
+
domains: Record<string, { stableIds: StableIds; schemas: Record<string, WireSchema> }>;
|
|
891
|
+
}
|
|
892
|
+
const lockPath = path.join(cfgDir, "nomos.stable-ids.json");
|
|
893
|
+
const priorLock: StableIdLock | undefined = existsSync(lockPath)
|
|
894
|
+
? (JSON.parse(readFileSync(lockPath, "utf8")) as StableIdLock)
|
|
895
|
+
: undefined;
|
|
896
|
+
const priorBuildManifestsPath = path.join(outDir, "domain_identity_manifests.json");
|
|
897
|
+
const priorBuildManifests: Record<string, string> | undefined = existsSync(priorBuildManifestsPath)
|
|
898
|
+
? (JSON.parse(readFileSync(priorBuildManifestsPath, "utf8")) as Record<string, string>)
|
|
899
|
+
: undefined;
|
|
900
|
+
const inferredRenames: { domain: string; renames: InferredRename[] }[] = [];
|
|
901
|
+
const nextLock: StableIdLock = { v: 1, domains: {} };
|
|
902
|
+
for (const mod of domainModules) {
|
|
903
|
+
const identityName = mod.name;
|
|
904
|
+
// The CURRENT canonical aggregate schemas (a pre-continuity manifest pass —
|
|
905
|
+
// aggregates/schemas do not depend on continuity).
|
|
906
|
+
let currentAggs: { id: string; schema: WireSchema; kinds?: Record<string, string> }[];
|
|
907
|
+
try {
|
|
908
|
+
const preManifest = domainManifest(mod);
|
|
909
|
+
currentAggs = preManifest.aggregates.map((a) => ({
|
|
910
|
+
id: a.id,
|
|
911
|
+
schema: a.schema,
|
|
912
|
+
// The CURRENT field kinds (off the pre-continuity stableIds emission) — the
|
|
913
|
+
// pair rule's kind axis ("rename + add a field" in one release still infers).
|
|
914
|
+
kinds: Object.fromEntries(
|
|
915
|
+
Object.entries(preManifest.stableIds?.aggregates[a.id]?.fields ?? {}).map(
|
|
916
|
+
([f, v]) => [f, v.kind ?? ""] as [string, string],
|
|
917
|
+
),
|
|
918
|
+
),
|
|
919
|
+
}));
|
|
920
|
+
} catch {
|
|
921
|
+
// A palette-gap domain has no canonical identity (recorded-and-excluded later);
|
|
922
|
+
// it carries no stable ids either.
|
|
923
|
+
continue;
|
|
924
|
+
}
|
|
925
|
+
// Continuity source: the lockfile first (durable, committed), else the prior build.
|
|
926
|
+
let prior: StableIds | undefined;
|
|
927
|
+
let priorSchemas: Record<string, WireSchema> | undefined;
|
|
928
|
+
const locked = priorLock?.domains[identityName];
|
|
929
|
+
if (locked !== undefined) {
|
|
930
|
+
prior = locked.stableIds;
|
|
931
|
+
priorSchemas = locked.schemas;
|
|
932
|
+
} else if (priorBuildManifests?.[identityName] !== undefined) {
|
|
933
|
+
prior = stableIdsFromManifestJson(priorBuildManifests[identityName]!);
|
|
934
|
+
try {
|
|
935
|
+
const parsed = JSON.parse(priorBuildManifests[identityName]!) as {
|
|
936
|
+
aggregates?: { id: string; schema: WireSchema }[];
|
|
937
|
+
};
|
|
938
|
+
priorSchemas = Object.fromEntries((parsed.aggregates ?? []).map((a) => [a.id, a.schema]));
|
|
939
|
+
} catch {
|
|
940
|
+
priorSchemas = undefined;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
const { continuity, inferred } = deriveStableIdContinuity(
|
|
944
|
+
identityName,
|
|
945
|
+
currentAggs,
|
|
946
|
+
prior,
|
|
947
|
+
priorSchemas,
|
|
948
|
+
);
|
|
949
|
+
mod.stableIdContinuity = continuity;
|
|
950
|
+
// The layered emission's per-module layers must carry the SAME sids (the flatten
|
|
951
|
+
// homomorphism is byte-asserted), so the continuity rides every layer input too.
|
|
952
|
+
for (const ld of layeredDomains) {
|
|
953
|
+
if (ld.key === (mod.domain ?? mod.name)) {
|
|
954
|
+
for (const inp of ld.inputs) inp.module.stableIdContinuity = continuity;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
if (inferred.length > 0) inferredRenames.push({ domain: identityName, renames: inferred });
|
|
958
|
+
// The lockfile records the EMITTED surface (sids + lineage + kinds — the next
|
|
959
|
+
// compile's pair rule reads the kinds), not the bare continuity.
|
|
960
|
+
nextLock.domains[identityName] = {
|
|
961
|
+
stableIds: domainManifest(mod).stableIds ?? continuity,
|
|
962
|
+
schemas: Object.fromEntries(currentAggs.map((a) => [a.id, a.schema])),
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
// Persist the lockfile (commit it with the law: it is how a rename keeps its
|
|
966
|
+
// identity across clean clones). Deterministic — re-compiles rewrite the same bytes.
|
|
967
|
+
writeFileSync(lockPath, JSON.stringify(nextLock, null, 2) + "\n", "utf8");
|
|
968
|
+
|
|
807
969
|
// ── reports ──
|
|
808
970
|
const entryReports: string[] = [];
|
|
809
971
|
for (const [reportId, ref] of Object.entries(cfg.reports ?? {})) {
|
|
@@ -931,6 +1093,54 @@ async function main(): Promise<void> {
|
|
|
931
1093
|
fail(`bundled engine entry does not assign globalThis.plan — refusing to package`);
|
|
932
1094
|
}
|
|
933
1095
|
|
|
1096
|
+
// ── 1a. THE CAPTURED-READ GATE (#57, lifted-when-declared since #58) ──
|
|
1097
|
+
// `read()` (the captured-read lane) is served by the sealed engine ONLY for law
|
|
1098
|
+
// that DECLARES it: a directive whose plan reads must declare each query with
|
|
1099
|
+
// `.reads(theQuery)`, which lands the `nomosReadGate` key in the law's USD-IR (the
|
|
1100
|
+
// era key — the gate serves/captures/CAS-verifies reads only under it). A lump that
|
|
1101
|
+
// references read() while NO directive declares a captured-read query would still
|
|
1102
|
+
// halt at first dispatch (the engine provides no nomos.read for it), so THAT
|
|
1103
|
+
// compile refuses fail-closed. The marker is a string that lives ONLY inside
|
|
1104
|
+
// `read()`'s body (dsl/src/read.ts — keep them byte-identical) and is tree-shaken
|
|
1105
|
+
// out of read-free lumps.
|
|
1106
|
+
const CAPTURED_READ_LUMP_MARKER = "nomos.read is not provided by this host";
|
|
1107
|
+
const declaresCapturedReads = domainModules.some((m) =>
|
|
1108
|
+
m.directives.some(
|
|
1109
|
+
(d) => ((d as { declaredQueryReads?: unknown[] }).declaredQueryReads ?? []).length > 0,
|
|
1110
|
+
),
|
|
1111
|
+
);
|
|
1112
|
+
if (javascript.includes(CAPTURED_READ_LUMP_MARKER) && !declaresCapturedReads) {
|
|
1113
|
+
fail(
|
|
1114
|
+
`this law references read() — the CAPTURED-READ lane — but NO directive declares a ` +
|
|
1115
|
+
`captured-read query, so the sealed engine would provide no nomos.read and every ` +
|
|
1116
|
+
`dispatch of a read()-calling plan would halt at runtime. The compile refuses fail-closed.\n` +
|
|
1117
|
+
` remedy: declare the read on the directive whose plan calls it — ` +
|
|
1118
|
+
`.reads(theQueryDecl) (the same QueryDecl the read() call passes). The law then ` +
|
|
1119
|
+
`opts into the captured-read gate: reads are served live at author, captured onto ` +
|
|
1120
|
+
`the intent, replayed at verify, and CAS-checked at every gate.\n` +
|
|
1121
|
+
` alternative: commit the premise in the payload instead — the CALLER reads (a ` +
|
|
1122
|
+
`typed query/count off the holon) and the payload carries the result as a stamped fact.`,
|
|
1123
|
+
);
|
|
1124
|
+
}
|
|
1125
|
+
// The symmetric validation: every DECLARED captured-read query must return a known
|
|
1126
|
+
// aggregate of its domain (fail-closed — a read recipe over an unknown type can
|
|
1127
|
+
// never be derived at the gate).
|
|
1128
|
+
for (const m of domainModules) {
|
|
1129
|
+
const aggIds = new Set(m.aggregates.map((a) => a.id));
|
|
1130
|
+
for (const d of m.directives) {
|
|
1131
|
+
for (const q of (d as { declaredQueryReads?: { id: string; returns: string }[] })
|
|
1132
|
+
.declaredQueryReads ?? []) {
|
|
1133
|
+
if (!aggIds.has(q.returns)) {
|
|
1134
|
+
fail(
|
|
1135
|
+
`domain '${m.domain ?? m.name}': directive '${d.id}' declares captured read ` +
|
|
1136
|
+
`'${q.id}' returning '${q.returns}', which is not an aggregate of this domain — ` +
|
|
1137
|
+
`captured reads are same-workspace, same-domain declared reads.`,
|
|
1138
|
+
);
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
|
|
934
1144
|
// ── 1b. THE FROZEN-SANDBOX BOOT PROBE (lazy boot only, fail-closed) ──
|
|
935
1145
|
// The sealed engine freezes globalThis after the lump's top level; a lazy
|
|
936
1146
|
// domain's module init runs LATER, inside the dispatch. Any module that writes
|
|
@@ -1053,6 +1263,7 @@ async function main(): Promise<void> {
|
|
|
1053
1263
|
directives: m.directives.map((d) => d.id),
|
|
1054
1264
|
queries: (m.queries ?? []).map((q) => q.id),
|
|
1055
1265
|
counts: (m.counts ?? []).map((c) => c.id),
|
|
1266
|
+
sums: (m.sums ?? []).map((s) => s.id),
|
|
1056
1267
|
identityHash: identity.hashes[m.domain ?? m.name],
|
|
1057
1268
|
})),
|
|
1058
1269
|
};
|
|
@@ -1062,6 +1273,10 @@ async function main(): Promise<void> {
|
|
|
1062
1273
|
readManifest,
|
|
1063
1274
|
identityManifests: identity.manifests,
|
|
1064
1275
|
identityHashes: identity.hashes,
|
|
1276
|
+
// The capability lowering (unhashed sidecar; omit-when-empty — see the
|
|
1277
|
+
// collector's doc above). The worker stages it on the manifest overlay:
|
|
1278
|
+
// the deploy-time binding warning + the executor's scan map read it there.
|
|
1279
|
+
...(capabilityLowerings.length > 0 ? { capabilities: capabilityLowerings } : {}),
|
|
1065
1280
|
};
|
|
1066
1281
|
const deployPath = path.join(outDir, `${cfg.name}.deploy.json`);
|
|
1067
1282
|
writeFileSync(deployPath, JSON.stringify(deployBody) + "\n", "utf8");
|
|
@@ -1072,10 +1287,20 @@ async function main(): Promise<void> {
|
|
|
1072
1287
|
// synthesizer can't sample yet still compiles — the skip names its remedy.
|
|
1073
1288
|
let proofPath: string | undefined;
|
|
1074
1289
|
let proofSkip: string | undefined;
|
|
1290
|
+
let proofDomains: string[] = [];
|
|
1075
1291
|
try {
|
|
1076
1292
|
const proofSrc = generateTsProof(domainModules, { packageName: cfg.name, domainHash });
|
|
1077
1293
|
proofPath = path.join(outDir, `${cfg.name}.proof.mts`);
|
|
1078
1294
|
writeFileSync(proofPath, proofSrc, "utf8");
|
|
1295
|
+
// THE PER-DOMAIN LEGS INDEX: every domain with a creating write, so
|
|
1296
|
+
// `githolon proof --domain <key>` proves ANY of them offline (the .mts
|
|
1297
|
+
// proves the primary one live). A multi-domain package no longer locks the
|
|
1298
|
+
// proof to whichever domain happens to be first.
|
|
1299
|
+
const legsIndex = proofLegsIndex(domainModules);
|
|
1300
|
+
if (legsIndex !== null) {
|
|
1301
|
+
writeFileSync(path.join(outDir, `${cfg.name}.proof-legs.json`), JSON.stringify(legsIndex, null, 2) + "\n", "utf8");
|
|
1302
|
+
proofDomains = Object.keys(legsIndex.domains);
|
|
1303
|
+
}
|
|
1079
1304
|
} catch (e) {
|
|
1080
1305
|
proofSkip = (e as Error).message;
|
|
1081
1306
|
}
|
|
@@ -1087,7 +1312,7 @@ async function main(): Promise<void> {
|
|
|
1087
1312
|
`domain '${d.domain}'${d.identityHash ? ` (identity ${d.identityHash.slice(0, 12)}…)` : " (EXCLUDED from identity manifest)"}`,
|
|
1088
1313
|
` aggregates: ${d.aggregates.join(", ") || "—"}`,
|
|
1089
1314
|
` directives: ${d.directives.join(", ") || "—"}`,
|
|
1090
|
-
` queries: ${d.queries.join(", ") || "—"} counts: ${d.counts.join(", ") || "—"}`,
|
|
1315
|
+
` queries: ${d.queries.join(", ") || "—"} counts: ${d.counts.join(", ") || "—"} sums: ${d.sums.join(", ") || "—"}`,
|
|
1091
1316
|
]),
|
|
1092
1317
|
`deploy: POST ${cfg.name}.deploy.json to /v1/workspaces/<ws>/domains (or: githolon deploy <ws>)`,
|
|
1093
1318
|
`typed client: ${cfg.name}.client.ts — ${tsClientFactoryName(domainModules[0]?.domain ?? domainModules[0]?.name ?? cfg.name)}(holon), law hash baked in`,
|
|
@@ -1129,10 +1354,21 @@ async function main(): Promise<void> {
|
|
|
1129
1354
|
` layered ${rel(layeredRootPath)} (${layeredFileCount} module layer(s) in build/layers/ — flatten-verified byte-identical to the canonical IR)`,
|
|
1130
1355
|
);
|
|
1131
1356
|
}
|
|
1132
|
-
console.log(` read ${rel(readPath)} (${Object.keys(readManifest.aggregateFieldKinds).length} aggregate(s), ${readManifest.queries.length} quer(y/ies), ${readManifest.counts.length} count(s))`);
|
|
1357
|
+
console.log(` read ${rel(readPath)} (${Object.keys(readManifest.aggregateFieldKinds).length} aggregate(s), ${readManifest.queries.length} quer(y/ies), ${readManifest.counts.length} count(s), ${readManifest.sums?.length ?? 0} sum(s))`);
|
|
1133
1358
|
console.log(` identity ${rel(manifestsPath)} (${Object.keys(identity.manifests).length} domain(s)${identity.excluded.length ? `, ${identity.excluded.length} EXCLUDED (palette gap)` : ""})`);
|
|
1134
1359
|
for (const [dom, h] of Object.entries(identity.hashes)) console.log(` ${dom.padEnd(20)} ${h}`);
|
|
1135
1360
|
for (const ex of identity.excluded) console.log(` EXCLUDED ${ex.domain}: ${ex.reason}`);
|
|
1361
|
+
// STABLE IDS (#58): the minted/carried identity surface + every INFERRED rename —
|
|
1362
|
+
// the silent lane made visible (if an inference is wrong — you meant remove+add —
|
|
1363
|
+
// split the edit into two compiles, or answer the evolve gate with a disposition).
|
|
1364
|
+
console.log(` stable-ids ${rel(lockPath)} (committed lockfile — identity across renames)`);
|
|
1365
|
+
for (const { domain, renames } of inferredRenames) {
|
|
1366
|
+
for (const r of renames) {
|
|
1367
|
+
console.log(
|
|
1368
|
+
` INFERRED RENAME [${domain}] ${r.from} -> ${r.to} (stable id ${r.sid} carried; a rename is metadata — same identity, new label)`,
|
|
1369
|
+
);
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1136
1372
|
for (const m of domainModules) {
|
|
1137
1373
|
if (m.workspaceTypes !== undefined && m.workspaceTypes.length > 0) {
|
|
1138
1374
|
console.log(
|
|
@@ -1156,7 +1392,12 @@ async function main(): Promise<void> {
|
|
|
1156
1392
|
console.log(` # raw lane: curl -X POST -H 'content-type: application/json' -H 'Authorization: Bearer <workspaceSecret>' \\`);
|
|
1157
1393
|
console.log(` # --data-binary @${rel(deployPath)} https://nomos.captainapp.co.uk/v1/workspaces/<ws>/domains`);
|
|
1158
1394
|
if (proofPath !== undefined) {
|
|
1159
|
-
console.log(`prove it: npx githolon proof
|
|
1395
|
+
console.log(`prove it OFFLINE first: npx githolon proof # the generated proof's offline legs on a LOCAL holon (no cloud workspace)`);
|
|
1396
|
+
console.log(` inner loop: npx githolon dev # watch -> recompile -> law live locally -> offline proof, on every save`);
|
|
1397
|
+
console.log(` then the cloud loop: npx githolon proof --live # throwaway workspace, retired on exit (--keep keeps it)`);
|
|
1398
|
+
if (proofDomains.length > 1) {
|
|
1399
|
+
console.log(` this package proves ${proofDomains.length} domains (${proofDomains.join(", ")}) — default '${proofDomains[0]}'; pick another: npx githolon proof --domain <key>`);
|
|
1400
|
+
}
|
|
1160
1401
|
} else {
|
|
1161
1402
|
console.log(`prove it: npm run e2e # offline write -> sync -> admission -> cloud query, live`);
|
|
1162
1403
|
}
|
package/src/directive.ts
CHANGED
|
@@ -21,6 +21,7 @@ import type { PlannedOp } from "./ops.js";
|
|
|
21
21
|
import type { Ports } from "./ctx.js";
|
|
22
22
|
import type { CertifiedReadDecl } from "./certified_read.js";
|
|
23
23
|
import type { RelationDecl } from "./relation.js";
|
|
24
|
+
import type { QueryDecl } from "./query.js";
|
|
24
25
|
|
|
25
26
|
export type ReferentialMarker = "creates" | "mutates" | "ensures" | "archives";
|
|
26
27
|
|
|
@@ -70,6 +71,18 @@ export interface Directive<P = unknown> {
|
|
|
70
71
|
* (read ⊆ declared) is a SEPARATE later step; here it only DECLARES.
|
|
71
72
|
*/
|
|
72
73
|
readonly declaredReads: string[];
|
|
74
|
+
/**
|
|
75
|
+
* The directive's DECLARED CAPTURED-READ queries (#58 — the captured-read lane):
|
|
76
|
+
* the O(1) DSL-defined queries its `plan` may `read()` on the write path. Each is a
|
|
77
|
+
* declared, indexed {@link QueryDecl}; the gate serves `nomos.read` LIVE from the
|
|
78
|
+
* pre-apply committed state at author, replays the captured result at verify, and
|
|
79
|
+
* RE-DERIVES it at the pre-apply position for the read-conflict (CAS-as-law) check.
|
|
80
|
+
* Same-workspace only, size-bounded. Defaults to `[]` (no captured reads — the
|
|
81
|
+
* engine then provides no `nomos.read`, exactly the pre-#58 posture). OPTIONAL +
|
|
82
|
+
* ADDITIVE (a hand-built directive object without it is a read-free directive);
|
|
83
|
+
* OMITTED from the canonical manifest when empty (hash-stable for read-free directives).
|
|
84
|
+
*/
|
|
85
|
+
readonly declaredQueryReads?: QueryDecl[];
|
|
73
86
|
/**
|
|
74
87
|
* The directive's DECLARED emit boundary: the event types its `plan` may EMIT,
|
|
75
88
|
* each with an optional `max` bound. Part of the domain identity. Defaults to `{}`
|
|
@@ -130,13 +143,18 @@ export interface RequirableDirective<P> extends Directive<P> {
|
|
|
130
143
|
*/
|
|
131
144
|
requires(capability?: string, scopeFrom?: ScopeFrom<P>): RequirableDirective<P>;
|
|
132
145
|
/**
|
|
133
|
-
* Declare the directive's READ boundary
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
146
|
+
* Declare the directive's READ boundary. TWO argument kinds, one declaration site:
|
|
147
|
+
* * a STRING declares a ref TYPE its `plan` may read (the original boundary —
|
|
148
|
+
* deduped + sorted on `declaredReads`);
|
|
149
|
+
* * a {@link QueryDecl} declares a CAPTURED-READ query (#58): an O(1) DSL query
|
|
150
|
+
* its `plan` may `read()` on the write path — served live at author from the
|
|
151
|
+
* pre-apply state, captured onto the intent, replayed + CAS-re-derived at every
|
|
152
|
+
* later gate (deduped by query id on `declaredQueryReads`).
|
|
153
|
+
* Callable MULTIPLE times to accumulate. Purely additive — a directive that never
|
|
154
|
+
* calls this declares no reads and is byte-identical in the canonical manifest to
|
|
155
|
+
* before this existed. Returns a NEW requirable directive (non-destructive).
|
|
138
156
|
*/
|
|
139
|
-
reads(...
|
|
157
|
+
reads(...reads: (string | QueryDecl)[]): RequirableDirective<P>;
|
|
140
158
|
/**
|
|
141
159
|
* Declare ONE entry of the directive's EMIT boundary: an `eventType` its `plan` may
|
|
142
160
|
* emit, with an optional `max` count bound. Callable MULTIPLE times to accumulate
|
|
@@ -239,6 +257,7 @@ class DirectivePlanStep<Id extends string, P> {
|
|
|
239
257
|
payloadSchema: this.payloadSchema,
|
|
240
258
|
plan: fn,
|
|
241
259
|
declaredReads: [],
|
|
260
|
+
declaredQueryReads: [],
|
|
242
261
|
declaredEmits: {},
|
|
243
262
|
declaredCertifiedReads: {},
|
|
244
263
|
declaredRelations: [],
|
|
@@ -263,11 +282,17 @@ function makeRequirable<P>(base: Directive<P>): RequirableDirective<P> {
|
|
|
263
282
|
...(scopeFrom !== undefined ? { scopeFrom } : {}),
|
|
264
283
|
});
|
|
265
284
|
},
|
|
266
|
-
reads(...
|
|
267
|
-
//
|
|
268
|
-
//
|
|
285
|
+
reads(...reads: (string | QueryDecl)[]): RequirableDirective<P> {
|
|
286
|
+
// Split by argument kind: strings are the ref-type boundary; QueryDecls are the
|
|
287
|
+
// captured-read lane (#58). Both accumulate, dedup (ref types by value, queries
|
|
288
|
+
// by id — later decl wins), sort — only the SET is the contract.
|
|
289
|
+
const refTypes = reads.filter((r): r is string => typeof r === "string");
|
|
290
|
+
const queryDecls = reads.filter((r): r is QueryDecl => typeof r !== "string");
|
|
269
291
|
const merged = [...new Set([...base.declaredReads, ...refTypes])].sort();
|
|
270
|
-
|
|
292
|
+
const byId = new Map<string, QueryDecl>((base.declaredQueryReads ?? []).map((q) => [q.id, q]));
|
|
293
|
+
for (const q of queryDecls) byId.set(q.id, q);
|
|
294
|
+
const mergedQueries = [...byId.values()].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
295
|
+
return makeRequirable({ ...base, declaredReads: merged, declaredQueryReads: mergedQueries });
|
|
271
296
|
},
|
|
272
297
|
emits(eventType: string, opts?: { max?: number }): RequirableDirective<P> {
|
|
273
298
|
// Accumulate one event type onto any prior emits; a `max` bound is recorded only
|
package/src/engine_entry.ts
CHANGED
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
* tenant package for the long-form rationale of each branch.
|
|
53
53
|
*/
|
|
54
54
|
import { executeDirectiveToIntent } from "./wire_encode.js";
|
|
55
|
+
import { expandCapabilityExports } from "./capability_exports.js";
|
|
55
56
|
import type { Directive } from "./directive.js";
|
|
56
57
|
import type { AggregateHandle, AggregateInvariantFn, AggregateInvariantVerdict } from "./aggregate.js";
|
|
57
58
|
import type { Ports } from "./ctx.js";
|
|
@@ -200,7 +201,10 @@ function combinedsOf(mod: DomainModuleExports): CombinedDecl[] {
|
|
|
200
201
|
function mergeModules(mods: readonly DomainModuleExports[]): DomainModuleExports {
|
|
201
202
|
let merged: DomainModuleExports = {};
|
|
202
203
|
for (const m of mods) merged = { ...merged, ...m };
|
|
203
|
-
|
|
204
|
+
// `capability()` bundles expand into discoverable entries here — the engine
|
|
205
|
+
// plane's ONE merge point, so the registry sees the task aggregate + quartet
|
|
206
|
+
// without per-piece re-exports (capability_exports.ts).
|
|
207
|
+
return expandCapabilityExports(merged);
|
|
204
208
|
}
|
|
205
209
|
|
|
206
210
|
/**
|