@githolon/dsl 0.32.0 → 0.33.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/lib/src/errors.dart +105 -1
- package/dart/lib/src/webview_bridge.dart +6 -4
- package/dart/test/webview_bridge_test.dart +65 -0
- package/package.json +1 -1
- package/src/build_package.ts +25 -0
- package/src/codegen_dart.ts +347 -3
- package/src/codegen_ts.ts +74 -1
- package/src/compile_package_main.ts +26 -2
- package/src/fields.ts +84 -0
- package/src/index.ts +19 -0
- package/src/usd.ts +21 -0
- package/src/usd_layers.ts +11 -0
- package/src/value_object.ts +413 -0
package/dart/lib/src/errors.dart
CHANGED
|
@@ -14,7 +14,111 @@
|
|
|
14
14
|
/// All extend [NomosBridgeError] so existing `catch (NomosBridgeError)` handlers still catch them.
|
|
15
15
|
library;
|
|
16
16
|
|
|
17
|
-
import 'webview_bridge.dart' show NomosBridgeError;
|
|
17
|
+
import 'webview_bridge.dart' show NomosBridgeError, NomosErrorInfo;
|
|
18
|
+
|
|
19
|
+
/// Mint the MOST SPECIFIC typed error for a structured refusal — the one construction point the bridge
|
|
20
|
+
/// uses everywhere it surfaces an `errorInfo`. A `nomos.custody.*` code becomes a [CustodyError] (typed,
|
|
21
|
+
/// recoverable, carrying the eviction/escalation decision fields); everything else stays the generic
|
|
22
|
+
/// [NomosBridgeError]. Apps `switch` on the type / `code`, never scrape prose.
|
|
23
|
+
NomosBridgeError nomosErrorFromInfo(NomosErrorInfo info) {
|
|
24
|
+
if (info.code.startsWith('nomos.custody.')) return CustodyError.fromInfo(info);
|
|
25
|
+
return NomosBridgeError(info.message, info: info);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/// The custody phase a [CustodyError] was raised in.
|
|
29
|
+
enum CustodyPhase { fold, author, fetch, unknown }
|
|
30
|
+
|
|
31
|
+
/// Where the incomplete custody came from — the axis of the recovery decision.
|
|
32
|
+
enum CustodySource { localSnapshot, cloudPack, unknown }
|
|
33
|
+
|
|
34
|
+
/// A TYPED, RECOVERABLE CUSTODY ERROR — local custody (the clone/snapshot this holon folds from) is
|
|
35
|
+
/// missing history the operation needed (`nomos.custody.shallow_insufficient` /
|
|
36
|
+
/// `nomos.custody.missing_object`), and the in-client gap-fill repair could not complete it.
|
|
37
|
+
///
|
|
38
|
+
/// This replaces pattern-matching `shallow-insufficient` out of a stringly [NomosBridgeError]: the app
|
|
39
|
+
/// switches on [code] and decides from the structured fields whether to EVICT LOCAL CUSTODY
|
|
40
|
+
/// ([recommendedAction] == `quarantineLocalSnapshotAndReclone` — quarantine the snapshot, `inspectSnapshot`
|
|
41
|
+
/// / `recoverFrom`, reconnect fresh) or ESCALATE CLOUD CUSTODY (`refetchFullHistoryAndRetry` — the cloud
|
|
42
|
+
/// must serve the full chain). Trace IDs ride [NomosBridgeError.info.trace] as on every bridge error.
|
|
43
|
+
class CustodyError extends NomosBridgeError {
|
|
44
|
+
/// The stable machine code, e.g. `nomos.custody.shallow_insufficient`.
|
|
45
|
+
final String code;
|
|
46
|
+
|
|
47
|
+
/// The workspace whose custody is incomplete.
|
|
48
|
+
final String? workspace;
|
|
49
|
+
|
|
50
|
+
/// The ref being folded (e.g. `refs/heads/main`).
|
|
51
|
+
final String? ref;
|
|
52
|
+
|
|
53
|
+
/// The ref's head commit at the time of the fault, when resolvable.
|
|
54
|
+
final String? head;
|
|
55
|
+
|
|
56
|
+
/// The missing object id the fold stopped at, when the kernel named one.
|
|
57
|
+
final String? missingObject;
|
|
58
|
+
|
|
59
|
+
/// The operation phase that hit the gap: fold (read), author (offer), fetch (the repair itself).
|
|
60
|
+
final CustodyPhase phase;
|
|
61
|
+
|
|
62
|
+
/// Provenance of the incomplete custody: a restored local snapshot, or a cloud-served pack.
|
|
63
|
+
final CustodySource source;
|
|
64
|
+
|
|
65
|
+
/// True when a known recovery lane exists (it always does for custody gaps — see [recommendedAction]).
|
|
66
|
+
final bool recoverable;
|
|
67
|
+
|
|
68
|
+
/// The named remedy: `quarantineLocalSnapshotAndReclone` | `refetchFullHistoryAndRetry`.
|
|
69
|
+
final String recommendedAction;
|
|
70
|
+
|
|
71
|
+
CustodyError({
|
|
72
|
+
required this.code,
|
|
73
|
+
required String message,
|
|
74
|
+
this.workspace,
|
|
75
|
+
this.ref,
|
|
76
|
+
this.head,
|
|
77
|
+
this.missingObject,
|
|
78
|
+
this.phase = CustodyPhase.unknown,
|
|
79
|
+
this.source = CustodySource.unknown,
|
|
80
|
+
this.recoverable = true,
|
|
81
|
+
this.recommendedAction = 'refetchFullHistoryAndRetry',
|
|
82
|
+
NomosErrorInfo? info,
|
|
83
|
+
}) : super(message, info: info);
|
|
84
|
+
|
|
85
|
+
/// Decode the structured custody payload the client attaches (`nomos.custody.*` attributes).
|
|
86
|
+
factory CustodyError.fromInfo(NomosErrorInfo info) {
|
|
87
|
+
String? attr(String name) {
|
|
88
|
+
final v = info.attributes['nomos.custody.$name'] ?? info.attributes[name];
|
|
89
|
+
return v?.toString();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return CustodyError(
|
|
93
|
+
code: info.code,
|
|
94
|
+
message: info.message,
|
|
95
|
+
workspace: info.workspace,
|
|
96
|
+
ref: attr('ref'),
|
|
97
|
+
head: attr('head'),
|
|
98
|
+
missingObject: attr('missing_object') ?? attr('missingObject'),
|
|
99
|
+
phase: CustodyPhase.values.firstWhere(
|
|
100
|
+
(p) => p.name == attr('phase'),
|
|
101
|
+
orElse: () => CustodyPhase.unknown,
|
|
102
|
+
),
|
|
103
|
+
source: CustodySource.values.firstWhere(
|
|
104
|
+
(s) => s.name == attr('source'),
|
|
105
|
+
orElse: () => CustodySource.unknown,
|
|
106
|
+
),
|
|
107
|
+
recoverable: attr('recoverable') != 'false',
|
|
108
|
+
recommendedAction: attr('recommended_action') ?? attr('recommendedAction') ?? 'refetchFullHistoryAndRetry',
|
|
109
|
+
info: info,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/// True when the remedy is to quarantine the local snapshot and re-clone (evict local custody).
|
|
114
|
+
bool get shouldEvictLocalCustody => recommendedAction == 'quarantineLocalSnapshotAndReclone';
|
|
115
|
+
|
|
116
|
+
@override
|
|
117
|
+
String toString() =>
|
|
118
|
+
'CustodyError($code${workspace != null ? ', workspace: $workspace' : ''}'
|
|
119
|
+
'${ref != null ? ', ref: $ref' : ''}${missingObject != null ? ', missing: $missingObject' : ''}'
|
|
120
|
+
', phase: ${phase.name}, source: ${source.name}, recommendedAction: $recommendedAction): $message';
|
|
121
|
+
}
|
|
18
122
|
|
|
19
123
|
/// The auth-provider access token the cloud was given was missing, malformed, expired, or rejected — the
|
|
20
124
|
/// cloud answered 401 / "invalid token" on a lane that requires a verified principal (every birth does).
|
|
@@ -17,6 +17,8 @@ library;
|
|
|
17
17
|
import 'dart:async';
|
|
18
18
|
import 'dart:math';
|
|
19
19
|
|
|
20
|
+
import 'errors.dart' show nomosErrorFromInfo;
|
|
21
|
+
|
|
20
22
|
/// The protocol version the runner and the bridge agree on (`webview-runner.mjs` `WEBVIEW_BRIDGE_VERSION`).
|
|
21
23
|
const int kWebviewBridgeVersion = 1;
|
|
22
24
|
|
|
@@ -336,7 +338,7 @@ class NomosBridge {
|
|
|
336
338
|
final info = NomosErrorInfo.fromBridgeMessage(m,
|
|
337
339
|
fallbackMessage: 'the watch read failed',
|
|
338
340
|
fallbackOperation: 'watch');
|
|
339
|
-
ctrl.addError(
|
|
341
|
+
ctrl.addError(nomosErrorFromInfo(info));
|
|
340
342
|
} else {
|
|
341
343
|
ctrl.add((m['rows'] as List?) ?? const []);
|
|
342
344
|
}
|
|
@@ -349,7 +351,7 @@ class NomosBridge {
|
|
|
349
351
|
final info = NomosErrorInfo.fromBridgeMessage(m,
|
|
350
352
|
fallbackMessage: 'the compat watch failed',
|
|
351
353
|
fallbackOperation: 'watchCompatibility');
|
|
352
|
-
cctrl.addError(
|
|
354
|
+
cctrl.addError(nomosErrorFromInfo(info));
|
|
353
355
|
} else {
|
|
354
356
|
cctrl.add(((m['rows'] as Map?)?.cast<String, Object?>()) ?? const {});
|
|
355
357
|
}
|
|
@@ -410,7 +412,7 @@ class NomosBridge {
|
|
|
410
412
|
fallbackOperation: op,
|
|
411
413
|
fallbackTrace: trace,
|
|
412
414
|
fallbackMessage: 'the githolon refused the request');
|
|
413
|
-
completer.completeError(
|
|
415
|
+
completer.completeError(nomosErrorFromInfo(info));
|
|
414
416
|
}
|
|
415
417
|
}
|
|
416
418
|
|
|
@@ -931,7 +933,7 @@ class NomosBridge {
|
|
|
931
933
|
code: 'nomos.bridge.disposed',
|
|
932
934
|
operation: _pendingOps[entry.key],
|
|
933
935
|
trace: trace);
|
|
934
|
-
c.completeError(
|
|
936
|
+
c.completeError(nomosErrorFromInfo(info));
|
|
935
937
|
}
|
|
936
938
|
}
|
|
937
939
|
_pending.clear();
|
|
@@ -61,6 +61,36 @@ class FakeRunner implements BridgeTransport {
|
|
|
61
61
|
'attributes': {'test.case': 'refusal'}
|
|
62
62
|
}
|
|
63
63
|
});
|
|
64
|
+
} else if (intentJson.contains('"directiveId":"custodyGap"')) {
|
|
65
|
+
// The client's typed custody error: structured errorInfo, code=nomos.custody.*.
|
|
66
|
+
_incoming.add({
|
|
67
|
+
'v': kWebviewBridgeVersion,
|
|
68
|
+
'id': id,
|
|
69
|
+
'ok': false,
|
|
70
|
+
'error':
|
|
71
|
+
'shallow-insufficient: fold refs/heads/main: find commit: NotFound',
|
|
72
|
+
'errorInfo': {
|
|
73
|
+
'code': 'nomos.custody.shallow_insufficient',
|
|
74
|
+
'message':
|
|
75
|
+
'shallow-insufficient: fold refs/heads/main: find commit: NotFound',
|
|
76
|
+
'component': 'nomos.custody',
|
|
77
|
+
'operation': 'offer',
|
|
78
|
+
'workspace': 'ws-1',
|
|
79
|
+
'trace': message['trace'],
|
|
80
|
+
'attributes': {
|
|
81
|
+
'nomos.custody.ref': 'refs/heads/main',
|
|
82
|
+
'nomos.custody.head':
|
|
83
|
+
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
|
84
|
+
'nomos.custody.missing_object':
|
|
85
|
+
'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
|
|
86
|
+
'nomos.custody.phase': 'author',
|
|
87
|
+
'nomos.custody.source': 'localSnapshot',
|
|
88
|
+
'nomos.custody.recoverable': true,
|
|
89
|
+
'nomos.custody.recommended_action':
|
|
90
|
+
'quarantineLocalSnapshotAndReclone',
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
});
|
|
64
94
|
} else {
|
|
65
95
|
_incoming.add(reply({'head': 'HEAD-offer'}));
|
|
66
96
|
}
|
|
@@ -420,6 +450,41 @@ void main() {
|
|
|
420
450
|
expect(e.info.toTelemetryAttributes()['nomos.operation'], 'offer');
|
|
421
451
|
});
|
|
422
452
|
|
|
453
|
+
test(
|
|
454
|
+
'a custody fault surfaces as the TYPED CustodyError (switch on code, never scrape prose)',
|
|
455
|
+
() async {
|
|
456
|
+
await bridge.ready;
|
|
457
|
+
Object? err;
|
|
458
|
+
try {
|
|
459
|
+
await bridge.offer(
|
|
460
|
+
intentBytes: base64Encode(utf8.encode(jsonEncode({
|
|
461
|
+
'v': 2,
|
|
462
|
+
'intent': {
|
|
463
|
+
'domain': 'x',
|
|
464
|
+
'directiveId': 'custodyGap',
|
|
465
|
+
'payload': {},
|
|
466
|
+
'domainHash': 'h'
|
|
467
|
+
}
|
|
468
|
+
}))));
|
|
469
|
+
} on NomosBridgeError catch (e) {
|
|
470
|
+
err = e; // still catchable as the generic bridge error
|
|
471
|
+
}
|
|
472
|
+
expect(err, isA<CustodyError>());
|
|
473
|
+
final e = err as CustodyError;
|
|
474
|
+
expect(e.code, 'nomos.custody.shallow_insufficient');
|
|
475
|
+
expect(e.workspace, 'ws-1');
|
|
476
|
+
expect(e.ref, 'refs/heads/main');
|
|
477
|
+
expect(e.head, 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa');
|
|
478
|
+
expect(e.missingObject, 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb');
|
|
479
|
+
expect(e.phase, CustodyPhase.author);
|
|
480
|
+
expect(e.source, CustodySource.localSnapshot);
|
|
481
|
+
expect(e.recoverable, isTrue);
|
|
482
|
+
expect(e.recommendedAction, 'quarantineLocalSnapshotAndReclone');
|
|
483
|
+
expect(e.shouldEvictLocalCustody, isTrue);
|
|
484
|
+
expect(e.info.trace.traceId, isNotEmpty); // trace IDs ride as on every bridge error
|
|
485
|
+
expect(e.message, contains('shallow-insufficient'));
|
|
486
|
+
});
|
|
487
|
+
|
|
423
488
|
test(
|
|
424
489
|
'legacy string-only runner errors still get operation and trace metadata',
|
|
425
490
|
() async {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@githolon/dsl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.33.0",
|
|
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",
|
package/src/build_package.ts
CHANGED
|
@@ -56,6 +56,7 @@ import type { WorkspaceInvariantDecl } from "./framework/workspace_invariant.js"
|
|
|
56
56
|
import { synthesizePreconditionInvariants } from "./precondition.js";
|
|
57
57
|
import type { RelationTypeDecl } from "./relation_schema.js";
|
|
58
58
|
import { domainHash, emitManifestBytes } from "./manifest.js";
|
|
59
|
+
import { collectValueObjectDescriptors } from "./value_object.js";
|
|
59
60
|
import { emitUsd } from "./usd.js";
|
|
60
61
|
import { expandCapabilityExports } from "./capability_exports.js";
|
|
61
62
|
import { RelationTuple } from "./framework/relations.js";
|
|
@@ -188,6 +189,27 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
|
|
|
188
189
|
// a directive with no precondition synthesizes nothing (byte-identical manifest).
|
|
189
190
|
for (const d of directives) workspaceInvariants.push(...synthesizePreconditionInvariants(d));
|
|
190
191
|
const relationTypes = [...(spec.relationTypes ?? [])];
|
|
192
|
+
// FIRST-CLASS VALUE OBJECTS (value_object.ts): the deduped, sorted descriptors of every
|
|
193
|
+
// named VO the domain's aggregates carry (transitive nested VOs included) — the input to
|
|
194
|
+
// the additive `nomosValueObjects` USD key. Omit-when-empty (era rule): a VO-free domain's
|
|
195
|
+
// module — and thus its USD/package bytes — is byte-identical to before VOs existed.
|
|
196
|
+
const valueObjects = collectValueObjectDescriptors(aggregates);
|
|
197
|
+
// SPATIAL × VO PAIRING (fail at build, not at fold): a spatial index over a VO-typed field
|
|
198
|
+
// must index a GEO value object (an RFC 7946 geometry — the R*Tree's bbox source). A
|
|
199
|
+
// structural VO (e.g. a PostalAddress) has no geometry to index. Non-VO fields (a
|
|
200
|
+
// hand-rolled `t.jsonObject()` GeoJSON leaf) pass through unchecked, exactly as before.
|
|
201
|
+
for (const s of spatials) {
|
|
202
|
+
const agg = byId.get(s.of);
|
|
203
|
+
const field = agg?.fields[s.on];
|
|
204
|
+
const vo = field?.valueObject;
|
|
205
|
+
if (vo !== undefined && vo.geometry === undefined) {
|
|
206
|
+
throw new Error(
|
|
207
|
+
`build-package: spatial '${s.id}' indexes '${s.of}.${s.on}', a '${vo.name}' value object with NO ` +
|
|
208
|
+
`geometry — the R*Tree needs an RFC 7946 geometry leaf. Give the geometry its OWN field ` +
|
|
209
|
+
`(e.g. \`point: geo.point().field()\`) and index that (v1: no sub-path indexing).`,
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
191
213
|
const dartImports = [...(spec.dartImports ?? [])];
|
|
192
214
|
// THE PEER-SOVEREIGN ROOT-OF-TRUST: a home/child law that exports `requireParentAttestedBirth` declares
|
|
193
215
|
// it requires a parent-attested-birth cert chain. Discovered by shape across the module list (the SAME
|
|
@@ -233,6 +255,9 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
|
|
|
233
255
|
// Omit-when-empty — the relation schema is hashed law (P1); a relation-free domain's
|
|
234
256
|
// canonical manifest/USD stays byte-identical (the nomosRelations era rule).
|
|
235
257
|
...(relationTypes.length > 0 ? { relationTypes } : {}),
|
|
258
|
+
// Omit-when-empty — the `nomosValueObjects` era rule: a VO-free domain's USD/package
|
|
259
|
+
// bytes are byte-identical to before value objects existed.
|
|
260
|
+
...(valueObjects.length > 0 ? { valueObjects } : {}),
|
|
236
261
|
// THE PEER-SOVEREIGN ROOT-OF-TRUST: omit-when-false — a law that doesn't require parent-attested
|
|
237
262
|
// birth is byte-identical in the canonical manifest/USD (the nomosParentAttestedBirth era rule).
|
|
238
263
|
...(parentAttestedBirth ? { parentAttestedBirth } : {}),
|