@githolon/dsl 0.53.0 → 0.54.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/compat_package.mjs +46 -0
- package/dart/lib/src/webview_bridge.dart +160 -13
- package/dart/pubspec.yaml +1 -1
- package/dart/test/co2_acceptance_test.dart +102 -0
- package/dart/test/compat_test.dart +61 -0
- package/dart/test/vo_read_typing_test.dart +113 -0
- package/package.json +2 -1
- package/src/authoring.ts +77 -17
- package/src/codegen_dart.ts +94 -12
- package/src/codegen_dart_client.ts +18 -1
- package/src/codegen_openapi.ts +1 -1
- package/src/codegen_proof.ts +113 -16
- package/src/codegen_ts.ts +87 -12
- package/src/compat_package_main.ts +187 -0
- package/src/compile_package_main.ts +11 -1
- package/src/fields.ts +55 -9
- package/src/index.ts +14 -0
- package/src/law_check.ts +132 -1
- package/src/ops.ts +25 -6
- package/src/path.ts +115 -0
- package/src/query.ts +18 -9
- package/src/shape_compat.ts +46 -0
- package/src/spatial.ts +8 -4
- package/src/value_object.ts +119 -18
- package/src/wire_encode.ts +57 -9
package/src/codegen_ts.ts
CHANGED
|
@@ -55,6 +55,14 @@ const camel = (s: string) => s.replace(/[_\s-]+(\w)/g, (_m, c: string) => c.toUp
|
|
|
55
55
|
const lcFirst = (s: string) => (s.length ? s[0]!.toLowerCase() + s.slice(1) : s);
|
|
56
56
|
const upperSnake = (s: string) => s.replace(/[-\s]+/g, "_").replace(/([a-z0-9])([A-Z])/g, "$1_$2").toUpperCase();
|
|
57
57
|
|
|
58
|
+
/** A count/sum group-by field → a valid TS param identifier. A DOTTED group-by (`location.source`,
|
|
59
|
+
* the #30 follow-up) camel-joins into `locationSource` (a bare `location.source` is not a legal
|
|
60
|
+
* positional param name); a flat field is unchanged. Mirrors the Dart `dartKeyParam`. */
|
|
61
|
+
const tsGroupParam = (by: string) => {
|
|
62
|
+
const parts = by.split(".").filter((p) => p !== "");
|
|
63
|
+
return parts.length <= 1 ? by : parts[0]! + parts.slice(1).map((p) => p[0]!.toUpperCase() + p.slice(1)).join("");
|
|
64
|
+
};
|
|
65
|
+
|
|
58
66
|
// ── NAME NORMALIZATION (no naming convention to learn) ────────────────────────────
|
|
59
67
|
// `nomos.package.mjs` names and domain keys may be ANY string — kebab-case,
|
|
60
68
|
// snake_case, space-separated, PascalCase. Artifact FILENAMES keep the raw name
|
|
@@ -381,8 +389,11 @@ function tsTypeOfField(field: Field): string {
|
|
|
381
389
|
case "set":
|
|
382
390
|
return "string[]"; // `t.set(t.string())` is the one set shape (fields.ts)
|
|
383
391
|
case "map": {
|
|
392
|
+
// A map of a NAMED value object (`t.map(VO.field())`) types each entry as the VO
|
|
393
|
+
// interface; scalar-valued maps keep their primitive value type.
|
|
384
394
|
const v =
|
|
385
|
-
field.
|
|
395
|
+
field.mapValueVo !== undefined ? field.mapValueVo.name
|
|
396
|
+
: field.mapValueKind === "string" ? "string"
|
|
386
397
|
: field.mapValueKind === "int" ? "number"
|
|
387
398
|
: "unknown";
|
|
388
399
|
return `Record<string, ${v}>`;
|
|
@@ -399,13 +410,22 @@ const geometryCoordsTs: Record<string, string> = {
|
|
|
399
410
|
Point: "number[]",
|
|
400
411
|
LineString: "number[][]",
|
|
401
412
|
Polygon: "number[][][]",
|
|
413
|
+
MultiPoint: "number[][]",
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
/** The `geo.*` factory name for a geometry kind (used in generated doc comments). */
|
|
417
|
+
const geometryFactoryName: Record<string, string> = {
|
|
418
|
+
Point: "point",
|
|
419
|
+
LineString: "lineString",
|
|
420
|
+
Polygon: "polygon",
|
|
421
|
+
MultiPoint: "multiPoint",
|
|
402
422
|
};
|
|
403
423
|
|
|
404
424
|
/** One descriptor field → its TS type text. NO FALLBACK (the descriptor set is closed). */
|
|
405
425
|
function tsTypeOfVoField(f: ValueObjectFieldDescriptor): string {
|
|
406
426
|
switch (f.type) {
|
|
407
427
|
case "string":
|
|
408
|
-
return "string";
|
|
428
|
+
return f.brand ?? "string"; // a brandedId(...) → the emitted branded alias
|
|
409
429
|
case "number":
|
|
410
430
|
return "number"; // `.int()`/units are Dart-side nominal products; TS keeps one number
|
|
411
431
|
case "boolean":
|
|
@@ -416,18 +436,36 @@ function tsTypeOfVoField(f: ValueObjectFieldDescriptor): string {
|
|
|
416
436
|
return JSON.stringify(f.value);
|
|
417
437
|
case "array":
|
|
418
438
|
return `${paren(tsTypeOfVoField(f.items))}[]`;
|
|
439
|
+
case "map":
|
|
440
|
+
return `Record<string, ${tsTypeOfVoField(f.values)}>`;
|
|
419
441
|
case "object":
|
|
420
442
|
case "geometry":
|
|
421
443
|
return f.ref; // the named VO interface (emitted alongside)
|
|
422
444
|
}
|
|
423
445
|
}
|
|
424
446
|
|
|
447
|
+
/** Every branded-id name a VO descriptor carries (recursing arrays/maps/union arms) — one
|
|
448
|
+
* `export type <Brand> = string & {…}` alias is emitted per distinct brand. */
|
|
449
|
+
function voBrandNames(d: ValueObjectDescriptor): Set<string> {
|
|
450
|
+
const brands = new Set<string>();
|
|
451
|
+
const walk = (f: ValueObjectFieldDescriptor): void => {
|
|
452
|
+
if (f.type === "string" && f.brand !== undefined) brands.add(f.brand);
|
|
453
|
+
else if (f.type === "array") walk(f.items);
|
|
454
|
+
else if (f.type === "map") walk(f.values);
|
|
455
|
+
};
|
|
456
|
+
for (const f of Object.values(d.fields ?? {})) walk(f);
|
|
457
|
+
for (const arm of Object.values(d.union ?? {})) {
|
|
458
|
+
if ("fields" in arm) for (const f of Object.values(arm.fields)) walk(f);
|
|
459
|
+
}
|
|
460
|
+
return brands;
|
|
461
|
+
}
|
|
462
|
+
|
|
425
463
|
/** Emit one `export interface <VO> { ... }` from its canonical descriptor. A geo VO
|
|
426
464
|
* emits the fixed RFC 7946 shape (`{ type: "<kind>"; coordinates: ... }`). */
|
|
427
465
|
function emitValueObjectTs(d: ValueObjectDescriptor): string[] {
|
|
428
466
|
if (d.geometry !== undefined) {
|
|
429
467
|
return [
|
|
430
|
-
`/** RFC 7946 ${d.geometry} — a \`geo.${d.geometry
|
|
468
|
+
`/** RFC 7946 ${d.geometry} — a \`geo.${geometryFactoryName[d.geometry]}(...)\` value object. */`,
|
|
431
469
|
`export interface ${d.name} { type: ${JSON.stringify(d.geometry)}; coordinates: ${geometryCoordsTs[d.geometry]}; }`,
|
|
432
470
|
``,
|
|
433
471
|
];
|
|
@@ -596,7 +634,7 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
596
634
|
` count(countId: string, groupKey?: string): Promise<number>;`,
|
|
597
635
|
` /** THE SPATIAL READ (R*Tree probe): rows intersecting the bbox — same row shape as query().`,
|
|
598
636
|
` * OPTIONAL member: structural binding tolerates a pre-spatial holon (the accessor then throws at call). */`,
|
|
599
|
-
` spatialWithin?(spatialId: string, bbox: { minLng: number; minLat: number; maxLng: number; maxLat: number }): Promise<unknown>;`,
|
|
637
|
+
` spatialWithin?(spatialId: string, bbox: { minX: number; minY: number; maxX: number; maxY: number } | { minLng: number; minLat: number; maxLng: number; maxLat: number }): Promise<unknown>;`,
|
|
600
638
|
` sum(sumId: string, groupKey?: string): Promise<number>;`,
|
|
601
639
|
...(anyPackedTaxonomy(modules)
|
|
602
640
|
? [
|
|
@@ -609,10 +647,33 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
609
647
|
` deadLetters(): Promise<{ id?: string; oid: string; source: string; error: string; domain?: string | null; directiveId?: string | null; at: string }[]>;`,
|
|
610
648
|
` retryDeadLetter(id: string): Promise<{ ok: boolean; head?: string; alreadyOnMain?: boolean; error?: string }>;`,
|
|
611
649
|
` discardDeadLetter(id: string): Promise<{ ok: boolean; removed: number }>;`,
|
|
612
|
-
` /** THE
|
|
613
|
-
` *
|
|
614
|
-
` *
|
|
615
|
-
`
|
|
650
|
+
` /** THE COMPAT SUPERPOWER (#72b) — OPTIONAL: a holon from @githolon/client's connect() attaches it.`,
|
|
651
|
+
` * The factory hands over this client's baked compat manifest + structural interface (the requirement),`,
|
|
652
|
+
` * and the holon computes the LIVE per-member verdict on \`holon.compat\`. A KNOWN-incompatible directive`,
|
|
653
|
+
` * then refuses CLIENT-SIDE at write time (LawMismatchError), never as a server 422. */`,
|
|
654
|
+
` checkCompat?(compat: { lawHash: string; interfaceHash: string; packageName: string }, requirement?: unknown): NomosCompatVerdict | null;`,
|
|
655
|
+
` /** The live compat verdict (set by checkCompat). \`has(memberId)\` is the dark-launch feature flag;`,
|
|
656
|
+
` * \`refresh()\` re-derives against the current projection. Absent until the factory binds. */`,
|
|
657
|
+
` compat?: NomosCompatVerdict;`,
|
|
658
|
+
` /** Subscribe to holon events. \`lawChanged\` fires when a synced install/upgrade/retire moves the`,
|
|
659
|
+
` * workspace's offer under this live client (the compat verdict re-derived). Returns unsubscribe. */`,
|
|
660
|
+
` on?(event: "lawChanged", cb: (compat: NomosCompatVerdict) => void): () => void;`,
|
|
661
|
+
`}`,
|
|
662
|
+
``,
|
|
663
|
+
`/** THE LIVE COMPAT VERDICT (#72b) stashed on \`holon.compat\`. \`verdict\` is the three-state roll-up;`,
|
|
664
|
+
` * \`members\` names each directive/aggregate's own \`{ ok, reason? }\`; \`install\` is the deployed law's`,
|
|
665
|
+
` * lifecycle phase. \`has(id)\` = the feature flag; \`refresh()\` re-evaluates against the projection. */`,
|
|
666
|
+
`export interface NomosCompatVerdict {`,
|
|
667
|
+
` generatedFor: string;`,
|
|
668
|
+
` workspaceRuns: string[];`,
|
|
669
|
+
` matches: boolean;`,
|
|
670
|
+
` offerKnown: boolean;`,
|
|
671
|
+
` structurallyCompatible: boolean | null;`,
|
|
672
|
+
` verdict: "compatible" | "degraded" | "incompatible";`,
|
|
673
|
+
` members: Record<string, { ok: boolean; reason?: string }>;`,
|
|
674
|
+
` install: { phase: string; supersededBy?: string };`,
|
|
675
|
+
` has(memberId: string): boolean;`,
|
|
676
|
+
` refresh(): Promise<NomosCompatVerdict | null>;`,
|
|
616
677
|
`}`,
|
|
617
678
|
``,
|
|
618
679
|
`/** AUTHORITY IS UNFORGEABLE-BY-CONSTRUCTION: a GATED directive (one carrying a \`.requires(relation)\` write`,
|
|
@@ -944,6 +1005,14 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
944
1005
|
}
|
|
945
1006
|
if (voByName.size > 0) {
|
|
946
1007
|
out.push(`// ── value objects (named, composable json-object leaves — value_object.ts) ──`, ``);
|
|
1008
|
+
// BRANDED-ID aliases (brandedId(...)) — one nominal `string & { __brand }` per distinct
|
|
1009
|
+
// brand any VO carries; wire-identical to a string, a compile error to cross-assign.
|
|
1010
|
+
const brands = new Set<string>();
|
|
1011
|
+
for (const [, d] of voByName) for (const b of voBrandNames(d)) brands.add(b);
|
|
1012
|
+
for (const b of [...brands].sort()) {
|
|
1013
|
+
out.push(`/** Branded id \`${b}\` — a nominal string (\`brandedId("${b}")\`); wire-identical to a string. */`);
|
|
1014
|
+
out.push(`export type ${b} = string & { readonly __brand: "${b}" };`, ``);
|
|
1015
|
+
}
|
|
947
1016
|
for (const [, d] of [...voByName].sort(([a], [b]) => a.localeCompare(b))) {
|
|
948
1017
|
out.push(...emitValueObjectTs(d));
|
|
949
1018
|
}
|
|
@@ -1290,7 +1359,7 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
1290
1359
|
for (const sp of (mod.spatials ?? []) as { id: string; of: string; on: string }[]) {
|
|
1291
1360
|
methods.push(
|
|
1292
1361
|
` /** Declared spatial index \`${sp.id}\` — rows of \`${sp.of}\` whose \`${sp.on}\` geometry intersects the bbox (R*Tree probe). */`,
|
|
1293
|
-
` ${lcFirst(camel(sp.id))}: (bbox: { minLng: number; minLat: number; maxLng: number; maxLat: number }) => (holon.spatialWithin ?? (() => { throw new Error("this holon predates spatial reads — upgrade @githolon/client"); }))(${JSON.stringify(sp.id)}, bbox) as Promise<Row<${sp.of}Data>[]>,`,
|
|
1362
|
+
` ${lcFirst(camel(sp.id))}: (bbox: { minX: number; minY: number; maxX: number; maxY: number } | { minLng: number; minLat: number; maxLng: number; maxLat: number }) => (holon.spatialWithin ?? (() => { throw new Error("this holon predates spatial reads — upgrade @githolon/client"); }))(${JSON.stringify(sp.id)}, bbox) as Promise<Row<${sp.of}Data>[]>,`,
|
|
1294
1363
|
);
|
|
1295
1364
|
}
|
|
1296
1365
|
for (const q of (mod.queries ?? []) as QueryDecl[]) {
|
|
@@ -1326,19 +1395,21 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
1326
1395
|
|
|
1327
1396
|
for (const c of (mod.counts ?? []).map((raw) => finishCount(raw))) {
|
|
1328
1397
|
const grouped = c.by != null;
|
|
1398
|
+
const p = grouped ? tsGroupParam(c.by!) : "";
|
|
1329
1399
|
methods.push(
|
|
1330
1400
|
` /** Declared count \`${c.id}\` — the maintained O(1) ${grouped ? `tally per \`${c.by}\`` : "grand total"}. */`,
|
|
1331
1401
|
grouped
|
|
1332
|
-
? ` ${lcFirst(camel(c.id))}: (${
|
|
1402
|
+
? ` ${lcFirst(camel(c.id))}: (${p}: string) => holon.count(${JSON.stringify(c.id)}, ${p}),`
|
|
1333
1403
|
: ` ${lcFirst(camel(c.id))}: () => holon.count(${JSON.stringify(c.id)}),`,
|
|
1334
1404
|
);
|
|
1335
1405
|
}
|
|
1336
1406
|
for (const s of (mod.sums ?? []).map((raw) => finishSum(raw))) {
|
|
1337
1407
|
const grouped = s.by != null;
|
|
1408
|
+
const p = grouped ? tsGroupParam(s.by!) : "";
|
|
1338
1409
|
methods.push(
|
|
1339
1410
|
` /** Declared sum \`${s.id}\` — the maintained O(1) ${grouped ? `total per \`${s.by}\`` : "grand total"}. */`,
|
|
1340
1411
|
grouped
|
|
1341
|
-
? ` ${lcFirst(camel(s.id))}: (${
|
|
1412
|
+
? ` ${lcFirst(camel(s.id))}: (${p}: string) => holon.sum(${JSON.stringify(s.id)}, ${p}),`
|
|
1342
1413
|
: ` ${lcFirst(camel(s.id))}: () => holon.sum(${JSON.stringify(s.id)}),`,
|
|
1343
1414
|
);
|
|
1344
1415
|
}
|
|
@@ -1369,7 +1440,11 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
1369
1440
|
` const actor = opts.actor;`,
|
|
1370
1441
|
...(encPresent ? [` const keyStore = opts.keyStore;`] : []),
|
|
1371
1442
|
...(opts.compat !== undefined
|
|
1372
|
-
? [
|
|
1443
|
+
? [
|
|
1444
|
+
opts.interface !== undefined
|
|
1445
|
+
? ` holon.checkCompat?.(${compatConstName(opts.packageName)}, ${interfaceConstName(opts.packageName)});`
|
|
1446
|
+
: ` holon.checkCompat?.(${compatConstName(opts.packageName)});`,
|
|
1447
|
+
]
|
|
1373
1448
|
: []),
|
|
1374
1449
|
` return {`,
|
|
1375
1450
|
` domain: ${JSON.stringify(domain)} as const,`,
|
|
@@ -0,0 +1,187 @@
|
|
|
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. If a file isn't this / hosting this / authoring for this / proving this — it's gone.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `githolon compat <target> [--config …] [--json]` — THE CI GATE (task #31, the compat superpower).
|
|
7
|
+
*
|
|
8
|
+
* Compares the CURRENT build's structural interface (`build/<name>.interface.json`, the client's
|
|
9
|
+
* REQUIREMENT) against a TARGET's installed law (the workspace OFFER), PER MEMBER, via the #72
|
|
10
|
+
* subtype relation (`memberVerdicts`). Exits NON-ZERO on any incompatible member — so a client/law
|
|
11
|
+
* mismatch is caught in CI, before it ships. The target is:
|
|
12
|
+
* • a deploy.json (`build/<name>.deploy.json`) — its staged `.interface` is the offer;
|
|
13
|
+
* • an `.interface.json` (or a build/package DIR containing one) — the offer directly;
|
|
14
|
+
* • a LIVE workspace URL (`https://host/v2/workspaces/<ws>` or `https://host --workspace <ws>`) —
|
|
15
|
+
* connect()s and reads the on-ledger offer (`holon.interfaceOffer()`).
|
|
16
|
+
*
|
|
17
|
+
* Human report by default; `--json` emits the machine surface (the per-member array + verdict).
|
|
18
|
+
*/
|
|
19
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
20
|
+
import path from "node:path";
|
|
21
|
+
|
|
22
|
+
import { memberVerdicts, type DomainInterface, type MemberVerdict } from "./shape_compat.js";
|
|
23
|
+
|
|
24
|
+
interface PackageConfig {
|
|
25
|
+
name: string;
|
|
26
|
+
}
|
|
27
|
+
type InterfaceMap = Record<string, DomainInterface>;
|
|
28
|
+
|
|
29
|
+
function fail(msg: string): never {
|
|
30
|
+
console.error(`githolon compat: ${msg}`);
|
|
31
|
+
process.exit(2);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Merge a per-domain interface map into ONE flat interface (directives + aggregates unioned). */
|
|
35
|
+
function mergeInterfaces(byDomain: InterfaceMap | undefined): DomainInterface {
|
|
36
|
+
const out: DomainInterface = { v: 1, domain: "*", directives: {}, aggregates: {} };
|
|
37
|
+
for (const di of Object.values(byDomain ?? {})) {
|
|
38
|
+
if (!di || typeof di !== "object") continue;
|
|
39
|
+
Object.assign(out.directives, di.directives ?? {});
|
|
40
|
+
Object.assign(out.aggregates, di.aggregates ?? {});
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The package name from `nomos.package.mjs` (only field we need — to locate `build/<name>.interface.json`). */
|
|
46
|
+
async function loadPackageName(configArg: string): Promise<{ name: string; dir: string }> {
|
|
47
|
+
const configPath = path.resolve(process.cwd(), configArg);
|
|
48
|
+
if (!existsSync(configPath)) fail(`config not found: ${configPath} (pass the package config, or run from the package dir)`);
|
|
49
|
+
const { pathToFileURL } = await import("node:url");
|
|
50
|
+
const cfg = ((await import(`${pathToFileURL(configPath).href}?t=${Date.now()}`)) as { default?: PackageConfig }).default;
|
|
51
|
+
if (!cfg || typeof cfg.name !== "string") fail(`config must default-export { name, … }`);
|
|
52
|
+
return { name: cfg.name, dir: path.dirname(configPath) };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The CURRENT build's interface — `build/<name>.interface.json`. Requires a prior `githolon compile`. */
|
|
56
|
+
function loadCurrentInterface(pkgDir: string, name: string): InterfaceMap {
|
|
57
|
+
const p = path.join(pkgDir, "build", `${name}.interface.json`);
|
|
58
|
+
if (!existsSync(p)) {
|
|
59
|
+
fail(
|
|
60
|
+
`no current interface at ${p} — run \`githolon compile\` first so the build carries the structural interface this checks.`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
return JSON.parse(readFileSync(p, "utf8")) as InterfaceMap;
|
|
65
|
+
} catch (e) {
|
|
66
|
+
fail(`could not parse ${p}: ${(e as Error).message}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Read a TARGET offer from a file path (deploy.json / interface.json / dir). Returns `{ offer, source }`. */
|
|
71
|
+
function loadFileOffer(arg: string): { offer: InterfaceMap; source: string } {
|
|
72
|
+
const abs = path.resolve(process.cwd(), arg);
|
|
73
|
+
if (!existsSync(abs)) fail(`target not found: ${abs}`);
|
|
74
|
+
let file = abs;
|
|
75
|
+
if (statSync(abs).isDirectory()) {
|
|
76
|
+
const scan = (d: string): string | undefined => {
|
|
77
|
+
if (!existsSync(d)) return undefined;
|
|
78
|
+
const hit =
|
|
79
|
+
readdirSync(d).find((f) => f.endsWith(".deploy.json")) ?? readdirSync(d).find((f) => f.endsWith(".interface.json"));
|
|
80
|
+
return hit !== undefined ? path.join(d, hit) : undefined;
|
|
81
|
+
};
|
|
82
|
+
const found = scan(abs) ?? scan(path.join(abs, "build"));
|
|
83
|
+
if (found === undefined) fail(`no *.deploy.json or *.interface.json under ${abs}`);
|
|
84
|
+
file = found;
|
|
85
|
+
}
|
|
86
|
+
let parsed: unknown;
|
|
87
|
+
try {
|
|
88
|
+
parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
89
|
+
} catch (e) {
|
|
90
|
+
fail(`could not parse ${file}: ${(e as Error).message}`);
|
|
91
|
+
}
|
|
92
|
+
const p = parsed as Record<string, unknown>;
|
|
93
|
+
// A deploy.json carries `{ …, interface }`; a bare interface.json IS the {[domain]: interface} map.
|
|
94
|
+
const offer = (p.interface !== undefined ? p.interface : p) as InterfaceMap;
|
|
95
|
+
if (offer === null || typeof offer !== "object") fail(`target ${file} carries no structural interface`);
|
|
96
|
+
return { offer, source: file };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Read a LIVE workspace's on-ledger offer via connect() (`@githolon/client`). */
|
|
100
|
+
async function loadLiveOffer(url: string, workspaceFlag?: string): Promise<{ offer: InterfaceMap; source: string }> {
|
|
101
|
+
const m = /^(https?:\/\/[^/]+)(?:\/v2\/workspaces\/([^/?#]+))?/.exec(url);
|
|
102
|
+
if (!m) fail(`could not parse the live target URL: ${url}`);
|
|
103
|
+
const cloud = m[1]!;
|
|
104
|
+
const workspace = workspaceFlag ?? m[2];
|
|
105
|
+
if (!workspace) fail(`live target needs a workspace — use https://host/v2/workspaces/<ws> or add --workspace <ws>`);
|
|
106
|
+
let connect: (opts: { cloud: string; workspace: string }) => Promise<{ interfaceOffer(): Promise<InterfaceMap | null> }>;
|
|
107
|
+
try {
|
|
108
|
+
({ connect } = (await import("@githolon/client")) as unknown as { connect: typeof connect });
|
|
109
|
+
} catch {
|
|
110
|
+
fail(`live target needs @githolon/client installed (it is a dependency of githolon)`);
|
|
111
|
+
}
|
|
112
|
+
const holon = await connect({ cloud, workspace });
|
|
113
|
+
const offer = await holon.interfaceOffer();
|
|
114
|
+
if (offer === null) fail(`workspace '${workspace}' exposes no on-ledger structural interface (pre-#72e law)`);
|
|
115
|
+
return { offer, source: `${cloud}/v2/workspaces/${workspace}` };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
interface MemberReport {
|
|
119
|
+
member: string;
|
|
120
|
+
kind: "directive" | "aggregate";
|
|
121
|
+
ok: boolean;
|
|
122
|
+
reason?: string;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function main(): Promise<void> {
|
|
126
|
+
const argv = process.argv.slice(2);
|
|
127
|
+
const flagVal = (name: string): string | undefined => {
|
|
128
|
+
const i = argv.indexOf(name);
|
|
129
|
+
if (i >= 0) return argv[i + 1];
|
|
130
|
+
const inline = argv.find((a) => a.startsWith(`${name}=`));
|
|
131
|
+
return inline?.slice(name.length + 1);
|
|
132
|
+
};
|
|
133
|
+
const asJson = argv.includes("--json");
|
|
134
|
+
const configArg = flagVal("--config") ?? "nomos.package.mjs";
|
|
135
|
+
const workspaceFlag = flagVal("--workspace");
|
|
136
|
+
// The positional target = the first non-flag arg that isn't a flag VALUE.
|
|
137
|
+
const flagsWithVals = new Set(["--config", "--workspace"]);
|
|
138
|
+
const target = argv.find((a, i) => !a.startsWith("--") && !(i > 0 && flagsWithVals.has(argv[i - 1]!)));
|
|
139
|
+
if (!target) fail(`usage: githolon compat <workspace-url | deploy.json | interface.json | dir> [--config <cfg>] [--workspace <ws>] [--json]`);
|
|
140
|
+
|
|
141
|
+
const { name, dir } = await loadPackageName(configArg);
|
|
142
|
+
const current = loadCurrentInterface(dir, name);
|
|
143
|
+
|
|
144
|
+
const { offer, source } = /^https?:\/\//.test(target)
|
|
145
|
+
? await loadLiveOffer(target, workspaceFlag)
|
|
146
|
+
: loadFileOffer(target);
|
|
147
|
+
|
|
148
|
+
// The subtype check is PER MEMBER over the merged interfaces (directives keyed by id, aggregates by label).
|
|
149
|
+
const verdicts = memberVerdicts(mergeInterfaces(current), mergeInterfaces(offer));
|
|
150
|
+
// Tag each member's kind for the report (a directive id vs an aggregate label).
|
|
151
|
+
const merged = mergeInterfaces(current);
|
|
152
|
+
const dirIds = new Set(Object.keys(merged.directives));
|
|
153
|
+
const reports: MemberReport[] = Object.entries(verdicts)
|
|
154
|
+
.map(([member, v]: [string, MemberVerdict]) => ({
|
|
155
|
+
member,
|
|
156
|
+
kind: (dirIds.has(member) ? "directive" : "aggregate") as MemberReport["kind"],
|
|
157
|
+
ok: v.ok,
|
|
158
|
+
...(v.reason !== undefined ? { reason: v.reason } : {}),
|
|
159
|
+
}))
|
|
160
|
+
.sort((a, b) => (a.member < b.member ? -1 : a.member > b.member ? 1 : 0));
|
|
161
|
+
|
|
162
|
+
const incompatible = reports.filter((r) => !r.ok);
|
|
163
|
+
const verdict = incompatible.length > 0 ? "incompatible" : "compatible";
|
|
164
|
+
|
|
165
|
+
if (asJson) {
|
|
166
|
+
process.stdout.write(JSON.stringify({ package: name, target: source, verdict, members: reports }) + "\n");
|
|
167
|
+
process.exit(incompatible.length > 0 ? 1 : 0);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
console.log(`githolon compat: package '${name}' vs ${source}`);
|
|
171
|
+
console.log(` ${reports.length} member(s) checked — ${verdict.toUpperCase()}`);
|
|
172
|
+
for (const r of reports) {
|
|
173
|
+
const mark = r.ok ? " ✓ " : " ✗ ";
|
|
174
|
+
console.log(`${mark}${r.kind} '${r.member}'${r.ok ? "" : ` — ${r.reason}`}`);
|
|
175
|
+
}
|
|
176
|
+
if (incompatible.length > 0) {
|
|
177
|
+
console.log(
|
|
178
|
+
`\ngitholon compat: ${incompatible.length} incompatible member(s) — this client cannot ship against that law. ` +
|
|
179
|
+
`Recompile + regenerate the client (githolon compile), or deploy a law that still exposes these members.`,
|
|
180
|
+
);
|
|
181
|
+
} else {
|
|
182
|
+
console.log(`\ngitholon compat: all members compatible — safe to ship.`);
|
|
183
|
+
}
|
|
184
|
+
process.exit(incompatible.length > 0 ? 1 : 0);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
void main();
|
|
@@ -1676,6 +1676,7 @@ async function main(): Promise<void> {
|
|
|
1676
1676
|
let proofSkip: string | undefined;
|
|
1677
1677
|
let proofDomains: string[] = [];
|
|
1678
1678
|
let birthWarnings: string[] = [];
|
|
1679
|
+
let proofNotes: string[] = [];
|
|
1679
1680
|
try {
|
|
1680
1681
|
const proofSrc = generateTsProof(domainModules, { packageName: cfg.name, domainHash });
|
|
1681
1682
|
proofPath = path.join(outDir, `${cfg.name}.proof.mts`);
|
|
@@ -1689,6 +1690,9 @@ async function main(): Promise<void> {
|
|
|
1689
1690
|
writeFileSync(path.join(outDir, `${cfg.name}.proof-legs.json`), JSON.stringify(legsIndex, null, 2) + "\n", "utf8");
|
|
1690
1691
|
proofDomains = Object.keys(legsIndex.domains);
|
|
1691
1692
|
birthWarnings = Object.values(legsIndex.domains).flatMap((l) => l.birthDiagnostics ?? []);
|
|
1693
|
+
// TAXONOMY AUDIT: every creating directive not covered by a leg is NAMED-WHY (exact-path synthesis
|
|
1694
|
+
// failure / attestation lane / gated-create authority gap) — never a silent drop.
|
|
1695
|
+
proofNotes = Object.values(legsIndex.domains).flatMap((l) => l.proofDiagnostics ?? []);
|
|
1692
1696
|
}
|
|
1693
1697
|
} catch (e) {
|
|
1694
1698
|
proofSkip = (e as Error).message;
|
|
@@ -1699,7 +1703,10 @@ async function main(): Promise<void> {
|
|
|
1699
1703
|
if (proofSkip !== undefined && birthWarnings.length === 0) {
|
|
1700
1704
|
try {
|
|
1701
1705
|
const idx = proofLegsIndex(domainModules);
|
|
1702
|
-
if (idx !== null)
|
|
1706
|
+
if (idx !== null) {
|
|
1707
|
+
birthWarnings = Object.values(idx.domains).flatMap((l) => l.birthDiagnostics ?? []);
|
|
1708
|
+
proofNotes = Object.values(idx.domains).flatMap((l) => l.proofDiagnostics ?? []);
|
|
1709
|
+
}
|
|
1703
1710
|
} catch { /* diagnostics are best-effort */ }
|
|
1704
1711
|
}
|
|
1705
1712
|
|
|
@@ -1817,6 +1824,9 @@ async function main(): Promise<void> {
|
|
|
1817
1824
|
for (const w of birthWarnings) {
|
|
1818
1825
|
console.log(` ⚠ births ${w}`);
|
|
1819
1826
|
}
|
|
1827
|
+
for (const w of proofNotes) {
|
|
1828
|
+
console.log(` – proof NAMED-WHY (unproven directive): ${w}`);
|
|
1829
|
+
}
|
|
1820
1830
|
// ── THE AUTHOR-TIME GATE (architecture/author_time_gate.md) — the static twins of the kernel
|
|
1821
1831
|
// gate's refusal vocabulary, unified into ONE report. `githolon check` runs the same pass;
|
|
1822
1832
|
// compile runs it implicitly and REFUSES on any refuse-level finding (a statically-provable
|
package/src/fields.ts
CHANGED
|
@@ -19,6 +19,22 @@ import { Conflict, Counter, LastPosition, Lww, type Driver } from "./drivers.js"
|
|
|
19
19
|
import type { AggregateHandle } from "./aggregate.js";
|
|
20
20
|
import type { FieldFacet } from "./principal_evidence.js";
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* A structured JSON value — the AUTHORING type a `json`-kind field (`t.json()`,
|
|
24
|
+
* `t.jsonObject()`, a value-object field) now accepts on the write path. The kernel
|
|
25
|
+
* stores json leaves as `Value::Json` (RFC 8785/JCS), so a plan may `set(...)` a real
|
|
26
|
+
* object/array/scalar (not just a pre-stringified `string`). TYPE-LEVEL ONLY: the wire +
|
|
27
|
+
* read-model type of a json field is unchanged; this only widens what `set`/`setEntry`
|
|
28
|
+
* ACCEPT. (`string` stays a member, so already-stringified callers keep typechecking.)
|
|
29
|
+
*/
|
|
30
|
+
export type JsonValue =
|
|
31
|
+
| string
|
|
32
|
+
| number
|
|
33
|
+
| boolean
|
|
34
|
+
| null
|
|
35
|
+
| JsonValue[]
|
|
36
|
+
| { readonly [k: string]: JsonValue };
|
|
37
|
+
|
|
22
38
|
export type FieldKind =
|
|
23
39
|
| "string"
|
|
24
40
|
| "int"
|
|
@@ -76,9 +92,11 @@ export const evidenceRefSchema: z.ZodType<EvidenceRefValue> = z.object({
|
|
|
76
92
|
// The descriptor types live HERE (dependency-free) so `Field` can carry the metadata
|
|
77
93
|
// without a `fields.ts → value_object.ts` cycle; `value_object.ts` re-exports them.
|
|
78
94
|
|
|
79
|
-
/** The RFC 7946 geometry kinds a `geo.*` value object can declare
|
|
80
|
-
* spatial-index-blessed one; see
|
|
81
|
-
|
|
95
|
+
/** The RFC 7946 geometry kinds a `geo.*` value object can declare. Point is the ONLY
|
|
96
|
+
* spatial-index-blessed one (the kernel R*Tree indexes point bounding boxes; see
|
|
97
|
+
* `spatial.ts`) — LineString/Polygon/MultiPoint round-trip as typed geometry but are
|
|
98
|
+
* NOT yet indexable (non-point R*Tree ingestion is future kernel work). */
|
|
99
|
+
export type GeometryKind = "Point" | "LineString" | "Polygon" | "MultiPoint";
|
|
82
100
|
|
|
83
101
|
/**
|
|
84
102
|
* One field of a value-object DESCRIPTOR — the canonical, JSON-schema-ish shape that
|
|
@@ -88,7 +106,10 @@ export type GeometryKind = "Point" | "LineString" | "Polygon";
|
|
|
88
106
|
* not expressible — the v1 constrained set).
|
|
89
107
|
*/
|
|
90
108
|
export type ValueObjectFieldDescriptor =
|
|
91
|
-
|
|
109
|
+
/** A plain string, OR a BRANDED nominal string when `brand` is present (a `brandedId(...)`
|
|
110
|
+
* — the generated TS branded alias / Dart extension-type wrapper's name). Wire-identical
|
|
111
|
+
* to a plain string; the brand is a compile-time nominal tag only. */
|
|
112
|
+
| { readonly type: "string"; readonly brand?: string; readonly optional?: true }
|
|
92
113
|
/** `int: true` when the zod carried `.int()` (Dart `int`, TS `number`); `unit` when the field
|
|
93
114
|
* was declared through `unit(...)` — the generated Dart extension-type wrapper's name. */
|
|
94
115
|
| { readonly type: "number"; readonly int?: true; readonly unit?: string; readonly optional?: true }
|
|
@@ -97,6 +118,9 @@ export type ValueObjectFieldDescriptor =
|
|
|
97
118
|
/** A `z.literal(<string>)` — a CONSTANT discriminator-style member (generated as a getter). */
|
|
98
119
|
| { readonly type: "literal"; readonly value: string; readonly optional?: true }
|
|
99
120
|
| { readonly type: "array"; readonly items: ValueObjectFieldDescriptor; readonly optional?: true }
|
|
121
|
+
/** A MAP/RECORD (`z.record(z.string(), X)`) — string-keyed entries of `values`. TS
|
|
122
|
+
* `Record<string, V>`, Dart `Map<String, V>`; the wire is a plain json object. */
|
|
123
|
+
| { readonly type: "map"; readonly values: ValueObjectFieldDescriptor; readonly optional?: true }
|
|
100
124
|
/** A nested value object, referenced BY NAME (its own descriptor rides alongside). */
|
|
101
125
|
| { readonly type: "object"; readonly ref: string; readonly optional?: true }
|
|
102
126
|
/** A nested GEO value object (RFC 7946 geometry), referenced by name. */
|
|
@@ -203,6 +227,15 @@ export interface Field<T = unknown, K extends FieldKind = FieldKind> {
|
|
|
203
227
|
* {@link ValueObjectFieldMeta}). `undefined` for every non-VO field.
|
|
204
228
|
*/
|
|
205
229
|
readonly valueObject?: ValueObjectFieldMeta;
|
|
230
|
+
/**
|
|
231
|
+
* For a `t.map(inner)` whose INNER value is a NAMED VALUE OBJECT (`t.map(VO.field())`):
|
|
232
|
+
* the inner VO's metadata, so codegen surfaces the map as `Map<String, VoClass>` /
|
|
233
|
+
* `Record<string, VoClass>` (each entry decoded via `VoClass.fromProjected`) instead of
|
|
234
|
+
* the raw `Map<String, Object?>` / `Record<string, unknown>`. FRONTEND/IR-ONLY (the wire
|
|
235
|
+
* driver is `MapOf(Lww)` exactly as for a plain json-valued map — NO hash change).
|
|
236
|
+
* `undefined` for a non-map field or a map of scalars.
|
|
237
|
+
*/
|
|
238
|
+
readonly mapValueVo?: ValueObjectFieldMeta;
|
|
206
239
|
/**
|
|
207
240
|
* Whether a folded aggregate may LACK this field. A SCALAR field
|
|
208
241
|
* (`string`/`int`/`enum`/`ref`/`json`) that a `.creates` directive does not
|
|
@@ -279,6 +312,7 @@ function makeField<T, K extends FieldKind = FieldKind>(
|
|
|
279
312
|
mapValueKind?: FieldKind;
|
|
280
313
|
jsonShape?: "object";
|
|
281
314
|
valueObject?: ValueObjectFieldMeta;
|
|
315
|
+
mapValueVo?: ValueObjectFieldMeta;
|
|
282
316
|
encrypted?: true;
|
|
283
317
|
meta?: true;
|
|
284
318
|
facet?: FieldFacet;
|
|
@@ -297,6 +331,7 @@ function makeField<T, K extends FieldKind = FieldKind>(
|
|
|
297
331
|
...(extra.mapValueKind !== undefined ? { mapValueKind: extra.mapValueKind } : {}),
|
|
298
332
|
...(extra.jsonShape !== undefined ? { jsonShape: extra.jsonShape } : {}),
|
|
299
333
|
...(extra.valueObject !== undefined ? { valueObject: extra.valueObject } : {}),
|
|
334
|
+
...(extra.mapValueVo !== undefined ? { mapValueVo: extra.mapValueVo } : {}),
|
|
300
335
|
...(extra.encrypted ? { isEncrypted: true as const } : {}),
|
|
301
336
|
...(extra.meta ? { isMeta: true as const } : {}),
|
|
302
337
|
...(extra.fromBirth ? { isFromBirth: true as const } : {}),
|
|
@@ -442,7 +477,13 @@ export const t = {
|
|
|
442
477
|
"map",
|
|
443
478
|
z.record(z.string(), inner.zod),
|
|
444
479
|
{ kind: "MapOf", inner: inner.driver },
|
|
445
|
-
{
|
|
480
|
+
{
|
|
481
|
+
mapValueKind: inner.kind,
|
|
482
|
+
// A map of a NAMED value object (`t.map(VO.field())`) carries the inner VO's
|
|
483
|
+
// metadata so codegen types each entry as the VO class (frontend-only; the wire
|
|
484
|
+
// driver is `MapOf(Lww)` either way — no hash change).
|
|
485
|
+
...(inner.valueObject !== undefined ? { mapValueVo: inner.valueObject } : {}),
|
|
486
|
+
},
|
|
446
487
|
);
|
|
447
488
|
},
|
|
448
489
|
/** Arbitrary JSON-string leaf (used inside maps); encodes as a Str at op time. The
|
|
@@ -450,8 +491,11 @@ export const t = {
|
|
|
450
491
|
* JSON leaf — the kernel `Value` is Str|Int only), so the v1-free projection surfaces
|
|
451
492
|
* it as a permissive `Object?`. Use `t.jsonObject()` when the leaf is ALWAYS an OBJECT
|
|
452
493
|
* value-object (e.g. a `location`) to get a strict `Map<String,Object?>` read type. */
|
|
453
|
-
json(): Field<
|
|
454
|
-
|
|
494
|
+
json(): Field<JsonValue, "json"> {
|
|
495
|
+
// AUTHORING type is the structured JsonValue (the kernel stores Value::Json), so a plan may
|
|
496
|
+
// set()/setEntry() a real object/array/scalar. The runtime wire + zod are UNCHANGED (a `Str`
|
|
497
|
+
// leaf at op time — no hash change); only the accepted TS value widens (a `string` stays valid).
|
|
498
|
+
return makeField<string, "json">("json", z.string(), Lww) as unknown as Field<JsonValue, "json">;
|
|
455
499
|
},
|
|
456
500
|
/** A JSON-OBJECT value-object leaf: an arbitrary JSON-string leaf (identical wire +
|
|
457
501
|
* driver to `t.json()` — a `Str` at op time, so NO hash change) whose decoded value is
|
|
@@ -460,8 +504,10 @@ export const t = {
|
|
|
460
504
|
* `Map<String,Object?>` with a STRICT reader (a present-but-non-object value throws —
|
|
461
505
|
* the strict-decode contract), as opposed to a plain `t.json()` (decoded shape unknown
|
|
462
506
|
* → permissive `Object?`). Marks `jsonShape:"object"`; the kind stays `"json"`. */
|
|
463
|
-
jsonObject(): Field<
|
|
464
|
-
|
|
507
|
+
jsonObject(): Field<JsonValue, "json"> {
|
|
508
|
+
// Same JsonValue authoring widening as `t.json()` (structured value in, `Str` wire out — no hash
|
|
509
|
+
// change); the `jsonShape:"object"` marker only steers the read-model to a strict object reader.
|
|
510
|
+
return makeField<string, "json">("json", z.string(), Lww, { jsonShape: "object" }) as unknown as Field<JsonValue, "json">;
|
|
465
511
|
},
|
|
466
512
|
/**
|
|
467
513
|
* Reference to another aggregate (stored as its id string).
|
package/src/index.ts
CHANGED
|
@@ -30,6 +30,7 @@ export {
|
|
|
30
30
|
type Field,
|
|
31
31
|
type FieldValue,
|
|
32
32
|
type FieldKind,
|
|
33
|
+
type JsonValue,
|
|
33
34
|
type EvidenceRefValue,
|
|
34
35
|
evidenceRefSchema,
|
|
35
36
|
// FIELD FACETS (the endorse model — security_model.md): authority/standing/attribution gate
|
|
@@ -51,6 +52,8 @@ export {
|
|
|
51
52
|
unit,
|
|
52
53
|
idOf,
|
|
53
54
|
idOfMetaOf,
|
|
55
|
+
brandedId,
|
|
56
|
+
brandedIdMetaOf,
|
|
54
57
|
voMetaOf,
|
|
55
58
|
voSample,
|
|
56
59
|
NOMOS_VO_BRAND,
|
|
@@ -65,6 +68,7 @@ export {
|
|
|
65
68
|
type GeoPointValue,
|
|
66
69
|
type GeoLineStringValue,
|
|
67
70
|
type GeoPolygonValue,
|
|
71
|
+
type GeoMultiPointValue,
|
|
68
72
|
type ValueObjectUsage,
|
|
69
73
|
} from "./value_object.js";
|
|
70
74
|
export {
|
|
@@ -201,6 +205,16 @@ export {
|
|
|
201
205
|
type OfSpatial,
|
|
202
206
|
type TypelessSpatial,
|
|
203
207
|
} from "./spatial.js";
|
|
208
|
+
// TYPED PATH HANDLES (#29): `path(Agg).location.point` — a compile-checked field-path reference that
|
|
209
|
+
// collapses to the dotted string, accepted by `spatial(...).on(...)` and `query(...).key(...)`.
|
|
210
|
+
export {
|
|
211
|
+
path,
|
|
212
|
+
isPathRef,
|
|
213
|
+
pathString,
|
|
214
|
+
type PathRef,
|
|
215
|
+
type PathNode,
|
|
216
|
+
type PathRoot,
|
|
217
|
+
} from "./path.js";
|
|
204
218
|
export {
|
|
205
219
|
sum,
|
|
206
220
|
finishSum,
|