@githolon/dsl 0.107.0 → 0.107.2

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.
@@ -1664,8 +1664,8 @@ const NOMOS_TYPES_DEP = "^0.8.8";
1664
1664
  // 0.48.x: session-bound clients (XClient.bind/NomosClientInterface/DomainClientSessionMismatch/
1665
1665
  // DomainNotInstalledForSession, engine.follow, pendingConflicts lanes) — the generated client
1666
1666
  // REQUIRES this surface. 0.48.1 fixes NomosSession.watchPendingConflicts delegation.
1667
- const NOMOS_CLIENT_DEP = "^0.64.40";
1668
- const NOMOS_FLUTTER_DEP = "^0.63.68";
1667
+ const NOMOS_CLIENT_DEP = "^0.64.46";
1668
+ const NOMOS_FLUTTER_DEP = "^0.63.76";
1669
1669
  // DRIFT TRIPWIRE: pubspec_pins.test.ts asserts these equal the CURRENT versions in dsl/dart/pubspec.yaml
1670
1670
  // and dsl/dart_types/pubspec.yaml — a Dart release without this bump fails the suite, so tenants never
1671
1671
  // land back in dependency_overrides (the exact co2 report this closes, third occurrence).
@@ -300,9 +300,15 @@ export function capability<
300
300
  // The loop is dynamic (declared field names), so the per-field `set()` typing
301
301
  // is asserted — the PUBLIC payload types stay fully inferred from the zod shape.
302
302
  writeParams: (taskId, p) =>
303
- Object.keys(spec.request).map((k) =>
304
- set(instance(taskAggregate, taskId), k as never, (p as Record<string, unknown>)[k] as never),
305
- ),
303
+ Object.keys(spec.request)
304
+ // ABSENT OPTIONALS ARE SKIPPED, symmetrically with the receipt half below: an optional
305
+ // request field the caller omitted must author NO op — `undefined` can never be ledger
306
+ // state, and writing it deterministically halts the author plan (caught live 2026-08-19:
307
+ // the first tempo-less oblige after the tempo law refused on `estimatedMs`).
308
+ .filter((k) => (p as Record<string, unknown>)[k] !== undefined)
309
+ .map((k) =>
310
+ set(instance(taskAggregate, taskId), k as never, (p as Record<string, unknown>)[k] as never),
311
+ ),
306
312
  // The receipt half, symmetrical with the request half above: declared fields are merged
307
313
  // onto the complete envelope as TYPED values and written straight through. The payload
308
314
  // boundary has already validated them against the same zod the author declared, so the
@@ -137,7 +137,7 @@ export const DomainInstallation = aggregate("DomainInstallation", {
137
137
  }).public();
138
138
 
139
139
  /**
140
- * The accepted, exact-history evidence for one activation. This is deliberately an
140
+ * The accepted current-frontier evidence for one activation. This is deliberately an
141
141
  * obscure framework concept: tenant models never author or name it. The generated
142
142
  * lifecycle client runs pre-flight and folds this receipt in the SAME intent that
143
143
  * activates the candidate, so recording evidence cannot make its own head anchor stale.
@@ -151,18 +151,7 @@ export const ReleaseReceipt = aggregate(RELEASE_RECEIPT_AGGREGATE, {
151
151
  domain: t.string().merge(Lww),
152
152
  atHash: t.gitObjectId().merge(Lww),
153
153
  atStateRoot: t.gitObjectId().merge(Lww),
154
- historyHash: t.sha256Digest().merge(Lww),
155
- reportHash: t.sha256Digest().merge(Lww),
156
- reportJson: t.string().merge(Lww),
157
- complete: t.bool(),
158
154
  evolveOk: t.bool(),
159
- rawPromotable: t.bool(),
160
- projectionEqual: t.bool(),
161
- replayedIntents: t.int().merge(Lww),
162
- candidateIntents: t.int().merge(Lww),
163
- divergenceCount: t.int().merge(Lww),
164
- baselineRoot: t.sha256Digest().merge(Lww),
165
- candidateRoot: t.sha256Digest().merge(Lww),
166
155
  acceptanceReason: t.string().merge(Lww).optional(),
167
156
  acceptedBy: attributionField(t.string().merge(Lww)),
168
157
  acceptedAt: t.int().merge(Lww),
@@ -176,12 +165,12 @@ function domainInstallationInstance(domainHash: Sha256Digest) {
176
165
  return instance(DomainInstallation, domainInstallationAggregateId(domainHash));
177
166
  }
178
167
 
179
- export function releaseReceiptAggregateId(reportHash: Sha256Digest): string {
180
- return `${RELEASE_RECEIPT_PREFIX}${reportHash}`;
168
+ export function releaseReceiptAggregateId(candidateHash: Sha256Digest, atStateRoot: string): string {
169
+ return `${RELEASE_RECEIPT_PREFIX}${candidateHash}:${atStateRoot}`;
181
170
  }
182
171
 
183
- function releaseReceiptInstance(reportHash: Sha256Digest) {
184
- return instance(ReleaseReceipt, releaseReceiptAggregateId(reportHash));
172
+ function releaseReceiptInstance(candidateHash: Sha256Digest, atStateRoot: string) {
173
+ return instance(ReleaseReceipt, releaseReceiptAggregateId(candidateHash, atStateRoot));
185
174
  }
186
175
 
187
176
  const dispositions = z.object({
@@ -198,28 +187,9 @@ const preflightReceipt = z.object({
198
187
  domain: z.string().min(1),
199
188
  atHash: gitOid,
200
189
  atStateRoot: gitOid,
201
- historyHash: lowerHex64,
202
- reportHash: lowerHex64,
203
- reportJson: z.string().min(2),
204
- complete: z.literal(true),
205
190
  evolveOk: z.literal(true),
206
- rawPromotable: z.boolean(),
207
- projectionEqual: z.boolean(),
208
- replayedIntents: z.number().int().nonnegative(),
209
- candidateIntents: z.number().int().nonnegative(),
210
- divergenceCount: z.number().int().nonnegative(),
211
- baselineRoot: lowerHex64,
212
- candidateRoot: lowerHex64,
213
191
  acceptanceReason: z.string().trim().min(1).optional(),
214
- }).superRefine((receipt, ctx) => {
215
- if (!receipt.rawPromotable && receipt.acceptanceReason === undefined) {
216
- ctx.addIssue({
217
- code: z.ZodIssueCode.custom,
218
- path: ["acceptanceReason"],
219
- message: "a divergent but complete report needs an explicit business acceptance reason",
220
- });
221
- }
222
- });
192
+ }).strict();
223
193
 
224
194
  const installPackagePayload = {
225
195
  domainHash: lowerHex64,
@@ -317,7 +287,7 @@ export const promoteDomain = directive(DOMAIN_LIFECYCLE_DIRECTIVES.Promote)
317
287
  }))
318
288
  .plan((p, ctx): PlannedOp[] => {
319
289
  const promoted = domainInstallationInstance(p.domainHash);
320
- const receipt = releaseReceiptInstance(p.preflight.reportHash);
290
+ const receipt = releaseReceiptInstance(p.domainHash, p.preflight.atStateRoot);
321
291
  const ops: PlannedOp[] = [
322
292
  set(promoted, "status.phase", DomainInstallationPhase.Active),
323
293
  set(promoted, "status.observedGeneration", 1),
@@ -326,18 +296,7 @@ export const promoteDomain = directive(DOMAIN_LIFECYCLE_DIRECTIVES.Promote)
326
296
  set(receipt, "domain", p.preflight.domain),
327
297
  set(receipt, "atHash", p.preflight.atHash),
328
298
  set(receipt, "atStateRoot", p.preflight.atStateRoot),
329
- set(receipt, "historyHash", p.preflight.historyHash),
330
- set(receipt, "reportHash", p.preflight.reportHash),
331
- set(receipt, "reportJson", p.preflight.reportJson),
332
- set(receipt, "complete", true),
333
299
  set(receipt, "evolveOk", true),
334
- set(receipt, "rawPromotable", p.preflight.rawPromotable),
335
- set(receipt, "projectionEqual", p.preflight.projectionEqual),
336
- set(receipt, "replayedIntents", p.preflight.replayedIntents),
337
- set(receipt, "candidateIntents", p.preflight.candidateIntents),
338
- set(receipt, "divergenceCount", p.preflight.divergenceCount),
339
- set(receipt, "baselineRoot", p.preflight.baselineRoot),
340
- set(receipt, "candidateRoot", p.preflight.candidateRoot),
341
300
  ...(p.preflight.acceptanceReason !== undefined
342
301
  ? [set(receipt, "acceptanceReason", p.preflight.acceptanceReason)]
343
302
  : []),
@@ -380,7 +339,7 @@ export const rollbackDomain = directive(DOMAIN_LIFECYCLE_DIRECTIVES.Rollback)
380
339
  .plan((p, ctx): PlannedOp[] => {
381
340
  const candidate = domainInstallationInstance(p.candidateHash);
382
341
  const prior = domainInstallationInstance(p.priorHash);
383
- const receipt = releaseReceiptInstance(p.preflight.reportHash);
342
+ const receipt = releaseReceiptInstance(p.priorHash, p.preflight.atStateRoot);
384
343
  return [
385
344
  set(candidate, "status.phase", DomainInstallationPhase.Disabled),
386
345
  set(candidate, "status.supersededBy", p.priorHash),
@@ -391,18 +350,7 @@ export const rollbackDomain = directive(DOMAIN_LIFECYCLE_DIRECTIVES.Rollback)
391
350
  set(receipt, "domain", p.preflight.domain),
392
351
  set(receipt, "atHash", p.preflight.atHash),
393
352
  set(receipt, "atStateRoot", p.preflight.atStateRoot),
394
- set(receipt, "historyHash", p.preflight.historyHash),
395
- set(receipt, "reportHash", p.preflight.reportHash),
396
- set(receipt, "reportJson", p.preflight.reportJson),
397
- set(receipt, "complete", true),
398
353
  set(receipt, "evolveOk", true),
399
- set(receipt, "rawPromotable", p.preflight.rawPromotable),
400
- set(receipt, "projectionEqual", p.preflight.projectionEqual),
401
- set(receipt, "replayedIntents", p.preflight.replayedIntents),
402
- set(receipt, "candidateIntents", p.preflight.candidateIntents),
403
- set(receipt, "divergenceCount", p.preflight.divergenceCount),
404
- set(receipt, "baselineRoot", p.preflight.baselineRoot),
405
- set(receipt, "candidateRoot", p.preflight.candidateRoot),
406
354
  ...(p.preflight.acceptanceReason !== undefined
407
355
  ? [set(receipt, "acceptanceReason", p.preflight.acceptanceReason)]
408
356
  : []),
@@ -0,0 +1,10 @@
1
+ // NOMOS — canonical framework governance relation identities.
2
+ //
3
+ // These names are global, kernel-coupled vocabulary. Coin them once so DSL
4
+ // sugar cannot accidentally redeclare them as tenant-local relations and cause
5
+ // the compiler to namespace gates away from the framework's founding tuples.
6
+ import { relationRef } from "../relation_schema.js";
7
+
8
+ /** The global workspace authority seated at `workspace:self#admin`. */
9
+ export const WORKSPACE_ADMIN_RELATION = relationRef("admin");
10
+
@@ -77,6 +77,7 @@ import { read } from "../read.js";
77
77
  import { readRef, readSet, workspaceInvariant, refAs, type WorkspaceInvariantVerdict } from "./workspace_invariant.js";
78
78
  import { writeTuple, removeTuple, RelationTuple, relationTupleInstanceId } from "./relations.js";
79
79
  import { relationType, self, user, key } from "../relation_schema.js";
80
+ import { WORKSPACE_ADMIN_RELATION } from "./governance_relations.js";
80
81
  import { GENESIS_WARRANT_ID } from "./genesis.js";
81
82
  import { idOf } from "../value_object.js";
82
83
  import { enrollSigner, signerObject, SIGNER_RELATION } from "./signing_identity.js";
@@ -191,7 +192,7 @@ export const workspaceRelations = relationType("workspace")
191
192
  // migration. (A TENANT that wants a workspace-scoped role uses a namespaced `relationType("workspace")`
192
193
  // + a `selfWorkspace()` seed; only THIS framework governance type is global.)
193
194
  .asGlobal()
194
- .relation("admin", { rewrite: self(), allow: [user(), key()] })
195
+ .relation(WORKSPACE_ADMIN_RELATION, { rewrite: self(), allow: [user(), key()] })
195
196
  .relation("creator", { rewrite: self(), allow: [user(), key()] })
196
197
  .relation("platformCreator", { rewrite: self(), allow: [user(), key()] });
197
198
 
package/src/role.ts CHANGED
@@ -14,8 +14,9 @@
14
14
  * `relationType("workspace")` block (the fail-open footgun is CLOSED — a `.requires(role.edit)`
15
15
  * always finds a declared relation), and the `<role>View` relation is a READ-VISIBILITY cap
16
16
  * (used as an aggregate `requires(role.view)` — usd.ts synthesizes the global workspace read
17
- * relation, exactly as any private aggregate does). The grant-authority relation (`grantableBy`,
18
- * default `admin`) is likewise authored into the schema so the gate on the grant directive has teeth.
17
+ * relation, exactly as any private aggregate does). A custom grant-authority relation is likewise
18
+ * authored locally. The default `admin` instead refers to the framework's existing GLOBAL
19
+ * `workspace:self#admin`; redeclaring it locally would namespace the gate away from its founding tuple.
19
20
  *
20
21
  * 2. GRANT / SEED DIRECTIVES per role — `grant<Role>` / `revoke<Role>` fold the relation TUPLE via
21
22
  * the existing `writeTuple`/`removeTuple` primitives (a grant seats `<role>Edit` + `<role>View`
@@ -41,6 +42,7 @@ import type { AggregateHandle } from "./aggregate.js";
41
42
  import type { Directive, RequirableDirective } from "./directive.js";
42
43
  import { directive } from "./directive.js";
43
44
  import { RelationTuple, writeTuple, removeTuple } from "./framework/relations.js";
45
+ import { WORKSPACE_ADMIN_RELATION } from "./framework/governance_relations.js";
44
46
  import { relationType, self, user, key, type RelationTypeDecl } from "./relation_schema.js";
45
47
 
46
48
  /** A role name: kebab-case (`editor`, `site-admin`) — lowered to relation/directive labels. */
@@ -234,7 +236,7 @@ export function role(name: string): RoleBuilder {
234
236
  name,
235
237
  editRelation: `${base}Edit`,
236
238
  viewRelation: `${base}View`,
237
- grantableBy: "admin",
239
+ grantableBy: WORKSPACE_ADMIN_RELATION,
238
240
  editAggregates: [],
239
241
  viewAggregates: [],
240
242
  canEdit: false,
@@ -256,8 +258,10 @@ const ROLE_RELATIONS_KEY = "__nomosRoleRelations";
256
258
  * EXPANDER (the `capability()`/birth-meter seam): given a module's exports bag, LIFT every `RoleEmission`
257
259
  * into discoverable entries — the grant/revoke/seed directives become top-level exports (so `directivesOf`
258
260
  * finds them), and ONE synthesized `relationType("workspace")` (the `<role>Edit` write relations + the
259
- * distinct `grantableBy` grant-authority relations, deduped) becomes a discoverable relation type (so
260
- * `.requires(role.edit)` / `.requires(grantableBy)` have TEETH — the fail-open footgun is closed). The
261
+ * distinct LOCAL `grantableBy` grant-authority relations, deduped) becomes a discoverable relation type
262
+ * (so `.requires(role.edit)` / `.requires(customGrantAuthority)` have TEETH — the fail-open footgun is
263
+ * closed). The default framework `admin` authority is already declared by the global governance schema
264
+ * and MUST NOT be redeclared here as tenant-local. The
261
265
  * `<role>View` read relations are NOT authored here: they are read-visibility caps, synthesized by usd.ts
262
266
  * from the aggregates that declare `requires(role.view)`. IDEMPOTENT + additive: a bag with no role is
263
267
  * returned unchanged (byte-identical), and re-running on an already-expanded bag is a no-op.
@@ -281,12 +285,14 @@ export function expandRoleExports(mod: Record<string, unknown>): Record<string,
281
285
  };
282
286
  for (const e of emissions) {
283
287
  if (e.meta.editRelation) declare(e.meta.editRelation);
284
- declare(e.meta.grantableBy);
288
+ if (e.meta.grantableBy !== WORKSPACE_ADMIN_RELATION) declare(e.meta.grantableBy);
285
289
  for (const dir of e.directives) {
286
290
  const anyDir = dir as { id: string };
287
291
  out[`__role_${anyDir.id}`] = dir;
288
292
  }
289
293
  }
290
- out[ROLE_RELATIONS_KEY] = builder as unknown as RelationTypeDecl;
294
+ // Keep the marker even for a view-only role using global admin so expansion stays idempotent without
295
+ // publishing an empty relation schema declaration.
296
+ out[ROLE_RELATIONS_KEY] = declared.size > 0 ? builder as unknown as RelationTypeDecl : true;
291
297
  return out;
292
298
  }