@githolon/dsl 0.105.23 → 0.106.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/CHANGELOG.md +15 -0
- package/dart/lib/src/runner/node-entry.mjs +4550 -279
- package/dart/lib/src/runner/offline-install.mjs +4354 -122
- package/dart/lib/src/test_harness.dart +316 -235
- package/dart/lib/src/webview_bridge.dart +1478 -1087
- package/dart/pubspec.lock +1 -1
- package/dart/pubspec.yaml +2 -2
- package/dart_types/CHANGELOG.md +5 -0
- package/dart_types/lib/src/errors.dart +78 -58
- package/dart_types/pubspec.yaml +1 -1
- package/package.json +1 -1
- package/runtime/generated-application-host.js +4439 -229
- package/src/aggregate.ts +21 -2
- package/src/capability_exports.ts +1 -1
- package/src/codegen_dart_client.ts +7 -10
- package/src/codegen_ts.ts +3 -6
- package/src/compat_manifest.ts +6 -7
- package/src/compile_package_main.ts +9 -11
- package/src/framework/capability.ts +20 -0
- package/src/framework/impure_capability.ts +131 -0
- package/src/framework/owned_custody.ts +4 -0
- package/src/framework/sync_lifecycle.ts +1 -0
- package/src/index.ts +9 -11
- package/src/kernel_capabilities.ts +79 -237
package/src/aggregate.ts
CHANGED
|
@@ -61,14 +61,20 @@ const READ_CAP_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$/;
|
|
|
61
61
|
/**
|
|
62
62
|
* THE READ AUDIENCE (secure-by-default rework — `architecture/secure_by_default_dsl.md`). Every
|
|
63
63
|
* aggregate DECLARES who may read it; there is no silent default (the compiler refuses an undeclared
|
|
64
|
-
* aggregate).
|
|
64
|
+
* aggregate). Four choices:
|
|
65
65
|
* • `public` — anyone reads (plaintext). The deliberate exception.
|
|
66
66
|
* • `readableBy` — holders of `cap` read (gate-enforced). Sugar over the shipped read-visibility
|
|
67
67
|
* (`requires(cap)`); per-field `.secret()` escalates to team-E2E.
|
|
68
|
+
* • `ownerReadable` — the workspace owner reads (plaintext, gate-enforced). Owner-only WITHOUT the
|
|
69
|
+
* confidentiality of `owned`: authorization and encryption are separable concerns,
|
|
70
|
+
* and a denormalised access index / discovery card needs the first, not the second.
|
|
71
|
+
* Gates on the framework's workspace-level owner-read relation, which every workspace
|
|
72
|
+
* already holds from birth — so an existing chain adopts it with no backfill and the
|
|
73
|
+
* operator can still serve the plaintext. This is `owned` minus the encryption pass.
|
|
68
74
|
* • `owned` — the owner reads; owner = the verified authoring principal, stamped into the
|
|
69
75
|
* obscure framework field. The read rule is record-grained so later sharing can
|
|
70
76
|
* extend one record without changing this declaration; content is operator-blind.
|
|
71
|
-
* `.public()`/`.readableBy(cap)`/`.owned()` are the fluent declarations on the
|
|
77
|
+
* `.public()`/`.readableBy(cap)`/`.ownerReadable()`/`.owned()` are the fluent declarations on the handle.
|
|
72
78
|
*/
|
|
73
79
|
export type Audience =
|
|
74
80
|
| { readonly kind: "public" }
|
|
@@ -320,6 +326,11 @@ export interface AudienceMethods<Id extends string, F extends Record<string, Fie
|
|
|
320
326
|
public(): NameableHandle<Id, F>;
|
|
321
327
|
/** Holders of `capability` may read (gate-enforced) — sugar over `requires(cap)`. */
|
|
322
328
|
readableBy(capability: string): NameableHandle<Id, F>;
|
|
329
|
+
/** The workspace owner may read (plaintext, gate-enforced). Owner-only authorization WITHOUT `owned`'s
|
|
330
|
+
* operator-blind encryption — for a denormalised access index / discovery card the operator legitimately
|
|
331
|
+
* serves. Gates on the framework owner-read relation every workspace holds from birth, so it adopts on an
|
|
332
|
+
* existing chain with no backfill. Tenant code never names the framework relation. See {@link owned}. */
|
|
333
|
+
ownerReadable(): NameableHandle<Id, F>;
|
|
323
334
|
/** The verified creator owns this record. Nomos stamps ownership, seats its per-record read relation and
|
|
324
335
|
* encrypts every non-`.meta()` content field; tenant code never names the framework field/relation. */
|
|
325
336
|
owned(): NameableHandle<Id, F>;
|
|
@@ -679,6 +690,14 @@ export function aggregate<const Id extends string, F extends Record<string, Fiel
|
|
|
679
690
|
const v = requires(cap); // validates the capability name
|
|
680
691
|
return build({ kind: "readableBy", cap: v.requires }, v, contracts, dartNameDecl);
|
|
681
692
|
},
|
|
693
|
+
// Owner-only, PLAINTEXT. Deliberately a `readableBy` audience gating on the framework owner-read
|
|
694
|
+
// relation — NOT `kind: "owned"`, so the encryption lowering below is skipped and content stays
|
|
695
|
+
// operator-readable. The relation is seated at workspace scope (`workspace:self#<rel>`) at birth
|
|
696
|
+
// (see framework/home.ts genesis), which is why an existing chain resolves it with no backfill.
|
|
697
|
+
// `owned()` is the same read gate PLUS per-record encryption; this is the confidentiality-free rung.
|
|
698
|
+
ownerReadable: () => {
|
|
699
|
+
return build({ kind: "readableBy", cap: OWNED_READ_RELATION }, requires(OWNED_READ_RELATION), contracts, dartNameDecl);
|
|
700
|
+
},
|
|
682
701
|
owned: () => {
|
|
683
702
|
return build({ kind: "owned", ownerField: OWNER_FIELD }, requires(OWNED_READ_RELATION), contracts, dartNameDecl);
|
|
684
703
|
},
|
|
@@ -57,7 +57,7 @@ export function capabilityDeclarationsOf(v: unknown): Record<string, unknown>[]
|
|
|
57
57
|
// `relations` carries the receipt fence's relation SCHEMA. It MUST expand with the bundle:
|
|
58
58
|
// `.requires(...)` is inert in a law that declares no relation schema, so omitting it would
|
|
59
59
|
// leave every capability looking fenced while enforcing nothing.
|
|
60
|
-
const PIECES = ["aggregate", "obligation", "complete", "fail", "block", "deadLetter", "tasks", "relations", "grantProvider", "revokeProvider"] as const;
|
|
60
|
+
const PIECES = ["aggregate", "obligation", "complete", "fail", "block", "deadLetter", "reschedule", "tasks", "relations", "grantProvider", "revokeProvider"] as const;
|
|
61
61
|
|
|
62
62
|
/**
|
|
63
63
|
* Expand every `capability()` bundle in an exports bag into discoverable
|
|
@@ -39,7 +39,7 @@ import { isGitObjectIdSchema, isSha256DigestSchema, type Field } from "./fields.
|
|
|
39
39
|
import type { Directive } from "./directive.js";
|
|
40
40
|
import type { QueryDecl } from "./query.js";
|
|
41
41
|
import type { DomainModule } from "./codegen_dart.js";
|
|
42
|
-
import {
|
|
42
|
+
import { requiredKernelContracts } from "./kernel_capabilities.js";
|
|
43
43
|
import {
|
|
44
44
|
cap,
|
|
45
45
|
readModelClassName,
|
|
@@ -352,7 +352,7 @@ export function generateDartBridgeClient(
|
|
|
352
352
|
})[domain];
|
|
353
353
|
const className = dartClientClassName(domain);
|
|
354
354
|
const hashConst = dartHashConstName(mod.name);
|
|
355
|
-
const
|
|
355
|
+
const kernelContracts = requiredKernelContracts([mod]);
|
|
356
356
|
const supportImport =
|
|
357
357
|
opts.supportImport === "package"
|
|
358
358
|
? "package:nomos_client/src/nms_generated_runtime.dart"
|
|
@@ -2360,10 +2360,9 @@ export function generateDartBridgeClient(
|
|
|
2360
2360
|
` /// THE STATIC INTERFACE DESCRIPTOR (session_bound_clients.md asks 5/9): what this client EXPECTS`,
|
|
2361
2361
|
` /// of the workspace it binds to — the domain key, the law hash it was generated against, and the`,
|
|
2362
2362
|
` /// structural interface (#72). Consumed by [bind] and by \`session.assertCompatible(${className}.interface)\`.`,
|
|
2363
|
-
` ///
|
|
2364
|
-
`
|
|
2365
|
-
|
|
2366
|
-
.map((f) => dartStr(f))
|
|
2363
|
+
` /// Standard OSGi Contract Namespace requirements derived from this law's shape.`,
|
|
2364
|
+
` static const List<Map<String, String>> kernelContracts = [${kernelContracts
|
|
2365
|
+
.map((contract) => `{${dartStr("namespace")}: ${dartStr(contract.namespace)}, ${dartStr("name")}: ${dartStr(contract.name)}, ${dartStr("version")}: ${dartStr(contract.version)}}`)
|
|
2367
2366
|
.join(", ")}];`,
|
|
2368
2367
|
``,
|
|
2369
2368
|
` static const NomosClientInterface interface = NomosClientInterface(`,
|
|
@@ -2459,10 +2458,8 @@ export function generateDartBridgeClient(
|
|
|
2459
2458
|
` final law = await session.lawStatus(interface.expectedDomainHash);`,
|
|
2460
2459
|
` if (law['drifted'] == true) await adoptGeneratedModel();`,
|
|
2461
2460
|
` await session.assertCompatible(interface);`,
|
|
2462
|
-
` // RELEASE COHERENCE:
|
|
2463
|
-
`
|
|
2464
|
-
` // up front — not an opaque deep-fold refusal. Best-effort (no-op if the cloud advertises no caps).`,
|
|
2465
|
-
` await session.assertKernelCoherence(requiresKernel, lawHash: ${hashConst});`,
|
|
2461
|
+
` // RELEASE COHERENCE: resolve this law's OSGi Contract Namespace requirements before use.`,
|
|
2462
|
+
` await session.assertKernelCoherence(kernelContracts, lawHash: ${hashConst});`,
|
|
2466
2463
|
...(ownedScope === undefined
|
|
2467
2464
|
? []
|
|
2468
2465
|
: [
|
package/src/codegen_ts.ts
CHANGED
|
@@ -1931,9 +1931,8 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
1931
1931
|
` checkCompat?(compat: { lawHash: string; interfaceHash: string; packageName: string }, requirement?: unknown): NomosCompatVerdict | null;`,
|
|
1932
1932
|
` /** Framework-private readiness hint; generated domain bindings keep first-use plan work off the offer path. */`,
|
|
1933
1933
|
` prepareDomain?(domain: string, packageHash: string): void;`,
|
|
1934
|
-
` /**
|
|
1935
|
-
`
|
|
1936
|
-
` assertKernelCoherence?(requiresKernel: readonly string[], opts?: { lawHash?: string }): unknown;`,
|
|
1934
|
+
` /** Resolve the law's standard OSGi Contract Namespace requirements before bind. */`,
|
|
1935
|
+
` assertKernelCoherence?(requirements: readonly { namespace: "osgi.contract"; name: string; version: string }[], opts?: { lawHash?: string }): unknown;`,
|
|
1937
1936
|
` /** The live compat verdict (set by checkCompat). \`has(memberId)\` is the dark-launch feature flag;`,
|
|
1938
1937
|
` * \`refresh()\` re-derives against the current projection. Absent until the factory binds. */`,
|
|
1939
1938
|
` compat?: NomosCompatVerdict;`,
|
|
@@ -3893,9 +3892,7 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
3893
3892
|
clientInterface !== undefined
|
|
3894
3893
|
? ` holon.checkCompat?.(${compatConstName(opts.packageName)}, ${interfaceConstName(opts.packageName)});`
|
|
3895
3894
|
: ` holon.checkCompat?.(${compatConstName(opts.packageName)});`,
|
|
3896
|
-
|
|
3897
|
-
// this law requires (e.g. compoundKeys) — instead of the opaque deep fold refusal later.
|
|
3898
|
-
` holon.assertKernelCoherence?.(${compatConstName(opts.packageName)}.requiresKernel ?? [], { lawHash: ${compatConstName(opts.packageName)}.lawHash });`,
|
|
3895
|
+
` holon.assertKernelCoherence?.(${compatConstName(opts.packageName)}.kernelContracts ?? [], { lawHash: ${compatConstName(opts.packageName)}.lawHash });`,
|
|
3899
3896
|
]
|
|
3900
3897
|
: []),
|
|
3901
3898
|
` return {`,
|
package/src/compat_manifest.ts
CHANGED
|
@@ -17,11 +17,10 @@ export interface NomosCompat {
|
|
|
17
17
|
readonly domains: Record<string, { readonly identityHash?: string }>;
|
|
18
18
|
/** The generator provenance — the `@githolon/dsl` version + this manifest's schema. */
|
|
19
19
|
readonly generator: { readonly dsl: string; readonly schema: number };
|
|
20
|
-
/**
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
readonly requiresKernel: readonly string[];
|
|
20
|
+
/** Standard OSGi Contract Namespace requirements derived from the law shape. */
|
|
21
|
+
readonly kernelContracts: readonly {
|
|
22
|
+
readonly namespace: "osgi.contract";
|
|
23
|
+
readonly name: string;
|
|
24
|
+
readonly version: string;
|
|
25
|
+
}[];
|
|
27
26
|
}
|
|
@@ -168,7 +168,7 @@ import { usdzPack } from "./usdz.js";
|
|
|
168
168
|
import { checkDomainModules, formatFindings, hasRefusal } from "./law_check.js";
|
|
169
169
|
import { currentEvolveLaw, evolveDiff, evolveLawFromLockfile } from "./evolve_preview.js";
|
|
170
170
|
import type { NomosCompat } from "./compat_manifest.js";
|
|
171
|
-
import {
|
|
171
|
+
import { requiredKernelContracts, type KernelContractProbeModule } from "./kernel_capabilities.js";
|
|
172
172
|
import { APPLICATION_HOME_DOMAIN } from "./framework/application_home_domain.js";
|
|
173
173
|
import { applicationHomePolicyLaw } from "./framework/application_home.js";
|
|
174
174
|
import { isApplicationHomeGenesis } from "./application_home_genesis.js";
|
|
@@ -1660,12 +1660,12 @@ const dartTypesImportFor = (generatedSource: string): string => {
|
|
|
1660
1660
|
* just the floor generated code needs — a lagging pin forces tenants into dependency_overrides. */
|
|
1661
1661
|
// 0.3.0: SurfacedValue<T>/SurfacedObservation/PendingConflictRow — generated Surfaced-field typing +
|
|
1662
1662
|
// the pendingConflicts read rows.
|
|
1663
|
-
const NOMOS_TYPES_DEP = "^0.8.
|
|
1663
|
+
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.
|
|
1668
|
-
const NOMOS_FLUTTER_DEP = "^0.63.
|
|
1667
|
+
const NOMOS_CLIENT_DEP = "^0.64.40";
|
|
1668
|
+
const NOMOS_FLUTTER_DEP = "^0.63.68";
|
|
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).
|
|
@@ -5193,17 +5193,15 @@ async function main(): Promise<void> {
|
|
|
5193
5193
|
const identityHash = identityHashes[key];
|
|
5194
5194
|
compatDomains[key] = identityHash === undefined ? {} : { identityHash };
|
|
5195
5195
|
}
|
|
5196
|
-
// RELEASE COHERENCE —
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
const requiresKernel = ((): string[] => {
|
|
5200
|
-
const probes: FeatureProbeModule[] = domainModules.map((m) => ({
|
|
5196
|
+
// RELEASE COHERENCE — typed OSGi Contract Namespace requirements derived from law shape.
|
|
5197
|
+
const kernelContracts = (() => {
|
|
5198
|
+
const probes: KernelContractProbeModule[] = domainModules.map((m) => ({
|
|
5201
5199
|
...(m.queries ? { queries: m.queries } : {}),
|
|
5202
5200
|
...(m.deriveds ? { deriveds: m.deriveds } : {}),
|
|
5203
5201
|
...(m.surfacedResolvers ? { surfacedResolvers: m.surfacedResolvers } : {}),
|
|
5204
5202
|
...(m.aggregates ? { aggregates: m.aggregates } : {}),
|
|
5205
5203
|
}));
|
|
5206
|
-
return
|
|
5204
|
+
return requiredKernelContracts(probes);
|
|
5207
5205
|
})();
|
|
5208
5206
|
const nomosCompat: NomosCompat = {
|
|
5209
5207
|
packageName: cfg.name,
|
|
@@ -5211,7 +5209,7 @@ async function main(): Promise<void> {
|
|
|
5211
5209
|
interfaceHash: sha256HexUtf8(canonicalJson(packageInterface)),
|
|
5212
5210
|
domains: compatDomains,
|
|
5213
5211
|
generator: { dsl: dslVersion, schema: 1 },
|
|
5214
|
-
|
|
5212
|
+
kernelContracts,
|
|
5215
5213
|
};
|
|
5216
5214
|
|
|
5217
5215
|
// ── 3b. NO OPENAPI SIDECAR. OpenAPI remains the sanctioned standard for describing this package's
|
|
@@ -215,6 +215,9 @@ export function capability<
|
|
|
215
215
|
scheduleAfterMs?: number;
|
|
216
216
|
/** Time from acceptance until the obligation is overdue. Defaults to 24h. */
|
|
217
217
|
deadlineAfterMs?: number;
|
|
218
|
+
/** How many times an overdue-but-still-`requested` obligation may be RE-OPENED in place by
|
|
219
|
+
* the bounded `reschedule` verb before the next reschedule dead-letters it. Defaults to 5. */
|
|
220
|
+
maxRescheduleAttempts?: number;
|
|
218
221
|
/**
|
|
219
222
|
* THE ACKNOWLEDGED WAIVER — open this capability's Receipts to any authorised author.
|
|
220
223
|
*
|
|
@@ -280,6 +283,7 @@ export function capability<
|
|
|
280
283
|
failDirectiveId: `fail${pascal}`,
|
|
281
284
|
blockDirectiveId: `block${pascal}`,
|
|
282
285
|
deadLetterDirectiveId: `deadLetter${pascal}`,
|
|
286
|
+
rescheduleDirectiveId: `reschedule${pascal}`,
|
|
283
287
|
params,
|
|
284
288
|
paramsSchema: requestShape,
|
|
285
289
|
// The loop is dynamic (declared field names), so the per-field `set()` typing
|
|
@@ -308,6 +312,9 @@ export function capability<
|
|
|
308
312
|
...(spec.deadlineAfterMs !== undefined
|
|
309
313
|
? { deadlineAfterMs: spec.deadlineAfterMs }
|
|
310
314
|
: {}),
|
|
315
|
+
...(spec.maxRescheduleAttempts !== undefined
|
|
316
|
+
? { maxRescheduleAttempts: spec.maxRescheduleAttempts }
|
|
317
|
+
: {}),
|
|
311
318
|
});
|
|
312
319
|
|
|
313
320
|
const camel = pascal[0]!.toLowerCase() + pascal.slice(1);
|
|
@@ -382,6 +389,18 @@ export function capability<
|
|
|
382
389
|
fail: fenced(quartet.fail),
|
|
383
390
|
block: fenced(quartet.block),
|
|
384
391
|
deadLetter: fenced(quartet.deadLetter),
|
|
392
|
+
// The bounded `reschedule` verb is a DRUM-OPERATOR action: the operator decides to retry (or
|
|
393
|
+
// dead-letter) a stuck obligation; the provider merely FULFILS. So fence it on the SAME
|
|
394
|
+
// authority as the obligation REQUEST (`requestRequires`, e.g. admin) — NOT the provider
|
|
395
|
+
// receipt relation. Otherwise the operator who is allowed to REQUEST work cannot re-open it
|
|
396
|
+
// when the provider stalls, which is exactly the drum's job. Falls back to provider-fenced
|
|
397
|
+
// (like the receipts) when the capability declares no `requestRequires`.
|
|
398
|
+
reschedule: spec.requestRequires === undefined
|
|
399
|
+
? fenced(quartet.reschedule)
|
|
400
|
+
: quartet.reschedule.requires(spec.requestRequires.relation, {
|
|
401
|
+
...(spec.requestRequires.object !== undefined ? { object: spec.requestRequires.object } : {}),
|
|
402
|
+
...(spec.requestRequires.objectFrom !== undefined ? { objectFrom: spec.requestRequires.objectFrom } : {}),
|
|
403
|
+
}),
|
|
385
404
|
// The relation SCHEMA rides with the bundle so the gate can never be inert (a law without
|
|
386
405
|
// a relation schema does not enforce `.requires` at all).
|
|
387
406
|
relations: capabilityProviderRelations,
|
|
@@ -412,6 +431,7 @@ export function capability<
|
|
|
412
431
|
declaration.fail,
|
|
413
432
|
declaration.block,
|
|
414
433
|
declaration.deadLetter,
|
|
434
|
+
declaration.reschedule,
|
|
415
435
|
]) {
|
|
416
436
|
Object.defineProperty(lifecycle, NOMOS_CAPABILITY_DECLARATIONS, {
|
|
417
437
|
value: Object.freeze([declaration]),
|
|
@@ -72,6 +72,8 @@ import { attributionField, t, type Field } from "../fields.js";
|
|
|
72
72
|
import { Lww } from "../drivers.js";
|
|
73
73
|
import { set, type PlannedOp } from "../ops.js";
|
|
74
74
|
import { directive, type RequirableDirective } from "../directive.js";
|
|
75
|
+
import { query, type QueryDecl } from "../query.js";
|
|
76
|
+
import { readFrameworkDeclared } from "../read.js";
|
|
75
77
|
import { executeDirectiveToIntent } from "../wire_encode.js";
|
|
76
78
|
import type { WireIntent } from "../wire.js";
|
|
77
79
|
import type { Ports } from "../ctx.js";
|
|
@@ -124,6 +126,14 @@ export type ImpureCapabilityStatus = (typeof IMPURE_CAPABILITY_STATUSES)[number]
|
|
|
124
126
|
* - nextAction Lww (OPTIONAL) — human/host-action hint, e.g. refreshAuth.
|
|
125
127
|
* - deadLetterReason Lww (OPTIONAL) — why the obligation was moved to the DLQ.
|
|
126
128
|
* - deadLetteredAt Int (OPTIONAL) — epoch-ms the obligation entered the DLQ.
|
|
129
|
+
* - attempts Int (OPTIONAL) — how many times an overdue-but-still-`requested`
|
|
130
|
+
* obligation has been RESCHEDULED in place (the bounded in-law
|
|
131
|
+
* `reschedule` verb). Folded ONLY by a reschedule that re-opens the
|
|
132
|
+
* task, so a freshly-obligated task never writes it — ABSENT decodes as
|
|
133
|
+
* 0 (the same read-decode discipline as the other optionals). It is the
|
|
134
|
+
* bound the DLQ cap (`maxRescheduleAttempts`) is judged against: once an
|
|
135
|
+
* overdue task has already been rescheduled that many times, the next
|
|
136
|
+
* reschedule dead-letters it instead of re-opening.
|
|
127
137
|
*
|
|
128
138
|
* The OPTIONALs are `.optional()` so a freshly-obligated task (which folds none of them)
|
|
129
139
|
* decodes on the generated read type — the same read-decode discipline the rest of the
|
|
@@ -144,6 +154,12 @@ export const IMPURE_CAPABILITY_ENVELOPE = {
|
|
|
144
154
|
nextAction: t.string().merge(Lww).optional(),
|
|
145
155
|
deadLetterReason: t.string().merge(Lww).optional(),
|
|
146
156
|
deadLetteredAt: t.int().merge(Lww).optional(),
|
|
157
|
+
// The RESCHEDULE ATTEMPTS counter (OPTIONAL): how many times an overdue-but-non-terminal
|
|
158
|
+
// (`requested`) obligation has been re-opened in place by the bounded `reschedule` verb.
|
|
159
|
+
// MUST be `.optional()` — a freshly-obligated task folds it NEVER, so it decodes as ABSENT
|
|
160
|
+
// (read as 0); making it required would break decode of every pre-existing task AND the era
|
|
161
|
+
// rule (old chains fold no `attempts`). Judged against `maxRescheduleAttempts` to bound retries.
|
|
162
|
+
attempts: t.int().merge(Lww).optional(),
|
|
147
163
|
// The FRAMEWORK Obligation↔Receipt CLOSURE link (OPTIONAL): the id of the Obligation this
|
|
148
164
|
// RECEIPT closes (== the task's own requestId — the reactor mutates the task instance).
|
|
149
165
|
// GENERIC, written by every impureCapability Receipt (complete OR fail), so the read
|
|
@@ -246,6 +262,18 @@ const deadLetterSchema = z.object({
|
|
|
246
262
|
deadLetteredAt: z.number().int(),
|
|
247
263
|
});
|
|
248
264
|
|
|
265
|
+
/**
|
|
266
|
+
* The RESCHEDULE payload: which obligation to judge. Deliberately carries ONLY the requestId
|
|
267
|
+
* — the reschedule verb reads the task's OWN committed lifecycle state (status / deadlineAt /
|
|
268
|
+
* attempts) through the captured-read lane and reads "now" from the deterministic captured
|
|
269
|
+
* clock, so a caller cannot spoof the overdue-ness verdict or the fresh timing. The provider /
|
|
270
|
+
* drum authority authors it; the plan decides in-law whether it is a no-op, an in-place
|
|
271
|
+
* re-open, or a dead-letter.
|
|
272
|
+
*/
|
|
273
|
+
const rescheduleSchema = z.object({
|
|
274
|
+
requestId: z.string(),
|
|
275
|
+
});
|
|
276
|
+
|
|
249
277
|
/**
|
|
250
278
|
* The three plan builders. Each takes the task aggregate handle + the (already
|
|
251
279
|
* Zod-validated) payload and returns the `PlannedOp[]` writing the envelope. A tenant
|
|
@@ -372,6 +400,29 @@ export function deadLetterOps(
|
|
|
372
400
|
];
|
|
373
401
|
}
|
|
374
402
|
|
|
403
|
+
/**
|
|
404
|
+
* rescheduleOps — the IN-PLACE RE-OPEN half of the bounded `reschedule` verb (NOT a receipt,
|
|
405
|
+
* NOT terminal). Advances the timing window (`scheduledAt`/`deadlineAt`), bumps the `attempts`
|
|
406
|
+
* counter, and KEEPS `status:'requested'`. Because the task stays `requested` (non-terminal),
|
|
407
|
+
* the kernel's terminal-once gate does NOT treat this as a terminal transition — it is the one
|
|
408
|
+
* legal in-place mutation of a live obligation's timing (kernel-verified: mutating a
|
|
409
|
+
* still-`requested` task is not a receipt against a terminal obligation). Symmetrical with the
|
|
410
|
+
* terminal op-builders above: it writes ONLY envelope fields, bound to the task instance (#105).
|
|
411
|
+
*/
|
|
412
|
+
export function rescheduleOps(
|
|
413
|
+
aggregateId: string,
|
|
414
|
+
p: { requestId: string; scheduledAt: number; deadlineAt: number; attempts: number },
|
|
415
|
+
): PlannedOp[] {
|
|
416
|
+
const taskInstance = envelopeInstance(aggregateId, p.requestId);
|
|
417
|
+
return [
|
|
418
|
+
set(taskInstance, "scheduledAt", p.scheduledAt),
|
|
419
|
+
set(taskInstance, "deadlineAt", p.deadlineAt),
|
|
420
|
+
set(taskInstance, "attempts", p.attempts),
|
|
421
|
+
// KEEP status:'requested' — re-opening a non-terminal task, never a terminal transition.
|
|
422
|
+
set(taskInstance, "status", "requested"),
|
|
423
|
+
];
|
|
424
|
+
}
|
|
425
|
+
|
|
375
426
|
/**
|
|
376
427
|
* The generated directive QUARTET for a impure capability. A domain dev calls
|
|
377
428
|
* {@link impureCapability} ONCE and gets:
|
|
@@ -412,6 +463,15 @@ export interface ImpureCapability<
|
|
|
412
463
|
readonly fail: RequirableDirective<z.infer<typeof failSchema>>;
|
|
413
464
|
readonly block: RequirableDirective<z.infer<typeof blockSchema>>;
|
|
414
465
|
readonly deadLetter: RequirableDirective<z.infer<typeof deadLetterSchema>>;
|
|
466
|
+
/**
|
|
467
|
+
* The bounded IN-LAW `reschedule` lifecycle verb (provider / drum-authority action). Its plan
|
|
468
|
+
* reads the task's OWN committed `{status, deadlineAt, attempts}` through the captured-read
|
|
469
|
+
* lane and judges overdue-ness against the deterministic captured clock, then either NO-OPs
|
|
470
|
+
* (terminal, or not yet overdue), RE-OPENS in place (`rescheduleOps`, bumping `attempts` +
|
|
471
|
+
* advancing the window, keeping `status:'requested'`), or DEAD-LETTERS when at/over the
|
|
472
|
+
* `maxRescheduleAttempts` cap (reusing `deadLetterOps`). Kernel-legal without any kernel change.
|
|
473
|
+
*/
|
|
474
|
+
readonly reschedule: RequirableDirective<z.infer<typeof rescheduleSchema>>;
|
|
415
475
|
readonly instanceId: (requestId: string) => string;
|
|
416
476
|
/**
|
|
417
477
|
* The CAPABILITY CREDENTIAL this task's provider must hold to fulfil Orders
|
|
@@ -485,6 +545,8 @@ export function impureCapability<
|
|
|
485
545
|
failDirectiveId: FailId;
|
|
486
546
|
blockDirectiveId: string;
|
|
487
547
|
deadLetterDirectiveId: string;
|
|
548
|
+
/** The wire id for the bounded `reschedule` verb — caller-supplied, like the DLQ id. */
|
|
549
|
+
rescheduleDirectiveId: string;
|
|
488
550
|
/** The task's request-param FIELDS (merged onto the envelope for the aggregate shape). */
|
|
489
551
|
params: F;
|
|
490
552
|
/** The task's request-param Zod SHAPE (merged onto the obligation envelope schema). */
|
|
@@ -526,6 +588,9 @@ export function impureCapability<
|
|
|
526
588
|
scheduleAfterMs?: number;
|
|
527
589
|
/** Time from acceptance until the task is overdue. Defaults to 24 hours. */
|
|
528
590
|
deadlineAfterMs?: number;
|
|
591
|
+
/** How many times an overdue-but-still-`requested` obligation may be RE-OPENED in place by
|
|
592
|
+
* `reschedule` before the next reschedule dead-letters it instead. Defaults to 5. */
|
|
593
|
+
maxRescheduleAttempts?: number;
|
|
529
594
|
}): ImpureCapability<Id, ParamsShape, F, ReceiptShape> {
|
|
530
595
|
const taskAggregate = impureCapabilityAggregate(spec.aggregateId, spec.params);
|
|
531
596
|
const obligationPayload = z.object(spec.paramsSchema);
|
|
@@ -549,6 +614,17 @@ export function impureCapability<
|
|
|
549
614
|
const typedFailSchema = failSchema.extend(typedRequestId);
|
|
550
615
|
const typedBlockSchema = blockSchema.extend(typedRequestId);
|
|
551
616
|
const typedDeadLetterSchema = deadLetterSchema.extend(typedRequestId);
|
|
617
|
+
const typedRescheduleSchema = rescheduleSchema.extend(typedRequestId);
|
|
618
|
+
// THE BY-ID SELF-READ (the #58 captured-read lane): the reschedule plan reads the task's OWN
|
|
619
|
+
// committed lifecycle state by its requestId (== the instance id), keyed on the envelope's
|
|
620
|
+
// indexed `requestId` field. Declared here in the factory (the `tasks`/TasksByStatus query is
|
|
621
|
+
// keyed on `status`, so this is the by-id variant the reschedule verb needs) and DECLARED on
|
|
622
|
+
// the directive via `.reads(...)`; the compiler seats it in the domain automatically off that
|
|
623
|
+
// declaration, so it is routable even though it is not separately exported.
|
|
624
|
+
const taskById: QueryDecl = query(`${spec.aggregateId}ById`)
|
|
625
|
+
.key("requestId")
|
|
626
|
+
.returns(taskAggregate);
|
|
627
|
+
const maxRescheduleAttempts = spec.maxRescheduleAttempts ?? 5;
|
|
552
628
|
|
|
553
629
|
const obligation: RequirableDirective<ObligationPayload<ParamsShape>> = directive(
|
|
554
630
|
spec.obligationDirectiveId,
|
|
@@ -614,6 +690,60 @@ export function impureCapability<
|
|
|
614
690
|
.payload(typedDeadLetterSchema)
|
|
615
691
|
.plan((p) => deadLetterOps(spec.aggregateId, p));
|
|
616
692
|
|
|
693
|
+
// THE BOUNDED `reschedule` VERB. It `.mutates` the task and reads THAT SAME task's committed
|
|
694
|
+
// lifecycle state by id (the captured-read lane, declared via `.reads(taskById)`), judging
|
|
695
|
+
// overdue-ness IN-LAW against the deterministic captured clock — never `Date.now()`. Three
|
|
696
|
+
// outcomes, mirroring the consequent-outbox cap discipline (never touch a terminal task; only
|
|
697
|
+
// re-open genuinely-overdue work; dead-letter once the retry budget is exhausted):
|
|
698
|
+
// 1. status !== "requested" → NO-OP (never mutate a terminal task).
|
|
699
|
+
// 2. now <= deadlineAt → NO-OP (not overdue yet).
|
|
700
|
+
// 3. overdue & attempts < cap → RE-OPEN in place (rescheduleOps): fresh window +
|
|
701
|
+
// attempts+1, KEEP status:'requested' (kernel-legal —
|
|
702
|
+
// a still-`requested` task is non-terminal).
|
|
703
|
+
// 4. overdue & attempts >= cap → DEAD-LETTER (deadLetterOps, terminal) with the
|
|
704
|
+
// overdue reason.
|
|
705
|
+
const reschedule = directive(spec.rescheduleDirectiveId)
|
|
706
|
+
.mutates(taskAggregate)
|
|
707
|
+
.payload(typedRescheduleSchema)
|
|
708
|
+
.plan((p, ctx) => {
|
|
709
|
+
const now = ctx.clock().physical;
|
|
710
|
+
// Read the task's OWN committed state by id off the #58 captured-read lane. A framework
|
|
711
|
+
// plan bundled via the `@githolon/dsl` barrel reads through `readFrameworkDeclared` (the
|
|
712
|
+
// declared-by-construction twin of `read()`), so it never pins the tenant captured-read
|
|
713
|
+
// compile marker into the barrel lump; the directive DECLARES `taskById` below.
|
|
714
|
+
const rows = readFrameworkDeclared<Array<Record<string, unknown>>>(taskById.id, {
|
|
715
|
+
requestId: p.requestId,
|
|
716
|
+
});
|
|
717
|
+
const raw = Array.isArray(rows) && rows.length > 0 ? rows[0] : undefined;
|
|
718
|
+
// Projected rows are `{type,id,data:{…fields}}`; folded shapes may be flat — accept both.
|
|
719
|
+
const data = (raw && typeof raw === "object" && typeof (raw as { data?: unknown }).data === "object"
|
|
720
|
+
? (raw as { data: Record<string, unknown> }).data
|
|
721
|
+
: raw) as Record<string, unknown> | undefined;
|
|
722
|
+
const status = data?.status as string | undefined;
|
|
723
|
+
// (1) NEVER touch a terminal task — only `requested` is non-terminal.
|
|
724
|
+
if (status !== "requested") return [];
|
|
725
|
+
const deadlineAt = Number(data?.deadlineAt ?? 0);
|
|
726
|
+
// (2) Not overdue yet — the deterministic clock has not passed the committed deadline.
|
|
727
|
+
if (now <= deadlineAt) return [];
|
|
728
|
+
const attempts = Number(data?.attempts ?? 0);
|
|
729
|
+
// (4) Overdue AND the retry budget is exhausted → terminal dead-letter (reuse deadLetterOps).
|
|
730
|
+
if (attempts >= maxRescheduleAttempts) {
|
|
731
|
+
return deadLetterOps(spec.aggregateId, {
|
|
732
|
+
requestId: p.requestId,
|
|
733
|
+
deadLetterReason: `overdue: exhausted ${maxRescheduleAttempts} reschedule attempts`,
|
|
734
|
+
deadLetteredAt: now,
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
// (3) Overdue but under the cap → RE-OPEN in place with a fresh window + bumped attempts.
|
|
738
|
+
return rescheduleOps(spec.aggregateId, {
|
|
739
|
+
requestId: p.requestId,
|
|
740
|
+
scheduledAt: now + (spec.scheduleAfterMs ?? 0),
|
|
741
|
+
deadlineAt: now + (spec.deadlineAfterMs ?? 24 * 60 * 60 * 1000),
|
|
742
|
+
attempts: attempts + 1,
|
|
743
|
+
});
|
|
744
|
+
})
|
|
745
|
+
.reads(taskById);
|
|
746
|
+
|
|
617
747
|
return {
|
|
618
748
|
aggregate: taskAggregate,
|
|
619
749
|
obligation,
|
|
@@ -622,6 +752,7 @@ export function impureCapability<
|
|
|
622
752
|
fail,
|
|
623
753
|
block,
|
|
624
754
|
deadLetter,
|
|
755
|
+
reschedule,
|
|
625
756
|
instanceId: impureCapabilityInstanceId,
|
|
626
757
|
// Omit-when-absent (hash discipline): a capability-free declaration carries
|
|
627
758
|
// NO key at all, so its bundled source — and the package hash — is unchanged.
|
|
@@ -39,6 +39,7 @@ export const OWNED_RECOVERY_ACCEPT_DIRECTIVE = "nomos_internal_accept_owned_reco
|
|
|
39
39
|
const OWNED_RECOVERY_FAIL_DIRECTIVE = "nomos_internal_fail_owned_recovery_7c1f5e";
|
|
40
40
|
const OWNED_RECOVERY_BLOCK_DIRECTIVE = "nomos_internal_block_owned_recovery_7c1f5e";
|
|
41
41
|
const OWNED_RECOVERY_DEAD_LETTER_DIRECTIVE = "nomos_internal_dead_letter_owned_recovery_7c1f5e";
|
|
42
|
+
const OWNED_RECOVERY_RESCHEDULE_DIRECTIVE = "nomos_internal_reschedule_owned_recovery_7c1f5e";
|
|
42
43
|
const keySubject = z.string().regex(/^key:[0-9a-f]{64}$/);
|
|
43
44
|
const principal = z.string().regex(/^user:[A-Za-z0-9][A-Za-z0-9_.-]*$/);
|
|
44
45
|
const b64 = z.string().regex(/^[A-Za-z0-9+/]+={0,2}$/);
|
|
@@ -99,6 +100,7 @@ const ownedRecoveryLifecycle = impureCapability({
|
|
|
99
100
|
failDirectiveId: OWNED_RECOVERY_FAIL_DIRECTIVE,
|
|
100
101
|
blockDirectiveId: OWNED_RECOVERY_BLOCK_DIRECTIVE,
|
|
101
102
|
deadLetterDirectiveId: OWNED_RECOVERY_DEAD_LETTER_DIRECTIVE,
|
|
103
|
+
rescheduleDirectiveId: OWNED_RECOVERY_RESCHEDULE_DIRECTIVE,
|
|
102
104
|
params: recoveryOrderFields,
|
|
103
105
|
paramsSchema: {
|
|
104
106
|
ownerPrincipal: principal,
|
|
@@ -128,6 +130,7 @@ const hiddenRecoveryLifecycle = {
|
|
|
128
130
|
fail: hidden(ownedRecoveryLifecycle.fail),
|
|
129
131
|
block: hidden(ownedRecoveryLifecycle.block),
|
|
130
132
|
deadLetter: hidden(ownedRecoveryLifecycle.deadLetter),
|
|
133
|
+
reschedule: hidden(ownedRecoveryLifecycle.reschedule),
|
|
131
134
|
};
|
|
132
135
|
const ownedRecoveryCapability = {
|
|
133
136
|
...ownedRecoveryLifecycle,
|
|
@@ -140,6 +143,7 @@ const ownedRecoveryCapability = {
|
|
|
140
143
|
fail: hiddenRecoveryLifecycle.fail,
|
|
141
144
|
block: hiddenRecoveryLifecycle.block,
|
|
142
145
|
deadLetter: hiddenRecoveryLifecycle.deadLetter,
|
|
146
|
+
reschedule: hiddenRecoveryLifecycle.reschedule,
|
|
143
147
|
__isCapabilityDecl: true as const,
|
|
144
148
|
};
|
|
145
149
|
|
|
@@ -98,6 +98,7 @@ const workspaceSyncTask = impureCapability({
|
|
|
98
98
|
failDirectiveId: "failWorkspaceSync",
|
|
99
99
|
blockDirectiveId: "blockWorkspaceSync",
|
|
100
100
|
deadLetterDirectiveId: "deadLetterWorkspaceSync",
|
|
101
|
+
rescheduleDirectiveId: "rescheduleWorkspaceSync",
|
|
101
102
|
params: syncParams,
|
|
102
103
|
paramsSchema: {
|
|
103
104
|
workspaceId: z.string(),
|
package/src/index.ts
CHANGED
|
@@ -255,19 +255,17 @@ export { repair, repairsOf, isRepairDecl, type RepairDecl, type RepairMatcher, t
|
|
|
255
255
|
// `nomos-compile` REFUSES law that references read() (a named compile error, never a runtime halt).
|
|
256
256
|
// The captured-reads wave (task #58) wires the capability in and lifts the refusal.
|
|
257
257
|
export { read } from "./read.js";
|
|
258
|
-
// RELEASE COHERENCE —
|
|
258
|
+
// RELEASE COHERENCE — standard OSGi Contract Namespace requirements derived from law shape.
|
|
259
259
|
export {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
assessKernelCoherence,
|
|
260
|
+
requiredKernelContracts,
|
|
261
|
+
assessKernelContracts,
|
|
263
262
|
KernelCoherenceError,
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
type
|
|
267
|
-
type
|
|
268
|
-
type
|
|
269
|
-
type
|
|
270
|
-
type FeatureProbeModule,
|
|
263
|
+
NOMOS_KERNEL_CONTRACT,
|
|
264
|
+
NOMOS_KERNEL_CONTRACT_VERSION,
|
|
265
|
+
type OsgiContractRequirement,
|
|
266
|
+
type OsgiContractCapability,
|
|
267
|
+
type KernelContractVerdict,
|
|
268
|
+
type KernelContractProbeModule,
|
|
271
269
|
} from "./kernel_capabilities.js";
|
|
272
270
|
// ATTESTED CROSS-WORKSPACE READS (slice B — attested_reads_design.md): the declared trust
|
|
273
271
|
// surface (`trustSource`), the foreign query reference, and the reader-side SourceFrontier.
|