@githolon/dsl 0.113.3 → 0.114.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.
Files changed (58) hide show
  1. package/assets/operation-contract.wasm +0 -0
  2. package/compile_package.mjs +23 -3
  3. package/compile_proof.d.mts +7 -0
  4. package/compile_proof.mjs +38 -3
  5. package/dart/CHANGELOG.md +24 -0
  6. package/dart/lib/src/application_framework_calls.dart +9 -5
  7. package/dart/lib/src/generated_call.dart +14 -3
  8. package/dart/lib/src/nms_generated_runtime.dart +7 -0
  9. package/dart/lib/src/node_runner_transport.dart +77 -8
  10. package/dart/lib/src/nomos_trace.dart +107 -0
  11. package/dart/lib/src/reception/dart_reception.dart +148 -11
  12. package/dart/lib/src/rows_watch.dart +390 -0
  13. package/dart/lib/src/runner/node-entry.mjs +2807 -691
  14. package/dart/lib/src/sync_status_projection.dart +94 -1
  15. package/dart/lib/src/sync_status_reducer.dart +5 -2
  16. package/dart/lib/src/test_harness.dart +107 -41
  17. package/dart/lib/src/testing/test_peer.dart +107 -99
  18. package/dart/lib/src/testing/test_peer_host.dart +305 -0
  19. package/dart/lib/src/typed_watch_registry.dart +26 -3
  20. package/dart/pubspec.lock +1 -1
  21. package/dart/pubspec.yaml +2 -2
  22. package/dart/test/background_model_adoption_test.dart +1 -0
  23. package/dart/test/custodian_warming_test.dart +105 -0
  24. package/dart/test/fixtures/offer_watch_order_peer.dart +74 -0
  25. package/dart/test/kernel_author_after_read_test.dart +6 -3
  26. package/dart/test/kernel_fence_test.dart +4 -1
  27. package/dart/test/model_adoption_test.dart +17 -0
  28. package/dart/test/nomos_trace_test.dart +91 -0
  29. package/dart/test/own_offer_watch_order_test.dart +90 -0
  30. package/dart/test/receiving_group_open_test.dart +4 -8
  31. package/dart/test/replica_readiness_test.dart +14 -4
  32. package/dart/test/rows_watch_test.dart +331 -0
  33. package/dart/test/test_peer_host_selection_test.dart +49 -0
  34. package/dart_types/CHANGELOG.md +15 -0
  35. package/dart_types/lib/src/approved_model.dart +22 -5
  36. package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pb.dart +535 -1
  37. package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pbenum.dart +65 -0
  38. package/dart_types/lib/src/proto/nomos/runtime/v2/application_desk.pb.dart +14 -0
  39. package/dart_types/lib/src/proto/nomos/runtime/v2/correspondence_desk.pb.dart +359 -2
  40. package/dart_types/lib/src/proto/nomos/runtime/v2/correspondence_desk.pbenum.dart +65 -0
  41. package/dart_types/lib/src/proto/nomos/runtime/v2/model_adoption_desk.pb.dart +20 -2
  42. package/dart_types/lib/src/proto/nomos/runtime/v2/observability_desk.pb.dart +278 -9
  43. package/dart_types/lib/src/proto/nomos/runtime/v2/receiving_group_desk.pb.dart +7 -85
  44. package/dart_types/lib/src/proto/nomos/runtime/v2/residency_desk.pb.dart +35 -0
  45. package/dart_types/pubspec.yaml +1 -1
  46. package/package.json +4 -2
  47. package/src/build_provenance.ts +7 -2
  48. package/src/codegen_dart_client.ts +21 -9
  49. package/src/codegen_dart_facade.ts +1 -0
  50. package/src/codegen_dart_framework_calls.ts +12 -9
  51. package/src/codegen_dart_scenario.ts +7 -0
  52. package/src/codegen_ts.ts +26 -24
  53. package/src/compile_package_main.ts +65 -61
  54. package/src/engine_entry.ts +4 -48
  55. package/src/law_unit_set_identity.ts +44 -0
  56. package/src/query/compile.ts +0 -0
  57. package/src/release_inspection.ts +24 -0
  58. package/src/release_publication.ts +298 -0
Binary file
@@ -19,9 +19,11 @@ import { createRequire } from "node:module";
19
19
  import path from "node:path";
20
20
  import { fileURLToPath, pathToFileURL } from "node:url";
21
21
  import {
22
+ authoredImport,
22
23
  compileInputs,
23
24
  compileProofPath,
24
25
  readCompileProof,
26
+ recordedImportedSources,
25
27
  } from "./compile_proof.mjs";
26
28
 
27
29
  const DSL_DIR = path.dirname(fileURLToPath(import.meta.url));
@@ -49,12 +51,21 @@ if (cacheable) {
49
51
  dslDir: DSL_DIR,
50
52
  configPath,
51
53
  argumentIdentity,
54
+ importedSources: recordedImportedSources(proofPath),
52
55
  });
53
56
  const proof = readCompileProof({ proofPath, inputs });
54
57
  if (proof !== undefined) {
55
58
  console.log(`nomos-compile: ${proof.name} unchanged`);
56
- console.log(` law ${path.relative(process.cwd(), proof.law)}`);
57
- console.log(` domainHash ${proof.domainHash}`);
59
+ // The law identity is the unit set (CAP-773): the compile wrote it beside the package it proves.
60
+ const unitSet = (() => {
61
+ try {
62
+ return readFileSync(proof.law.replace(/\.law\.usdz$/u, ".units.sha256"), "utf8").trim();
63
+ } catch {
64
+ return "(unreadable — recompile)";
65
+ }
66
+ })();
67
+ console.log(` law unit set ${unitSet}`);
68
+ console.log(` package ${proof.domainHash} — package — not what targets serve (${path.relative(process.cwd(), proof.law)})`);
58
69
  console.log(` verify ✓ typed USDA content proof covers compiler, authored inputs and ${proof.outputCount} generated artifacts`);
59
70
  process.exit(0);
60
71
  }
@@ -85,6 +96,15 @@ const tsxApiEntry = path.join(
85
96
 
86
97
  // Global hooks (not a namespaced `tsImport`): the compiler's `import.meta.url` and every
87
98
  // `createRequire` must stay plain file URLs, exactly as under the tsx CLI.
99
+ // The compile's authored import closure (including modules outside the project directory) is
100
+ // collected here and recorded in the compile proof, so editing any of them invalidates it.
101
+ const importedSources = new Set();
102
+ globalThis[Symbol.for("nomos.compile.importedSources")] = importedSources;
88
103
  const { register } = await import(pathToFileURL(tsxApiEntry).href);
89
- register();
104
+ register({
105
+ onImport(url) {
106
+ const file = authoredImport(url, DSL_DIR);
107
+ if (file !== undefined) importedSources.add(file);
108
+ },
109
+ });
90
110
  await import(pathToFileURL(MAIN).href);
@@ -1,6 +1,8 @@
1
1
  export interface CompileProofInputs {
2
2
  readonly authoring: readonly { readonly path: string; readonly sha256: string }[];
3
3
  readonly compiler: readonly { readonly path: string; readonly sha256: string }[];
4
+ /** The authored modules the compile imported, including any outside the project directory. */
5
+ readonly importedSources: readonly string[];
4
6
  readonly authoringIdentity: string;
5
7
  readonly compilerIdentity: string;
6
8
  readonly argumentIdentity: string;
@@ -11,7 +13,12 @@ export function compileInputs(options: {
11
13
  dslDir: string;
12
14
  configPath: string;
13
15
  argumentIdentity: string;
16
+ importedSources?: readonly string[];
14
17
  }): CompileProofInputs;
18
+ /** The authored file a compile imported from `url`, or undefined for compiler and dependency code. */
19
+ export function authoredImport(url: string, dslDir: string): string | undefined;
20
+ /** The authored import closure a previous compile recorded in its proof. */
21
+ export function recordedImportedSources(proofPath: string): string[];
15
22
  export function compileProofPath(cwd: string, configPath: string): string;
16
23
  export function generatedOutputFiles(
17
24
  outDir: string,
package/compile_proof.mjs CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  } from "node:fs";
14
14
  import { createRequire } from "node:module";
15
15
  import path from "node:path";
16
- import { pathToFileURL } from "node:url";
16
+ import { fileURLToPath, pathToFileURL } from "node:url";
17
17
 
18
18
  const INPUT_EXTENSIONS = new Set([
19
19
  ".ts", ".mts", ".cts", ".js", ".mjs", ".cjs", ".json", ".yaml", ".yml",
@@ -94,8 +94,38 @@ function dependencyPackageFiles(dslDir, cwd) {
94
94
  return files;
95
95
  }
96
96
 
97
- export function compileInputs({ cwd, dslDir, configPath, argumentIdentity }) {
97
+ /**
98
+ * The authored module a running compile imported, or undefined for compiler and dependency code.
99
+ * A project may name modules outside its own directory (`../../domains/x.ts`); walking the project
100
+ * root never sees those, so the proof records the closure the compile actually loaded.
101
+ */
102
+ export function authoredImport(url, dslDir) {
103
+ if (!url.startsWith("file:")) return undefined;
104
+ const file = path.resolve(fileURLToPath(url));
105
+ if (!INPUT_EXTENSIONS.has(path.extname(file))) return undefined;
106
+ if (file.split(path.sep).includes("node_modules")) return undefined;
107
+ const fromCompiler = path.relative(path.resolve(dslDir), file);
108
+ if (!fromCompiler.startsWith("..") && !path.isAbsolute(fromCompiler)) return undefined;
109
+ return file;
110
+ }
111
+
112
+ /** The authored import closure a previous compile recorded, so an unchanged check re-hashes it. */
113
+ export function recordedImportedSources(proofPath) {
114
+ let text;
115
+ try {
116
+ text = readFileSync(proofPath, "utf8");
117
+ } catch {
118
+ return [];
119
+ }
120
+ const list = /custom uniform asset\[\] nomos:importedSources = \[([^\]]*)\]/u.exec(text)?.[1];
121
+ if (list === undefined) return [];
122
+ return [...list.matchAll(/@([^@]+)@/gu)].map((match) => path.resolve(path.dirname(proofPath), match[1]));
123
+ }
124
+
125
+ export function compileInputs({ cwd, dslDir, configPath, argumentIdentity, importedSources = [] }) {
126
+ const imported = [...new Set(importedSources.map((file) => path.resolve(file)))].sort(compareText);
98
127
  const authoring = inventory([
128
+ ...imported,
99
129
  configPath,
100
130
  // An operator may compile an absolute project path from a different repo.
101
131
  // Its sibling sources are inputs too; cwd alone can falsely certify stale law.
@@ -115,7 +145,9 @@ export function compileInputs({ cwd, dslDir, configPath, argumentIdentity }) {
115
145
  return {
116
146
  authoring,
117
147
  compiler,
118
- authoringIdentity: inventoryIdentity("authoring", authoring),
148
+ importedSources: imported,
149
+ // v2 of the authoring identity covers the imported closure; a v1 proof never certifies it.
150
+ authoringIdentity: inventoryIdentity("authoring-closure", authoring),
119
151
  compilerIdentity: inventoryIdentity("implementation", compiler),
120
152
  argumentIdentity: sha256(`nomos-compile-arguments-v1\0${argumentIdentity}`),
121
153
  };
@@ -163,6 +195,9 @@ function emitCompileProof({ proofPath, inputs, outputs, name, domainHash, law })
163
195
  ` custom uniform string nomos:authoringIdentity = "${inputs.authoringIdentity}"`,
164
196
  ` custom uniform string nomos:compilerIdentity = "${inputs.compilerIdentity}"`,
165
197
  ` custom uniform string nomos:argumentIdentity = "${inputs.argumentIdentity}"`,
198
+ ` custom uniform asset[] nomos:importedSources = [${(inputs.importedSources ?? [])
199
+ .map((file) => `@${path.relative(proofDir, file).replaceAll(path.sep, "/")}@`)
200
+ .join(", ")}]`,
166
201
  " custom rel nomos:inputs = [",
167
202
  ...relationships("Inputs", inputPrims).map((line, index) => `${line}${index + 1 === inputPrims.length ? "" : ","}`),
168
203
  " ]",
package/dart/CHANGELOG.md CHANGED
@@ -1,3 +1,27 @@
1
+ ## 0.68.0
2
+
3
+ Breaking: regenerate generated Dart clients with this compiler.
4
+
5
+ - Generated clients name the law by its unit set (CAP-773). Generated framework calls expose `lawUnitSet` and
6
+ `lawPackageSha256` in place of `applicationBirthContract`, and no generated offer carries a law expectation;
7
+ the kernel selects current law from the workspace's own state. `OpenReceivingGroupRequest.contract` is retired:
8
+ a Home is born under the law its platform serves. Pass `unitSet` to `NomosApprovedModel` (nomos_types 0.13.0).
9
+ - A local offer's call returns after the watches it changed have delivered the post-offer state, and it re-reads
10
+ only the watches it can change (CAP-755, CAP-760). Rows watches merge the rows that changed instead of
11
+ re-reading the whole result (`NmsRowsMerge`, `NomosRows`), and `NomosRowProjection` redoes only those rows
12
+ (CAP-731, CAP-751).
13
+ - The sync status reports a custodian warming up as `CustodianWarmingPosture`, not as an error (CAP-769).
14
+ - `--dart-define=NOMOS_TRACE=true` writes an opt-in `[nomos-trace]` timeline across the save path (CAP-741).
15
+ - Testing: `NmsTestPeer` stands on an explicit `NmsTestPeerHost`; `NOMOS_TEST_PEER_HOST=native` runs AppSim on the
16
+ real Apple native host (CAP-717). `NOMOS_PEER_CLASSES` selects the Node peer's classes, e.g.
17
+ `personal-device` (CAP-722).
18
+ - Bundles the runtime with instant local writes, the live link changes and the fixes listed in
19
+ nomos_flutter 0.67.0.
20
+
21
+ ## 0.67.2
22
+
23
+ - Bundles the 0.113.4 runtime: owed watch re-reads never starve the Peer Office's maintenance.
24
+
1
25
  ## 0.67.1
2
26
 
3
27
  - Git custody upkeep backing off after a failure is reported as `upkeepRetrying`, not `retrying`: the workspace is
@@ -4,7 +4,7 @@ import 'dart:convert' show utf8;
4
4
  import 'dart:typed_data' show Uint8List;
5
5
  import 'package:meta/meta.dart' show internal;
6
6
  import 'package:nomos_types/nomos_types.dart' show Sha256Digest;
7
- import 'package:nomos_types/nomos_kernel_protocol.dart' show ApplicationBirthContract, BirthCertificate;
7
+ import 'package:nomos_types/nomos_kernel_protocol.dart' show BirthCertificate;
8
8
 
9
9
  import 'generated_call.dart';
10
10
  import 'result_usda.dart' show NmsUsdaLayer, NmsUsdaPrim;
@@ -235,10 +235,14 @@ abstract interface class NmsApplicationFrameworkCalls {
235
235
  String get parentModelAuthoritySeatAggregate;
236
236
  String get parentModelAuthoritySeatId;
237
237
 
238
- /// The application's birth contract for the receiving-group desk: the compiled law package identity
239
- /// (the closure serving the injected application-home domain). The parent platform's controller is
240
- /// per-platform installed state the desk observes at birth, never a value the descriptor carries.
241
- ApplicationBirthContract get applicationBirthContract;
238
+ /// THE LAW this application was compiled as (CAP-773): the unit set identity of its `build/<name>.units`.
239
+ /// An application approves law by it; each workspace serves a closure its own kernel composes from those
240
+ /// units, so no compiled value names what a workspace serves.
241
+ String get lawUnitSet;
242
+
243
+ /// The compiled package closure (`build/<name>.law.usdz`): the bytes a device stages through the
244
+ /// model-adoption desk on a package-lane workspace. Package — not what a unit-lane workspace serves.
245
+ String get lawPackageSha256;
242
246
 
243
247
  NmsGeneratedCall enrollFirstDevice({
244
248
  required String domain,
@@ -2,6 +2,8 @@ import 'dart:typed_data';
2
2
 
3
3
  import 'package:meta/meta.dart' show internal;
4
4
 
5
+ import 'nomos_trace.dart' show NomosTrace;
6
+
5
7
  /// One compiler-authored, nominal domain call.
6
8
  ///
7
9
  /// Generated applications construct this directly from their typed input. The
@@ -63,10 +65,17 @@ abstract final class NmsInternalGeneratedCalls {
63
65
  throw ArgumentError(
64
66
  'A generated Nomos call needs one exact typed Protobuf payload.');
65
67
  }
66
- return _GeneratedCall(domain, directive, protobufType, protobufPayload);
68
+ return _GeneratedCall(domain, directive, protobufType, protobufPayload,
69
+ NomosTrace.enabled ? NomosTrace.now() : 0);
67
70
  }
68
71
  }
69
72
 
73
+ /// When generated code finished encoding [call] (Timeline microseconds), for the opt-in
74
+ /// timeline trace; 0 when tracing was off or the call was not compiler-authored.
75
+ @internal
76
+ int nmsGeneratedCallCreatedAt(NmsGeneratedCall call) =>
77
+ call is _GeneratedCall ? call.createdAtMicros : 0;
78
+
70
79
  /// Constructor namespace reserved for generated query code.
71
80
  @internal
72
81
  abstract final class NmsInternalGeneratedQueries {
@@ -112,8 +121,10 @@ abstract final class NmsInternalGeneratedResults {
112
121
  }
113
122
 
114
123
  final class _GeneratedCall implements NmsGeneratedCall {
115
- const _GeneratedCall(
116
- this.domain, this.directive, this.protobufType, this.protobufPayload);
124
+ const _GeneratedCall(this.domain, this.directive, this.protobufType,
125
+ this.protobufPayload, this.createdAtMicros);
126
+
127
+ final int createdAtMicros;
117
128
 
118
129
  @override
119
130
  final String domain;
@@ -39,6 +39,12 @@ export 'package:nomos_types/src/foreign_ref.dart'
39
39
  ForeignDenied,
40
40
  ForeignUnreachable;
41
41
  export 'reception/dart_reception.dart';
42
+ export 'rows_watch.dart'
43
+ show
44
+ NomosRowProjection,
45
+ NomosRows,
46
+ NomosRowsProjectionStream,
47
+ NomosWatchPositionMismatch;
42
48
  export 'session_bound.dart';
43
49
  export 'application_model_adoption.dart'
44
50
  show
@@ -49,6 +55,7 @@ export 'application_model_adoption.dart'
49
55
  NomosModelAdoptionPhase,
50
56
  NomosModelAdoptionProgress;
51
57
  export 'provider.dart';
58
+ export 'nomos_trace.dart' show NomosTrace;
52
59
  export 'record_origin.dart';
53
60
  export 'generated_call.dart'
54
61
  show
@@ -20,6 +20,8 @@ import 'package:package_config/package_config.dart' show loadPackageConfigUri;
20
20
 
21
21
  import 'package:nomos_types/nomos_kernel_protocol.dart' as nomos_protocol;
22
22
 
23
+ import 'nomos_trace.dart' show NomosTrace;
24
+
23
25
  import 'runtime_v2_transport.dart'
24
26
  show
25
27
  NmsBufferedRuntimeEvents,
@@ -40,8 +42,15 @@ class NodeRunnerTransport
40
42
  final void Function(String line)? _onDiagnostic;
41
43
  final Map<int, Completer<nomos_protocol.RuntimeResponse>> _runtimeReplies =
42
44
  {};
45
+
46
+ /// Opt-in timeline only: when each request was written, and when its answer was decoded.
47
+ final Map<int, int> _traceSentAt = {};
48
+ final Map<int, int> _traceArrivedAt = {};
43
49
  bool _disposed = false;
44
50
 
51
+ /// A watch change was delivered in this event-loop turn and its listeners have not run yet.
52
+ bool _eventsAhead = false;
53
+
45
54
  NodeRunnerTransport._(this._proc, this._onDiagnostic) {
46
55
  _proc.stdout.transform(utf8.decoder).transform(const LineSplitter()).listen(
47
56
  (line) {
@@ -49,12 +58,34 @@ class NodeRunnerTransport
49
58
  if (s.isEmpty) return;
50
59
  if (s.startsWith('runtime-v2:')) {
51
60
  try {
61
+ final traceArrived = NomosTrace.enabled ? NomosTrace.now() : 0;
52
62
  final response = nomos_protocol.RuntimeResponse.fromBuffer(
53
63
  base64Decode(s.substring('runtime-v2:'.length)),
54
64
  );
55
- _runtimeReplies
56
- .remove(response.requestId.toInt())
57
- ?.complete(response);
65
+ if (NomosTrace.enabled) {
66
+ final id = response.requestId.toInt();
67
+ final now = NomosTrace.now();
68
+ _traceArrivedAt[id] = now;
69
+ final sent = _traceSentAt[id];
70
+ NomosTrace.emit(
71
+ 'main',
72
+ 'rt.recv',
73
+ 'rr=$id outcome=${response.whichOutcome().name} '
74
+ 'decUs=${now - traceArrived} '
75
+ 'us=${sent == null ? -1 : now - sent} bytes=${s.length}');
76
+ }
77
+ final reply = _runtimeReplies.remove(response.requestId.toInt());
78
+ if (reply != null) {
79
+ if (_eventsAhead) {
80
+ // Watch changes the peer queued while serving this request were written ahead of
81
+ // its reply and may share this stdout chunk. Their listeners run as microtasks;
82
+ // complete the reply in the next event-loop turn so they all run first (CAP-755: a
83
+ // local offer's watches have delivered before its call returns).
84
+ Timer.run(() => reply.complete(response));
85
+ } else {
86
+ reply.complete(response);
87
+ }
88
+ }
58
89
  } catch (error, stackTrace) {
59
90
  _diag('[nomos-node-runner runtime-v2] $error');
60
91
  for (final reply in _runtimeReplies.values) {
@@ -72,6 +103,11 @@ class NodeRunnerTransport
72
103
  if (!consumeCredentialRefreshEvent(
73
104
  event, this, _runtimeEvents.addError)) {
74
105
  _runtimeEvents.add(event);
106
+ if (event.hasWatchChanged() && !_eventsAhead) {
107
+ _eventsAhead = true;
108
+ // Cleared once this chunk's microtasks (the events' listeners) have all run.
109
+ Timer.run(() => _eventsAhead = false);
110
+ }
75
111
  }
76
112
  } catch (error, stackTrace) {
77
113
  _runtimeEvents.addError(error, stackTrace);
@@ -89,7 +125,15 @@ class NodeRunnerTransport
89
125
  _proc.stderr
90
126
  .transform(utf8.decoder)
91
127
  .transform(const LineSplitter())
92
- .listen((line) => _diag('[nomos-node-runner] $line'));
128
+ .listen((line) {
129
+ // The peer's own timeline lines join this process's log as they are (nomos_trace.dart).
130
+ if (line.startsWith('[nomos-trace] ')) {
131
+ // ignore: avoid_print
132
+ print(line);
133
+ return;
134
+ }
135
+ _diag('[nomos-node-runner] $line');
136
+ });
93
137
  }
94
138
 
95
139
  void _diag(String line) {
@@ -199,15 +243,40 @@ class NodeRunnerTransport
199
243
  }
200
244
  final reply = Completer<nomos_protocol.RuntimeResponse>();
201
245
  _runtimeReplies[id] = reply;
246
+ final traceStarted = NomosTrace.enabled ? NomosTrace.now() : 0;
247
+ if (NomosTrace.enabled && !request.hasTrace()) {
248
+ // Sampled: the peer writes its own timeline lines for this frame.
249
+ request.trace = NomosTrace.sampledContext();
250
+ }
202
251
  try {
203
- _proc.stdin.writeln(
204
- 'runtime-v2:${base64Encode(request.writeToBuffer())}',
205
- );
252
+ final line = 'runtime-v2:${base64Encode(request.writeToBuffer())}';
253
+ _proc.stdin.writeln(line);
254
+ if (NomosTrace.enabled) {
255
+ _traceSentAt[id] = NomosTrace.now();
256
+ NomosTrace.emit(
257
+ 'main',
258
+ 'rt.send',
259
+ 'rr=$id op=${NomosTrace.describeRequest(request)} '
260
+ 'lr=${request.hasKernelCall() ? request.kernelCall.request.requestId : '-'} '
261
+ 'encUs=${NomosTrace.now() - traceStarted} bytes=${line.length}');
262
+ }
206
263
  } catch (error, stackTrace) {
207
264
  _runtimeReplies.remove(id);
208
265
  reply.completeError(error, stackTrace);
209
266
  }
210
- return reply.future;
267
+ if (!NomosTrace.enabled) return reply.future;
268
+ return reply.future.then((response) {
269
+ // From the decoded answer to the awaiting code running again.
270
+ final arrived = _traceArrivedAt.remove(id);
271
+ final sent = _traceSentAt.remove(id);
272
+ final now = NomosTrace.now();
273
+ NomosTrace.emit(
274
+ 'main',
275
+ 'rt.resume',
276
+ 'rr=$id resumeUs=${arrived == null ? -1 : now - arrived} '
277
+ 'us=${sent == null ? -1 : now - sent}');
278
+ return response;
279
+ });
211
280
  }
212
281
 
213
282
  /// Tear down: stop the runner process and close the message stream.
@@ -0,0 +1,107 @@
1
+ /// THE OPT-IN TIMELINE TRACE: `[nomos-trace]` lines across the whole call path.
2
+ ///
3
+ /// One generated call (a save, a read) crosses the reception, the transport, an isolate, the
4
+ /// native host, the Peer Office and the kernel. Every layer writes `[nomos-trace]` lines so one
5
+ /// call can be followed end to end and each millisecond given to a layer:
6
+ ///
7
+ /// * `c=` — this Dart call (one generated offer or query), joined to
8
+ /// * `lr=` — the kernel request id the reception gave it, joined to
9
+ /// * `rr=` — the Runtime v2 request id every lower layer (worker, host, office) logs.
10
+ ///
11
+ /// `t=` is microseconds on Dart's Timeline clock; on Apple the native host and the Peer Office
12
+ /// stamp the same clock (CLOCK_MONOTONIC_RAW), so cross-layer gaps are plain subtraction.
13
+ /// `scripts/nomos_trace_waterfall.ts` turns a log of these lines into a per-call waterfall.
14
+ ///
15
+ /// Off by default, including in debug. Build with `--dart-define=NOMOS_TRACE=true` (a release
16
+ /// phone build, `flutter test`, AppSim) or set [NomosTrace.enabled] before the first call. Every
17
+ /// Runtime v2 request is then sent with a `sampled` trace context, which is what turns the native
18
+ /// host's and the Peer Office's lines on: one switch for the whole path. Disabled, each call site
19
+ /// is one static bool read. Lines name operation kinds, ids, sizes and durations only: never
20
+ /// payloads, credentials or workspace contents.
21
+ library;
22
+
23
+ import 'dart:async';
24
+ import 'dart:developer' as developer;
25
+
26
+ import 'package:meta/meta.dart' show internal;
27
+ import 'package:nomos_types/nomos_kernel_protocol.dart' as nomos_protocol;
28
+
29
+ /// See the library comment.
30
+ abstract final class NomosTrace {
31
+ /// Whether `[nomos-trace]` lines are written. Per isolate: hosts that run a worker isolate pass
32
+ /// it on explicitly.
33
+ static bool enabled = const bool.fromEnvironment('NOMOS_TRACE');
34
+
35
+ static int _calls = 0;
36
+
37
+ /// Microseconds on the Timeline clock (the clock every `t=` uses).
38
+ static int now() => developer.Timeline.now;
39
+
40
+ /// A fresh Dart call id (`c=`).
41
+ static int nextCall() => ++_calls;
42
+
43
+ /// Write one line: `[nomos-trace] t=<now> L=<layer> e=<event> <fields>`. Build [fields] only
44
+ /// behind an `if (NomosTrace.enabled)` so a disabled build pays nothing for them.
45
+ static void emit(String layer, String event, String fields) {
46
+ // A release build's print reaches the device log (idevicesyslog -m nomos); debugPrint
47
+ // would throttle and batch it, which skews the very timings being read.
48
+ // ignore: avoid_print
49
+ print('[nomos-trace] t=${now()} L=$layer e=$event $fields');
50
+ }
51
+
52
+ /// The trace context every Runtime v2 request carries while tracing: `sampled` asks the native
53
+ /// host and the Peer Office to write their lines too. No ids are set, so the office keeps
54
+ /// deriving span ids from the request id exactly as when tracing is off.
55
+ @internal
56
+ static nomos_protocol.TraceContext sampledContext() =>
57
+ nomos_protocol.TraceContext(sampled: true);
58
+
59
+ /// What a Runtime v2 request asks for, as an operation kind (never argument values):
60
+ /// `kernelCall/offer:estate/placeAssetOnDrawing`, `kernelCall/query:rows:assetsByAssetId`, `watch`…
61
+ static String describeRequest(nomos_protocol.RuntimeRequest request) {
62
+ final command = request.whichCommand().name;
63
+ if (request.hasKernelCall()) {
64
+ return '$command/${describeCall(request.kernelCall.request)}';
65
+ }
66
+ if (request.hasWatch() && request.watch.hasQuery()) {
67
+ return '$command/${_describeQuery(request.watch.query)}';
68
+ }
69
+ return command;
70
+ }
71
+
72
+ /// A kernel request's operation kind.
73
+ static String describeCall(nomos_protocol.CallRequest call) {
74
+ if (call.hasOffer()) {
75
+ final offer = call.offer;
76
+ return offer.hasCall()
77
+ ? 'offer:${offer.call.domain}/${offer.call.directive}'
78
+ : 'offer:${offer.whichIntent().name}';
79
+ }
80
+ if (call.hasQuery()) return _describeQuery(call.query);
81
+ return call.whichOperation().name;
82
+ }
83
+
84
+ /// [source] with each data event's synchronous listener time traced as `e=watch.handler`
85
+ /// ([fields] identify the watch). The listener runs inside the transform's data handler, so
86
+ /// this is the time the application spent on one emission before yielding. Unchanged when off.
87
+ @internal
88
+ static Stream<T> timedListener<T>(Stream<T> source, String fields) {
89
+ if (!enabled) return source;
90
+ return source.transform(StreamTransformer<T, T>.fromHandlers(
91
+ handleData: (value, sink) {
92
+ final started = now();
93
+ sink.add(value);
94
+ emit('dart', 'watch.handler', '$fields us=${now() - started}');
95
+ },
96
+ ));
97
+ }
98
+
99
+ static String _describeQuery(nomos_protocol.QueryRequest query) {
100
+ if (query.hasRows()) return 'query:rows:${query.rows.id}';
101
+ if (query.hasPage()) return 'query:page:${query.page.id}';
102
+ if (query.hasById()) return 'query:byId:${query.byId.aggregateType}';
103
+ if (query.hasByType()) return 'query:byType:${query.byType.aggregateType}';
104
+ if (query.hasNamed()) return 'query:named:${query.named.operation}';
105
+ return 'query:${query.whichQuery().name}';
106
+ }
107
+ }