@githolon/dsl 0.74.0 → 0.75.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/dart/CHANGELOG.md +28 -0
- package/dart/lib/nomos_headless.dart +8 -0
- package/dart/lib/src/webview_bridge.dart +149 -84
- package/dart/pubspec.yaml +1 -1
- package/dart/test/acquire_session_credentials_test.dart +132 -0
- package/dart/test/cluster_births_test.dart +0 -1
- package/dart/test/dlq_session_surface_test.dart +203 -0
- package/dart/test/event_plane_seam_test.dart +188 -0
- package/dart/test/first_app_gauntlet_test.dart +301 -0
- package/dart/test/sealing_client_test.dart +179 -0
- package/dart/test/test_harness_test.dart +0 -1
- package/dart/test/watch_surfaces_test.dart +240 -0
- package/package.json +1 -1
- package/src/aggregate.ts +75 -1
- package/src/codegen_dart.ts +101 -0
- package/src/codegen_dart_client.ts +134 -18
- package/src/codegen_ts.ts +44 -0
- package/src/compile_package_main.ts +26 -1
- package/src/drivers.ts +9 -1
- package/src/fields.ts +23 -0
- package/src/manifest.ts +11 -0
- package/src/usd.ts +24 -0
- package/src/usd_layers.ts +32 -0
- package/src/wire.ts +5 -1
|
@@ -142,6 +142,17 @@ export interface DartBridgeClientOptions {
|
|
|
142
142
|
* omitted. Only meaningful when {@link emitActivity} is set.
|
|
143
143
|
*/
|
|
144
144
|
readonly activityImportUri?: string;
|
|
145
|
+
/**
|
|
146
|
+
* CROSS-DOMAIN REF TRAVERSAL (dart_codegen_parity.md §2a follow-on): every aggregate id in the
|
|
147
|
+
* PACKAGE → the domain module that owns it (`domain` for the client class name, `name` for the
|
|
148
|
+
* relative `<name>.client.dart` import). A same-workspace `t.ref` whose target lives in ANOTHER
|
|
149
|
+
* domain of this package emits a resolver taking THAT domain's generated client (the shared barrel
|
|
150
|
+
* makes the class + read model reachable; the client file imports the sibling client with `show`).
|
|
151
|
+
* Same-domain refs (target in THIS module) resolve via this client and never consult the map.
|
|
152
|
+
* Omit (or supply only self-owned aggregates) ⇒ NO cross-domain accessors are emitted — a package
|
|
153
|
+
* whose refs are all same-domain / foreign is byte-identical to before this existed.
|
|
154
|
+
*/
|
|
155
|
+
readonly packageAggregateDomains?: Readonly<Record<string, { domain: string; name: string }>>;
|
|
145
156
|
}
|
|
146
157
|
|
|
147
158
|
/** Generate the `<name>.client.dart` async bridge client for one domain module. */
|
|
@@ -489,32 +500,54 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
489
500
|
);
|
|
490
501
|
}
|
|
491
502
|
|
|
492
|
-
// ── REF-TRAVERSAL ACCESSORS (dart_codegen_parity.md P2 — ref ergonomics) ──
|
|
493
|
-
// For every
|
|
494
|
-
//
|
|
495
|
-
//
|
|
496
|
-
//
|
|
497
|
-
//
|
|
498
|
-
//
|
|
499
|
-
//
|
|
500
|
-
//
|
|
501
|
-
//
|
|
502
|
-
//
|
|
503
|
+
// ── REF-TRAVERSAL ACCESSORS (dart_codegen_parity.md P2 + §2a follow-on — ref ergonomics) ──
|
|
504
|
+
// For every reference field on a read model, emit a companion extension method that resolves the
|
|
505
|
+
// stored locator to the actual target row, composed ONLY from surface that already exists (no new
|
|
506
|
+
// read path, no new bridge op). THREE kinds, one grammar (`<Model>Refs on <Model>`):
|
|
507
|
+
// 1. SAME-DOMAIN `t.ref(Target)` (target in THIS module) — resolver takes THIS client, dispatches
|
|
508
|
+
// to its own `<target>ById` accessor. (The #72 original.)
|
|
509
|
+
// 2. CROSS-DOMAIN same-workspace `t.ref(Target)` (target in ANOTHER domain of the package) — the
|
|
510
|
+
// target's by-id accessor lives on the OTHER domain's client, so the resolver takes THAT client
|
|
511
|
+
// (`asset.listing(CatalogueClient c)`); the shared package barrel makes the class + read model
|
|
512
|
+
// reachable and the client file imports the sibling client with `show` (§2a follow-on).
|
|
513
|
+
// 3. CROSS-WORKSPACE `t.foreignRef` — the read model already surfaces the field as a typed
|
|
514
|
+
// `ForeignRef<Target>`; the resolver is thin sugar over the ESTABLISHED foreign-read semantic
|
|
515
|
+
// (`ref.resolve(engine.foreignReader())` — follow+read on the session-bound multi-workspace
|
|
516
|
+
// plane, no clone), NOT the law-grade attested-read lane (that is `ref.attest`, opt-in via
|
|
517
|
+
// `trustSource` + `.readsFrom`). Returns the typed concrete target row or null.
|
|
518
|
+
// A read model with no reference field emits nothing; a package with only same-domain / foreign refs
|
|
519
|
+
// (no cross-domain resolvable target) is byte-identical to before the §2a follow-on existed.
|
|
520
|
+
const pkgDomains = opts.packageAggregateDomains ?? {};
|
|
521
|
+
const thisDomainName = mod.name;
|
|
503
522
|
const refExtensions: string[] = [];
|
|
523
|
+
// importUri → the set of sibling names the ref resolvers reference (imported `show`-limited): a
|
|
524
|
+
// cross-domain `t.ref` needs the sibling CLIENT class + its read model (from `<name>.client.dart`); a
|
|
525
|
+
// cross-domain `t.foreignRef` needs the target's concrete class (from the sibling TYPES `<name>.dart`).
|
|
526
|
+
const crossDomainImports = new Map<string, Set<string>>();
|
|
527
|
+
const addImport = (uri: string, name: string): void => {
|
|
528
|
+
if (!crossDomainImports.has(uri)) crossDomainImports.set(uri, new Set());
|
|
529
|
+
crossDomainImports.get(uri)!.add(name);
|
|
530
|
+
};
|
|
504
531
|
for (const agg of mod.aggregates) {
|
|
505
532
|
const model = readModelClassName(agg.id);
|
|
506
|
-
const
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
const
|
|
511
|
-
|
|
533
|
+
const entries = Object.entries(agg.fields) as [string, Field][];
|
|
534
|
+
const sameDomainRefs = entries.filter(
|
|
535
|
+
([, f]) => f.kind === "ref" && f.foreignRef === undefined && f.refAggregateId !== undefined && aggById.has(f.refAggregateId!),
|
|
536
|
+
);
|
|
537
|
+
const crossDomainRefs = entries.filter(([, f]) => {
|
|
538
|
+
if (!(f.kind === "ref" && f.foreignRef === undefined && f.refAggregateId !== undefined)) return false;
|
|
539
|
+
if (aggById.has(f.refAggregateId!)) return false; // same-domain — handled above
|
|
540
|
+
const owner = pkgDomains[f.refAggregateId!];
|
|
541
|
+
return owner !== undefined && owner.domain !== domain; // resolvable on a SIBLING domain's client
|
|
542
|
+
});
|
|
543
|
+
const foreignRefs = entries.filter(([, f]) => f.kind === "ref" && f.foreignRef !== undefined);
|
|
544
|
+
if (sameDomainRefs.length === 0 && crossDomainRefs.length === 0 && foreignRefs.length === 0) continue;
|
|
512
545
|
const lines: string[] = [
|
|
513
546
|
`/// Ref-traversal accessors for [${model}] (dart_codegen_parity.md P2) — resolve a stored ref id`,
|
|
514
547
|
`/// to the actual target row via the SAME by-id read lane already exposed above.`,
|
|
515
548
|
`extension ${model}Refs on ${model} {`,
|
|
516
549
|
];
|
|
517
|
-
for (const [fieldName, f] of
|
|
550
|
+
for (const [fieldName, f] of sameDomainRefs) {
|
|
518
551
|
const targetType = f.refAggregateId!;
|
|
519
552
|
const targetModel = readModelClassName(targetType);
|
|
520
553
|
const targetConcrete = concreteClassName(targetType);
|
|
@@ -546,6 +579,80 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
546
579
|
``,
|
|
547
580
|
);
|
|
548
581
|
}
|
|
582
|
+
for (const [fieldName, f] of crossDomainRefs) {
|
|
583
|
+
const targetType = f.refAggregateId!;
|
|
584
|
+
const owner = pkgDomains[targetType]!;
|
|
585
|
+
const targetModel = readModelClassName(targetType);
|
|
586
|
+
const targetConcrete = concreteClassName(targetType);
|
|
587
|
+
const targetLc = lcFirst(camel(targetConcrete));
|
|
588
|
+
const byIdMethod = safeMethod(`${targetLc}ById`);
|
|
589
|
+
const targetClient = dartClientClassName(owner.domain);
|
|
590
|
+
// The sibling CLIENT file exposes both the client class AND (via its `export '<name>.dart'`) the
|
|
591
|
+
// read model — pull BOTH `show`-limited so this file references neither an unimported type.
|
|
592
|
+
const importUri = `${owner.name}.client.dart`;
|
|
593
|
+
addImport(importUri, targetClient);
|
|
594
|
+
addImport(importUri, targetModel);
|
|
595
|
+
const member = dartMemberName(fieldName);
|
|
596
|
+
const methodStem = member.length > 2 && member.endsWith("Id") ? member.slice(0, -2) : `${member}Ref`;
|
|
597
|
+
const method = safeMethod(methodStem);
|
|
598
|
+
const memberNullable = f.isFromBirth !== true;
|
|
599
|
+
lines.push(
|
|
600
|
+
` /// Resolve \`${fieldName}\` (${agg.id}.${fieldName} -> ${targetType}, a CROSS-DOMAIN ref) by`,
|
|
601
|
+
` /// dispatching to [${targetClient}.${byIdMethod}] on the target domain's client — same by-id`,
|
|
602
|
+
` /// read lane, never a second one. The shared package barrel makes [${targetClient}] +`,
|
|
603
|
+
` /// [${targetModel}] reachable; pass that domain's bound client.`,
|
|
604
|
+
` Future<${targetModel}?> ${method}(${targetClient} client) async {`,
|
|
605
|
+
...(memberNullable
|
|
606
|
+
? [
|
|
607
|
+
` final id = ${member};`,
|
|
608
|
+
` if (id == null) return null;`,
|
|
609
|
+
` final rows = await client.${byIdMethod}(id.value);`,
|
|
610
|
+
]
|
|
611
|
+
: [` final rows = await client.${byIdMethod}(${member}.value);`]),
|
|
612
|
+
` return rows.isEmpty ? null : rows.first;`,
|
|
613
|
+
` }`,
|
|
614
|
+
``,
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
for (const [fieldName, f] of foreignRefs) {
|
|
618
|
+
const fr = f.foreignRef!;
|
|
619
|
+
const targetType = fr.targetAggregateId;
|
|
620
|
+
// The resolver returns the target's CONCRETE class (what `ForeignRef<T>.resolve` decodes to). We can
|
|
621
|
+
// only reference it when it is reachable from this file: SAME-domain (already in the imported types),
|
|
622
|
+
// or a CROSS-DOMAIN concrete target in the package (import its sibling `<name>.dart` `show`-limited).
|
|
623
|
+
// A contract target (its DTO lives in the shared contracts file) or an unmapped target is left as the
|
|
624
|
+
// raw `ForeignRef<T>` field — no resolver, no dangling type reference.
|
|
625
|
+
if (fr.targetIsContract === true) continue;
|
|
626
|
+
const owner = pkgDomains[targetType];
|
|
627
|
+
const targetInThisDomain = aggById.has(targetType) || owner?.name === thisDomainName;
|
|
628
|
+
if (!targetInThisDomain) {
|
|
629
|
+
if (owner === undefined) continue; // target not in this package — unreachable, skip the resolver
|
|
630
|
+
// LAYERED-packages mode: the sibling domain's TYPES file lives in the TYPES package, not
|
|
631
|
+
// beside this client file — a relative `<name>.dart` import points at a nonexistent path in
|
|
632
|
+
// the client package (the co2 0.75.0 regression). The types-barrel import above already
|
|
633
|
+
// re-exports every generated concrete class, so the extra import is only needed FLAT.
|
|
634
|
+
if (opts.typesImportUri === undefined) addImport(`${owner.name}.dart`, concreteClassName(targetType));
|
|
635
|
+
}
|
|
636
|
+
const targetConcrete = concreteClassName(targetType);
|
|
637
|
+
const member = dartMemberName(fieldName);
|
|
638
|
+
// The stored locator member is a typed `ForeignRef<Target>?` getter, so the resolver takes a
|
|
639
|
+
// `remote…` name to avoid colliding with it (and to read as "the far side of the reference").
|
|
640
|
+
const method = safeMethod(`remote${cap(camel(member))}`);
|
|
641
|
+
lines.push(
|
|
642
|
+
` /// Resolve \`${fieldName}\` (${agg.id}.${fieldName} -> ${targetType} @ another workspace, a`,
|
|
643
|
+
` /// \`t.foreignRef\` locator) over the session-bound multi-workspace plane —`,
|
|
644
|
+
` /// \`${member}.resolve(engine.foreignReader())\`, which opens the foreign workspace ON DEMAND`,
|
|
645
|
+
` /// (no clone). This is the informational follow+read lane, NOT the law-grade attested-read one`,
|
|
646
|
+
` /// (use \`${member}!.attest(engine.foreignReader())\` when your law depends on the foreign fact).`,
|
|
647
|
+
` /// Null locator (absent / empty id or workspace) ⇒ null.`,
|
|
648
|
+
` Future<${targetConcrete}?> ${method}(NomosEngine engine, {String? cloud}) {`,
|
|
649
|
+
` final ref = ${member};`,
|
|
650
|
+
` if (ref == null) return Future<${targetConcrete}?>.value(null);`,
|
|
651
|
+
` return ref.resolve(engine.foreignReader(cloud: cloud));`,
|
|
652
|
+
` }`,
|
|
653
|
+
``,
|
|
654
|
+
);
|
|
655
|
+
}
|
|
549
656
|
lines.push(`}`);
|
|
550
657
|
refExtensions.push(lines.join("\n"));
|
|
551
658
|
}
|
|
@@ -631,6 +738,15 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
631
738
|
// Framework-shared aggregate types (RelationTupleReadModel, …) live in the ONE shared file — import
|
|
632
739
|
// them (the by-id accessors reference them) and re-export them (once per package, Dart de-dups).
|
|
633
740
|
...(opts.typesImportUri === undefined && opts.frameworkImportUri !== undefined ? [`import '${opts.frameworkImportUri}';`] : []),
|
|
741
|
+
// CROSS-DOMAIN REF TRAVERSAL (§2a follow-on): a cross-domain resolver references a SIBLING domain's
|
|
742
|
+
// generated client class + read model (from `<name>.client.dart`, co-located with this file in BOTH
|
|
743
|
+
// modes) or — FLAT MODE ONLY — a foreignRef target's concrete class from the sibling TYPES
|
|
744
|
+
// `<name>.dart` (in layered mode that file lives in the TYPES package and the types-barrel import
|
|
745
|
+
// above already re-exports it; emitting the relative import there was the co2 0.75.0 regression).
|
|
746
|
+
// Empty for a domain with no cross-domain ref (byte-identical to before this existed).
|
|
747
|
+
...[...crossDomainImports.entries()]
|
|
748
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
749
|
+
.map(([uri, names]) => `import '${uri}' show ${[...names].sort().join(", ")};`),
|
|
634
750
|
`// Re-export the sibling types (read models + payloads + the bridge runtime) so ONE import of this`,
|
|
635
751
|
`// client file gives an app the whole typed surface.`,
|
|
636
752
|
...(opts.typesImportUri !== undefined ? [`export '${opts.typesImportUri}';`] : [`export '${mod.name}.dart';`]),
|
package/src/codegen_ts.ts
CHANGED
|
@@ -1499,7 +1499,24 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
1499
1499
|
`_stampRefsP(holon.workspace, ${foreignRefFields.length > 0 ? `_hydrateP(${expr})` : expr})`;
|
|
1500
1500
|
const wrapRows = (expr: string): string =>
|
|
1501
1501
|
`_stampRefs(holon.workspace, ${foreignRefFields.length > 0 ? `_hydrateRows(${expr})` : expr})`;
|
|
1502
|
+
// ATTRIBUTED READS (architecture/field_provenance.md): the shared provenance shape +
|
|
1503
|
+
// per-aggregate accessors — emitted ONLY when the package declares ≥1 `.attributed()`
|
|
1504
|
+
// field, so a provenance-free package's client is byte-identical (the era rule).
|
|
1505
|
+
const anyAttributed = [...aggByType.values()].some(
|
|
1506
|
+
(a) => ((a as { attributedFields?: readonly string[] }).attributedFields ?? []).length > 0,
|
|
1507
|
+
);
|
|
1508
|
+
if (anyAttributed) {
|
|
1509
|
+
out.push(
|
|
1510
|
+
`/** Per-field AUTHOR PROVENANCE (attributed reads — field_provenance.md): the verified`,
|
|
1511
|
+
` * author of the field's CURRENT value (null when unknown — an anonymous or`,
|
|
1512
|
+
` * pre-attribution-era write) + whether an auto-merge discarded a concurrent competing`,
|
|
1513
|
+
` * write since it landed (escalate the field to Surfaced(...) law to retain the values). */`,
|
|
1514
|
+
`export interface FieldProvenance { author: string | null; wasContested: boolean }`,
|
|
1515
|
+
``,
|
|
1516
|
+
);
|
|
1517
|
+
}
|
|
1502
1518
|
for (const [typeId, agg] of [...aggByType].sort(([a], [b]) => a.localeCompare(b))) {
|
|
1519
|
+
const attributed = (agg as { attributedFields?: readonly string[] }).attributedFields ?? [];
|
|
1503
1520
|
out.push(`export interface ${typeId}Data {`);
|
|
1504
1521
|
for (const [name, field] of Object.entries(agg.fields)) {
|
|
1505
1522
|
const required = (field as Field).isFromBirth === true;
|
|
@@ -1509,8 +1526,35 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
1509
1526
|
const fr = (field as { foreignRef?: { workspaceField: string; targetAggregateId: string } }).foreignRef;
|
|
1510
1527
|
if (fr !== undefined) foreignRefFields.push({ agg: typeId, field: name, workspaceField: fr.workspaceField, target: fr.targetAggregateId });
|
|
1511
1528
|
}
|
|
1529
|
+
if (attributed.length > 0) {
|
|
1530
|
+
out.push(
|
|
1531
|
+
` /** attributed-read provenance (the reserved per-row map; prefer the generated accessors) */`,
|
|
1532
|
+
` "__prov"?: { ${attributed.map((f) => `${f}?: FieldProvenance`).join("; ")} };`,
|
|
1533
|
+
);
|
|
1534
|
+
}
|
|
1512
1535
|
for (const extra of extrasByType.get(typeId) ?? []) out.push(extra);
|
|
1513
1536
|
out.push(`}`, ``);
|
|
1537
|
+
if (attributed.length > 0) {
|
|
1538
|
+
const ns = camel(typeId);
|
|
1539
|
+
out.push(
|
|
1540
|
+
`/** Attributed-read accessors for ${typeId} (\`.attributed()\` fields only): per-field`,
|
|
1541
|
+
` * provenance + the \`lastAuthors\` convenience (verified author per attributed field,`,
|
|
1542
|
+
` * unknown-author fields omitted). */`,
|
|
1543
|
+
`export const ${ns}Provenance = {`,
|
|
1544
|
+
...attributed.map(
|
|
1545
|
+
(f) => ` ${f}: (data: ${typeId}Data): FieldProvenance | undefined => data.__prov?.${f},`,
|
|
1546
|
+
),
|
|
1547
|
+
` lastAuthors: (data: ${typeId}Data): Record<string, string> => {`,
|
|
1548
|
+
` const out: Record<string, string> = {};`,
|
|
1549
|
+
` for (const [field, p] of Object.entries(data.__prov ?? {})) {`,
|
|
1550
|
+
` if (p !== undefined && p.author !== null) out[field] = p.author;`,
|
|
1551
|
+
` }`,
|
|
1552
|
+
` return out;`,
|
|
1553
|
+
` },`,
|
|
1554
|
+
`} as const;`,
|
|
1555
|
+
``,
|
|
1556
|
+
);
|
|
1557
|
+
}
|
|
1514
1558
|
}
|
|
1515
1559
|
// MULTI-WORKSPACE LOCATOR FACTORY (t.foreignRef): per aggregate, a typed builder that turns a projection
|
|
1516
1560
|
// row into a `ForeignRef<TargetData>` — reading the foreign id (the field) + the foreign workspace id (the
|
|
@@ -500,6 +500,24 @@ function collectPackageContracts(modules: readonly DomainModule[]): ContractHand
|
|
|
500
500
|
return [...byId.values()].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
501
501
|
}
|
|
502
502
|
|
|
503
|
+
/**
|
|
504
|
+
* CROSS-DOMAIN REF TRAVERSAL map (dart_codegen_parity.md §2a follow-on): every aggregate id in the package
|
|
505
|
+
* → its owning domain (`domain` for the generated client class name; `name` for the sibling
|
|
506
|
+
* `<name>.client.dart` import). Handed to `generateDartBridgeClient` so a same-workspace `t.ref` whose
|
|
507
|
+
* target lives in ANOTHER domain of the package emits a resolver taking that domain's client. A same-domain
|
|
508
|
+
* ref never consults this (its target is in the local module); a foreign ref uses its own locator.
|
|
509
|
+
*/
|
|
510
|
+
function buildPackageAggregateDomains(
|
|
511
|
+
modules: readonly DomainModule[],
|
|
512
|
+
): Record<string, { domain: string; name: string }> {
|
|
513
|
+
const map: Record<string, { domain: string; name: string }> = {};
|
|
514
|
+
for (const m of modules) {
|
|
515
|
+
const owner = { domain: m.domain ?? m.name, name: m.name };
|
|
516
|
+
for (const agg of (m.aggregates as { id: string }[])) map[agg.id] = owner;
|
|
517
|
+
}
|
|
518
|
+
return map;
|
|
519
|
+
}
|
|
520
|
+
|
|
503
521
|
/**
|
|
504
522
|
* CIRCULAR-DEPENDENCY GUARD (domain_contracts.md). A `t.foreignRef` targeting a CONCRETE aggregate makes
|
|
505
523
|
* the referencing domain's generated file import the target domain's file. Two domains that concretely
|
|
@@ -789,6 +807,11 @@ function emitDart(
|
|
|
789
807
|
for (const m of modules) {
|
|
790
808
|
for (const agg of (m.aggregates as { id: string }[])) aggIdToDartFile.set(agg.id, m.name);
|
|
791
809
|
}
|
|
810
|
+
// CROSS-DOMAIN `t.ref` traversal (dart_codegen_parity.md §2a follow-on): the generated ref-traversal
|
|
811
|
+
// accessor for a same-workspace ref whose target lives in ANOTHER domain of THIS package takes that
|
|
812
|
+
// domain's client. Map every aggregate id → its owning domain (for the client class name) + file base
|
|
813
|
+
// (for the sibling `<name>.client.dart` import). Shared across both Dart-emit lanes.
|
|
814
|
+
const packageAggregateDomains = buildPackageAggregateDomains(modules);
|
|
792
815
|
// CIRCULAR-DEP GUARD (domain_contracts.md): concrete-domain → concrete-domain import edges (from
|
|
793
816
|
// concrete-aggregate-target foreignRefs). A 2-cycle A→B & B→A is the anti-pattern contracts exist to
|
|
794
817
|
// break — fail with a clear diagnostic. Contract-mediated refs go through contracts.dart (no concrete edge).
|
|
@@ -845,6 +868,7 @@ function emitDart(
|
|
|
845
868
|
supportImport,
|
|
846
869
|
...(domainFrameworkImport !== undefined ? { frameworkImportUri: domainFrameworkImport } : {}),
|
|
847
870
|
...(packageActivity.length > 0 ? { emitActivity: true, ...(activityImportUri !== undefined ? { activityImportUri } : {}) } : {}),
|
|
871
|
+
packageAggregateDomains,
|
|
848
872
|
});
|
|
849
873
|
const cp = path.join(genDir, `${mod.name}.client.dart`);
|
|
850
874
|
writeFileSync(cp, clientSrc, "utf8");
|
|
@@ -904,7 +928,7 @@ const NOMOS_TYPES_DEP = "^0.3.0";
|
|
|
904
928
|
// 0.48.x: session-bound clients (XClient.bind/NomosClientInterface/DomainClientSessionMismatch/
|
|
905
929
|
// DomainNotInstalledForSession, engine.follow, pendingConflicts lanes) — the generated client
|
|
906
930
|
// REQUIRES this surface. 0.48.1 fixes NomosSession.watchPendingConflicts delegation.
|
|
907
|
-
const NOMOS_CLIENT_DEP = "^0.
|
|
931
|
+
const NOMOS_CLIENT_DEP = "^0.51.0";
|
|
908
932
|
// DRIFT TRIPWIRE: pubspec_pins.test.ts asserts these equal the CURRENT versions in dsl/dart/pubspec.yaml
|
|
909
933
|
// and dsl/dart_types/pubspec.yaml — a Dart release without this bump fails the suite, so tenants never
|
|
910
934
|
// land back in dependency_overrides (the exact co2 report this closes, third occurrence).
|
|
@@ -1181,6 +1205,7 @@ function emitDartPackages(
|
|
|
1181
1205
|
supportImport: "package",
|
|
1182
1206
|
typesImportUri,
|
|
1183
1207
|
...(packageActivity.length > 0 ? { emitActivity: true } : {}),
|
|
1208
|
+
packageAggregateDomains: buildPackageAggregateDomains(modules),
|
|
1184
1209
|
});
|
|
1185
1210
|
const cp = path.join(clientGen, `${mod.name}.client.dart`);
|
|
1186
1211
|
writeFileSync(cp, clientSrc, "utf8");
|
package/src/drivers.ts
CHANGED
|
@@ -37,7 +37,8 @@ export type KernelDriverKind =
|
|
|
37
37
|
| "RemoveWins"
|
|
38
38
|
| "Counter"
|
|
39
39
|
| "LastPosition"
|
|
40
|
-
| "Surfaced"
|
|
40
|
+
| "Surfaced"
|
|
41
|
+
| "Attributed";
|
|
41
42
|
/**
|
|
42
43
|
* Drivers in the design palette the kernel does not yet merge. Currently EMPTY —
|
|
43
44
|
* every palette driver encodes. Kept as a (never-)union so a future palette
|
|
@@ -183,6 +184,13 @@ export function encodeDriver(d: Driver): WireDriver {
|
|
|
183
184
|
case "MapOf":
|
|
184
185
|
if (!d.inner) throw new Error("MapOf driver is missing its inner driver");
|
|
185
186
|
return { MapOf: encodeDriver(d.inner) };
|
|
187
|
+
case "Attributed":
|
|
188
|
+
// serde newtype variant `Driver::Attributed(Box<Driver>)` → `{ "Attributed": <inner> }`
|
|
189
|
+
// (architecture/field_provenance.md — the wrapper aggregate() applies for
|
|
190
|
+
// `.attributed()` fields; a pre-provenance kernel's strict manifest serde refuses
|
|
191
|
+
// it, the clean era refusal).
|
|
192
|
+
if (!d.inner) throw new Error("Attributed driver is missing its inner driver");
|
|
193
|
+
return { Attributed: encodeDriver(d.inner) };
|
|
186
194
|
case "Surfaced":
|
|
187
195
|
// serde newtype variant `Driver::Surfaced(Box<Driver>)` → `{ "Surfaced": <inner> }`
|
|
188
196
|
// (externally tagged, mirroring MapOf exactly).
|
package/src/fields.ts
CHANGED
|
@@ -361,6 +361,18 @@ export interface Field<T = unknown, K extends FieldKind = FieldKind> {
|
|
|
361
361
|
* the `encrypted()` METHOD sets it, mirroring `optional()`/`isOptional`.)
|
|
362
362
|
*/
|
|
363
363
|
readonly isEncrypted?: true;
|
|
364
|
+
/**
|
|
365
|
+
* ATTRIBUTED READS (architecture/field_provenance.md): keep per-field AUTHOR PROVENANCE
|
|
366
|
+
* beside this field's folded value — `author` (the verified principal of the current
|
|
367
|
+
* value) + `wasContested` (did an auto-merge discard a concurrent competing write?).
|
|
368
|
+
* O(1) per field forever (one author + one boolean — the ledger stays the history;
|
|
369
|
+
* escalate to `.merge(Surfaced(...))` if you need the discarded VALUES). PRESENT only
|
|
370
|
+
* when declared; the wire driver becomes `{"Attributed": <inner>}` (a LAW change —
|
|
371
|
+
* old kernels refuse it cleanly, unattributed fields are byte-identical). VALID ONLY
|
|
372
|
+
* on Lww-class scalar fields; `aggregate()` enforces it. (Flag named `isAttributed` —
|
|
373
|
+
* the `attributed()` METHOD sets it, mirroring `encrypted()`/`isEncrypted`.)
|
|
374
|
+
*/
|
|
375
|
+
readonly isAttributed?: true;
|
|
364
376
|
/**
|
|
365
377
|
* METADATA (secure-by-default rework — `secure_by_default_dsl.md`): on an `.owned()` aggregate the
|
|
366
378
|
* default is that content fields are E2E (operator-blind); `.meta()` opts THIS field OUT to plaintext,
|
|
@@ -385,6 +397,9 @@ export interface Field<T = unknown, K extends FieldKind = FieldKind> {
|
|
|
385
397
|
withFacet<Fa extends FieldFacet>(facet: Fa): Field<T, K> & { readonly facet: Fa };
|
|
386
398
|
/** Mark this field's value ENCRYPTED (sets {@link Field.isEncrypted}). Chainable + non-destructive. */
|
|
387
399
|
encrypted(): Field<T, K>;
|
|
400
|
+
/** Keep AUTHOR PROVENANCE beside this field's folded value (sets {@link Field.isAttributed}) —
|
|
401
|
+
* `<field>Provenance` accessors are generated on the read models. Lww-class scalars only. */
|
|
402
|
+
attributed(): Field<T, K>;
|
|
388
403
|
/** Mark this field plaintext METADATA (sets {@link Field.isMeta}) — the indexable opt-out on an owned aggregate. */
|
|
389
404
|
meta(): Field<T, K>;
|
|
390
405
|
/**
|
|
@@ -432,6 +447,7 @@ function makeField<T, K extends FieldKind = FieldKind>(
|
|
|
432
447
|
valueObject?: ValueObjectFieldMeta;
|
|
433
448
|
mapValueVo?: ValueObjectFieldMeta;
|
|
434
449
|
encrypted?: true;
|
|
450
|
+
attributed?: true;
|
|
435
451
|
meta?: true;
|
|
436
452
|
facet?: FieldFacet;
|
|
437
453
|
nullSemantic?: NullSemantic;
|
|
@@ -456,6 +472,7 @@ function makeField<T, K extends FieldKind = FieldKind>(
|
|
|
456
472
|
...(extra.valueObject !== undefined ? { valueObject: extra.valueObject } : {}),
|
|
457
473
|
...(extra.mapValueVo !== undefined ? { mapValueVo: extra.mapValueVo } : {}),
|
|
458
474
|
...(extra.encrypted ? { isEncrypted: true as const } : {}),
|
|
475
|
+
...(extra.attributed ? { isAttributed: true as const } : {}),
|
|
459
476
|
...(extra.meta ? { isMeta: true as const } : {}),
|
|
460
477
|
...(extra.fromBirth ? { isFromBirth: true as const } : {}),
|
|
461
478
|
...(extra.nullSemantic !== undefined ? { nullSemantic: extra.nullSemantic } : {}),
|
|
@@ -513,6 +530,12 @@ function makeField<T, K extends FieldKind = FieldKind>(
|
|
|
513
530
|
// `nomosEncryption` era key, built by aggregate()/manifest, is what carries the fact.
|
|
514
531
|
return makeField<T, K>(kind, zod, driver, { ...extra, encrypted: true });
|
|
515
532
|
},
|
|
533
|
+
attributed(): Field<T, K> {
|
|
534
|
+
// Carry the flag through merge/optional (extra is threaded), so `.merge(Lww).attributed()`
|
|
535
|
+
// and `.attributed().merge(Lww)` agree. aggregate() validates Lww-class eligibility and
|
|
536
|
+
// wraps the wire driver in `{"Attributed": <inner>}` (the law-era mechanism).
|
|
537
|
+
return makeField<T, K>(kind, zod, driver, { ...extra, attributed: true });
|
|
538
|
+
},
|
|
516
539
|
meta(): Field<T, K> {
|
|
517
540
|
// Plaintext-metadata opt-out (owned aggregates) — threaded through merge/optional like `encrypted`.
|
|
518
541
|
// Driver/schema unchanged; the fact rides the aggregate's audience surface, not the wire kind.
|
package/src/manifest.ts
CHANGED
|
@@ -107,6 +107,14 @@ export interface CanonicalAggregate {
|
|
|
107
107
|
* so adding/changing it MOVES the hash. The scope is the aggregate's `requiresRead`.
|
|
108
108
|
*/
|
|
109
109
|
readonly encryptedFields?: string[];
|
|
110
|
+
/**
|
|
111
|
+
* ATTRIBUTED READS (architecture/field_provenance.md) — the STORED field names whose
|
|
112
|
+
* folded values keep author provenance (`.attributed()`), SORTED. PRESENT only when
|
|
113
|
+
* ≥1 field is attributed; OMITTED otherwise (an unattributed aggregate is
|
|
114
|
+
* byte-identical). The hash-bearing LAW is the wrapped `{"Attributed": <inner>}`
|
|
115
|
+
* driver in `schema`; this key is the self-describing mirror codegen/openapi read.
|
|
116
|
+
*/
|
|
117
|
+
readonly attributedFields?: string[];
|
|
110
118
|
}
|
|
111
119
|
|
|
112
120
|
/** One captured directive contract — NO `plan`/`scopeFrom` behaviour. */
|
|
@@ -1057,6 +1065,9 @@ export function domainManifest(mod: DomainModule): CanonicalManifest {
|
|
|
1057
1065
|
// E2E ENCRYPTION (slice 2) — the encrypted field names. Hash-bearing law;
|
|
1058
1066
|
// omit-when-none so plaintext aggregates are byte-stable.
|
|
1059
1067
|
...(agg.encryptedFields !== undefined ? { encryptedFields: [...agg.encryptedFields] } : {}),
|
|
1068
|
+
// ATTRIBUTED READS — the attributed field names (the driver wrap in `schema` is
|
|
1069
|
+
// the hash-bearing law; this mirror is what codegen reads). Omit-when-none.
|
|
1070
|
+
...(agg.attributedFields !== undefined ? { attributedFields: [...agg.attributedFields] } : {}),
|
|
1060
1071
|
};
|
|
1061
1072
|
})
|
|
1062
1073
|
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
package/src/usd.ts
CHANGED
|
@@ -238,6 +238,21 @@ export interface UsdLayer {
|
|
|
238
238
|
* as `queries`/`references`. Hand-adding it to a foreign bundle only opts IN to
|
|
239
239
|
* stricter checking; removing it makes new law, for that law's own chains (status quo).
|
|
240
240
|
*/
|
|
241
|
+
/**
|
|
242
|
+
* THE ATTRIBUTED-READS DECLARATION (architecture/field_provenance.md — "the law is the
|
|
243
|
+
* era"). Present IFF the layer declares ≥1 `.attributed()` field: the law's EXPLICIT
|
|
244
|
+
* opt-in to per-field author-provenance capture at the one gate/fold. The hash-bearing
|
|
245
|
+
* enforcement rides the aggregate schema's `{"Attributed": <inner>}` wire driver (a
|
|
246
|
+
* pre-provenance kernel's strict manifest serde REFUSES it — the clean era refusal);
|
|
247
|
+
* this key is the SELF-DESCRIBING surface (which aggregates/fields are attributed)
|
|
248
|
+
* hosts and tooling read without re-deriving it from drivers. OMITTED for
|
|
249
|
+
* unattributed law — byte-identical hash, the `nomosInvariantGate` discipline.
|
|
250
|
+
*/
|
|
251
|
+
readonly nomosAttributedGate?: {
|
|
252
|
+
readonly v: 1;
|
|
253
|
+
/** aggregate TYPE → its attributed STORED field names, keys + fields sorted. */
|
|
254
|
+
readonly aggregates: Record<string, string[]>;
|
|
255
|
+
};
|
|
241
256
|
readonly nomosInvariantGate?: {
|
|
242
257
|
readonly v: 1;
|
|
243
258
|
/** Aggregate types whose declared `invariant` bodies the gate executes, sorted. */
|
|
@@ -1332,8 +1347,14 @@ export function emitUsd(
|
|
|
1332
1347
|
// (byte-determinism); omit-when-empty so an all-public layer is byte-identical.
|
|
1333
1348
|
const visAggregates: Record<string, { requires: string }> = {};
|
|
1334
1349
|
const encAggregates: Record<string, { scope: string; fields: string[]; delegatable: false }> = {};
|
|
1350
|
+
// ATTRIBUTED READS (field_provenance.md): aggregate TYPE → its attributed field
|
|
1351
|
+
// names — the self-describing nomosAttributedGate era key. Omit-when-empty.
|
|
1352
|
+
const attrAggregates: Record<string, string[]> = {};
|
|
1335
1353
|
for (const a of [...manifest.aggregates].sort((x, y) => (x.id < y.id ? -1 : x.id > y.id ? 1 : 0))) {
|
|
1336
1354
|
if (a.requiresRead !== undefined) visAggregates[a.id] = { requires: a.requiresRead };
|
|
1355
|
+
if (a.attributedFields !== undefined && a.attributedFields.length > 0) {
|
|
1356
|
+
attrAggregates[a.id] = [...a.attributedFields];
|
|
1357
|
+
}
|
|
1337
1358
|
// THE ENCRYPTION GATE (slice 2): an aggregate with encrypted field(s) — its scope
|
|
1338
1359
|
// is its read cap (aggregate()/the gate guarantee they coincide). delegatable:false.
|
|
1339
1360
|
if (a.encryptedFields !== undefined && a.encryptedFields.length > 0 && a.requiresRead !== undefined) {
|
|
@@ -1486,6 +1507,9 @@ export function emitUsd(
|
|
|
1486
1507
|
...(Object.keys(encAggregates).length > 0
|
|
1487
1508
|
? { nomosEncryption: { v: 1 as const, aggregates: encAggregates } }
|
|
1488
1509
|
: {}),
|
|
1510
|
+
...(Object.keys(attrAggregates).length > 0
|
|
1511
|
+
? { nomosAttributedGate: { v: 1 as const, aggregates: attrAggregates } }
|
|
1512
|
+
: {}),
|
|
1489
1513
|
...(nomosCapabilities.length > 0
|
|
1490
1514
|
? { nomosCapabilities: { v: 1 as const, capabilities: nomosCapabilities } }
|
|
1491
1515
|
: {}),
|
package/src/usd_layers.ts
CHANGED
|
@@ -111,6 +111,8 @@ export function driverToken(driver: WireDriver): string {
|
|
|
111
111
|
if ("LwwBy" in driver) return `LwwBy(${driver.LwwBy})`;
|
|
112
112
|
// `Surfaced(<inner>)` — the user-resolvable posture stays legible in the tree too.
|
|
113
113
|
if ("Surfaced" in driver) return `Surfaced(${driverToken(driver.Surfaced)})`;
|
|
114
|
+
// `Attributed(<inner>)` — attributed reads (field_provenance.md), legible in the tree.
|
|
115
|
+
if ("Attributed" in driver) return `Attributed(${driverToken(driver.Attributed)})`;
|
|
114
116
|
return `MapOf(${driverToken(driver.MapOf)})`;
|
|
115
117
|
}
|
|
116
118
|
|
|
@@ -122,6 +124,9 @@ export function parseDriverToken(token: string): WireDriver {
|
|
|
122
124
|
if (token.startsWith("Surfaced(") && token.endsWith(")")) {
|
|
123
125
|
return { Surfaced: parseDriverToken(token.slice("Surfaced(".length, -1)) };
|
|
124
126
|
}
|
|
127
|
+
if (token.startsWith("Attributed(") && token.endsWith(")")) {
|
|
128
|
+
return { Attributed: parseDriverToken(token.slice("Attributed(".length, -1)) };
|
|
129
|
+
}
|
|
125
130
|
// `LwwBy(<principal>)` — the principal is an opaque non-empty string (uids carry
|
|
126
131
|
// hyphens, so it is NOT the bare-identifier shape the unit-driver branch validates).
|
|
127
132
|
if (token.startsWith("LwwBy(") && token.endsWith(")")) {
|
|
@@ -175,6 +180,9 @@ function domainLayerAttrLines(domain: string, layer: UsdLayer, indent: string):
|
|
|
175
180
|
...(layer.nomosEncryption !== undefined
|
|
176
181
|
? [`${indent}custom string nomos:encryptionHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosEncryption)))}`]
|
|
177
182
|
: []),
|
|
183
|
+
...(layer.nomosAttributedGate !== undefined
|
|
184
|
+
? [`${indent}custom string nomos:attributedGateHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosAttributedGate)))}`]
|
|
185
|
+
: []),
|
|
178
186
|
...(layer.nomosRelations !== undefined
|
|
179
187
|
? [`${indent}custom string nomos:relationsHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosRelations)))}`]
|
|
180
188
|
: []),
|
|
@@ -767,6 +775,21 @@ export function flattenLayeredUsd(layers: readonly UsdLayer[]): UsdDocument {
|
|
|
767
775
|
encryption = { v: 1, aggregates: aggregatesRec };
|
|
768
776
|
}
|
|
769
777
|
}
|
|
778
|
+
// THE ATTRIBUTED-READS GATE (field_provenance.md): per-aggregate attributed-field
|
|
779
|
+
// sets merged later-wins, SORTED keys (matching the composed emission). Omit-when-empty.
|
|
780
|
+
let attributedGate: UsdLayer["nomosAttributedGate"];
|
|
781
|
+
{
|
|
782
|
+
const a = new Map<string, string[]>();
|
|
783
|
+
for (const l of stack) {
|
|
784
|
+
if (l.nomosAttributedGate === undefined) continue;
|
|
785
|
+
for (const [type, fields] of Object.entries(l.nomosAttributedGate.aggregates)) a.set(type, fields);
|
|
786
|
+
}
|
|
787
|
+
if (a.size > 0) {
|
|
788
|
+
const aggregatesRec: Record<string, string[]> = {};
|
|
789
|
+
for (const k of [...a.keys()].sort()) aggregatesRec[k] = a.get(k)!;
|
|
790
|
+
attributedGate = { v: 1, aggregates: aggregatesRec };
|
|
791
|
+
}
|
|
792
|
+
}
|
|
770
793
|
// THE RELATION SCHEMA (P1): per-type relation defs merged later-wins across the stack,
|
|
771
794
|
// SORTED keys (matching the composed emission). Omit-when-empty (the nomosRelations era rule).
|
|
772
795
|
let relations: UsdLayer["nomosRelations"];
|
|
@@ -1002,6 +1025,7 @@ return {
|
|
|
1002
1025
|
...(readGate !== undefined ? { nomosReadGate: readGate } : {}),
|
|
1003
1026
|
...(readVisibility !== undefined ? { nomosReadVisibility: readVisibility } : {}),
|
|
1004
1027
|
...(encryption !== undefined ? { nomosEncryption: encryption } : {}),
|
|
1028
|
+
...(attributedGate !== undefined ? { nomosAttributedGate: attributedGate } : {}),
|
|
1005
1029
|
...(relations !== undefined ? { nomosRelations: relations } : {}),
|
|
1006
1030
|
...(relationNamespace !== undefined ? { nomosRelationNamespace: relationNamespace } : {}),
|
|
1007
1031
|
...(readProjectionNamespace !== undefined ? { nomosReadProjectionNamespace: readProjectionNamespace } : {}),
|
|
@@ -1143,6 +1167,7 @@ export function parseUsdaDocument(text: string): UsdDocument {
|
|
|
1143
1167
|
const readGateByDomain = new Map<string, UsdLayer["nomosReadGate"]>();
|
|
1144
1168
|
const readVisibilityByDomain = new Map<string, UsdLayer["nomosReadVisibility"]>();
|
|
1145
1169
|
const encryptionByDomain = new Map<string, UsdLayer["nomosEncryption"]>();
|
|
1170
|
+
const attributedGateByDomain = new Map<string, UsdLayer["nomosAttributedGate"]>();
|
|
1146
1171
|
const relationsByDomain = new Map<string, UsdLayer["nomosRelations"]>();
|
|
1147
1172
|
const relationGateByDomain = new Map<string, UsdLayer["nomosRelationGate"]>();
|
|
1148
1173
|
const relationNamespaceByDomain = new Map<string, UsdLayer["nomosRelationNamespace"]>();
|
|
@@ -1193,6 +1218,11 @@ export function parseUsdaDocument(text: string): UsdDocument {
|
|
|
1193
1218
|
if (typeof encHex === "string") {
|
|
1194
1219
|
encryptionByDomain.set(domain, JSON.parse(unhexUtf8(encHex)) as UsdLayer["nomosEncryption"]);
|
|
1195
1220
|
}
|
|
1221
|
+
// THE ATTRIBUTED-READS GATE (field_provenance.md): the hex-carried per-aggregate map.
|
|
1222
|
+
const attrHex = p.attrs.get("nomos:attributedGateHex");
|
|
1223
|
+
if (typeof attrHex === "string") {
|
|
1224
|
+
attributedGateByDomain.set(domain, JSON.parse(unhexUtf8(attrHex)) as UsdLayer["nomosAttributedGate"]);
|
|
1225
|
+
}
|
|
1196
1226
|
// THE RELATION SCHEMA (P1): the hex-carried per-type relation rewrite map.
|
|
1197
1227
|
const relHex = p.attrs.get("nomos:relationsHex");
|
|
1198
1228
|
if (typeof relHex === "string") {
|
|
@@ -1359,6 +1389,7 @@ export function parseUsdaDocument(text: string): UsdDocument {
|
|
|
1359
1389
|
const rg = readGateByDomain.get(domain);
|
|
1360
1390
|
const rv = readVisibilityByDomain.get(domain);
|
|
1361
1391
|
const enc = encryptionByDomain.get(domain);
|
|
1392
|
+
const attrGate = attributedGateByDomain.get(domain);
|
|
1362
1393
|
const rel = relationsByDomain.get(domain);
|
|
1363
1394
|
const relGate = relationGateByDomain.get(domain);
|
|
1364
1395
|
const relNs = relationNamespaceByDomain.get(domain);
|
|
@@ -1386,6 +1417,7 @@ export function parseUsdaDocument(text: string): UsdDocument {
|
|
|
1386
1417
|
...(rg !== undefined ? { nomosReadGate: rg } : {}),
|
|
1387
1418
|
...(rv !== undefined ? { nomosReadVisibility: rv } : {}),
|
|
1388
1419
|
...(enc !== undefined ? { nomosEncryption: enc } : {}),
|
|
1420
|
+
...(attrGate !== undefined ? { nomosAttributedGate: attrGate } : {}),
|
|
1389
1421
|
...(rel !== undefined ? { nomosRelations: rel } : {}),
|
|
1390
1422
|
...(relNs !== undefined ? { nomosRelationNamespace: relNs } : {}),
|
|
1391
1423
|
...(rpNs !== undefined ? { nomosReadProjectionNamespace: rpNs } : {}),
|
package/src/wire.ts
CHANGED
|
@@ -222,7 +222,11 @@ export type WireDriver =
|
|
|
222
222
|
| { MapOf: WireDriver }
|
|
223
223
|
// `Surfaced(Box<Driver>)` — the user-resolvable third merge posture
|
|
224
224
|
// (architecture/surfaced_merge_conflict.md): `{ "Surfaced": <inner> }`.
|
|
225
|
-
| { Surfaced: WireDriver }
|
|
225
|
+
| { Surfaced: WireDriver }
|
|
226
|
+
// `Attributed(Box<Driver>)` — attributed reads (architecture/field_provenance.md):
|
|
227
|
+
// `{ "Attributed": <inner> }`. Value semantics = the inner driver's; the wrapper makes
|
|
228
|
+
// the fold capture per-field author provenance.
|
|
229
|
+
| { Attributed: WireDriver };
|
|
226
230
|
|
|
227
231
|
/** `Schema = BTreeMap<FieldId, Driver>` — a plain object, field -> Driver. */
|
|
228
232
|
export type WireSchema = Record<string, WireDriver>;
|