@githolon/dsl 0.84.0 → 0.85.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
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
## 0.55.2
|
|
2
|
+
|
|
3
|
+
- Session-state DX (co2): explicit workspace lifecycle `state` on `listWorkspaces()` —
|
|
4
|
+
`connected | resident | parked` — so a listed-but-unopened workspace (e.g. a locally-born
|
|
5
|
+
child) is distinguishable at a glance, ending the "listed but `session(ws)` fails on first op"
|
|
6
|
+
footgun.
|
|
7
|
+
- `NomosRef.acquire(bridge)` — open a born/resident ref directly:
|
|
8
|
+
`await outcome.bornRefs.single.acquire(bridge)` (sugar over `acquireSession`, keyed by the
|
|
9
|
+
ref's workspace).
|
|
10
|
+
- `NomosSession.isConnected` + `assertConnected()` — the fail-at-bind guard: a clear, actionable
|
|
11
|
+
`StateError` instead of an opaque "session is not connected" on the first read/write.
|
|
12
|
+
- Patch bump — keeps the exact-minor `^0.55.0` pins valid, no generated-client re-pin needed.
|
|
13
|
+
|
|
1
14
|
## 0.55.1
|
|
2
15
|
|
|
3
16
|
- Add `deriveWorkspaceName(namespace, key)` — re-derive the workspace name a keyed birth
|
|
@@ -689,6 +689,25 @@ class NomosOfferOutcome {
|
|
|
689
689
|
.toList();
|
|
690
690
|
}
|
|
691
691
|
|
|
692
|
+
/// OPEN A REF'S WORKSPACE DIRECTLY (co2 DX ask) — `await outcome.bornRefs.single.acquire(bridge)`. Sugar over
|
|
693
|
+
/// [NomosBridge.acquireSession] keyed by the ref's `workspace`: an already-open session returns as-is; a
|
|
694
|
+
/// device-resident/parked/just-born workspace opens LOCAL-ONLY in place (no cloud, no credentials); only a
|
|
695
|
+
/// genuinely-absent workspace needs [ifAbsentCredentials] (else a named [StateError]). Returns the live
|
|
696
|
+
/// [NomosSession] — so the failure surfaces HERE (at open), never later on the client's first query. Defined
|
|
697
|
+
/// in nomos_client (NOT on the pure `nomos_types` [NomosRef]) so the types package stays bridge-free.
|
|
698
|
+
extension NomosRefAcquire on NomosRef {
|
|
699
|
+
Future<NomosSession> acquire(
|
|
700
|
+
NomosBridge bridge, {
|
|
701
|
+
Future<NomosSessionCredentials> Function()? ifAbsentCredentials,
|
|
702
|
+
}) {
|
|
703
|
+
if (workspace.isEmpty) {
|
|
704
|
+
throw ArgumentError.value(this, 'ref',
|
|
705
|
+
'NomosRef.acquire: the ref carries no workspace — only a workspace-complete ref is openable');
|
|
706
|
+
}
|
|
707
|
+
return bridge.acquireSession(workspace, ifAbsentCredentials: ifAbsentCredentials);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
692
711
|
/// What a GENUINE cloud open needs — returned by an [NomosBridge.acquireSession] `ifAbsentCredentials`
|
|
693
712
|
/// callback, invoked ONLY when the workspace is nowhere on the local plane (never for a local-only or
|
|
694
713
|
/// already-mounted workspace). Mirrors the cloud-shaped subset of [NomosBridge.connect]'s parameters.
|
|
@@ -1026,13 +1045,28 @@ class NomosBridge {
|
|
|
1026
1045
|
List<String> get connectedSessions =>
|
|
1027
1046
|
List.unmodifiable(_eventPlane._sessionNames);
|
|
1028
1047
|
|
|
1029
|
-
/// ENUMERATE THE LOCAL PLANE — every workspace
|
|
1030
|
-
/// `[{ session, workspace, mounted, syncPosture
|
|
1031
|
-
///
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1048
|
+
/// ENUMERATE THE LOCAL PLANE — every workspace on this device + its lifecycle STATE and sync posture:
|
|
1049
|
+
/// `[{ session, workspace, state, mounted, parked, syncPosture }]`. The explicit **`state`** (co2 DX ask)
|
|
1050
|
+
/// is the field to branch on — it removes the "listed but `session(ws)` fails on first op" footgun:
|
|
1051
|
+
/// • `"connected"` — a live open session; `bridge.session(ws)` works, reads/writes flow NOW;
|
|
1052
|
+
/// • `"resident"` — custody is on the device but no session is open yet (e.g. a locally-born child) —
|
|
1053
|
+
/// call `bridge.acquireSession(ws)` / `ref.acquire(bridge)` before using it;
|
|
1054
|
+
/// • `"parked"` — custody resident but the holon is LRU-disposed; it re-mounts on demand (acquire).
|
|
1055
|
+
/// `syncPosture` is `"local-only"` (no cloud counterpart) or `"cloud-bound"`.
|
|
1056
|
+
Future<List<Map<String, Object?>>> listWorkspaces() async {
|
|
1057
|
+
final connected = _eventPlane._sessionNames.toSet();
|
|
1058
|
+
return (((_asMap(await _call('listWorkspaces'))['workspaces']) as List?) ?? const [])
|
|
1059
|
+
.map((e) => (e as Map).cast<String, Object?>())
|
|
1060
|
+
.map((w) {
|
|
1061
|
+
final ws = w['workspace']?.toString() ?? '';
|
|
1062
|
+
// `connected` is the app's own connect() truth; `parked`/resident come from the runner realm.
|
|
1063
|
+
w['state'] = (w['parked'] == true)
|
|
1064
|
+
? 'parked'
|
|
1065
|
+
: (connected.contains(ws) ? 'connected' : 'resident');
|
|
1066
|
+
return w;
|
|
1067
|
+
})
|
|
1068
|
+
.toList();
|
|
1069
|
+
}
|
|
1036
1070
|
|
|
1037
1071
|
/// Open a workspace on this plane: boot its LOCAL githolon under the installed law (from [restoreFrom]
|
|
1038
1072
|
/// custody, or a cloud pull when [cloud] is given). This is the sugar the single-workspace path uses; a
|
|
@@ -2362,6 +2396,26 @@ class NomosSession extends NomosBridge {
|
|
|
2362
2396
|
@override
|
|
2363
2397
|
String? get _sessionName => workspace;
|
|
2364
2398
|
|
|
2399
|
+
/// Whether this workspace has a LIVE open session on the plane (the app `connect`/`acquireSession`d it). A
|
|
2400
|
+
/// lazily-created `bridge.session(ws)` handle is valid to HOLD before connect, but an op on it fails until
|
|
2401
|
+
/// the workspace is open. Check this — or prefer `bridge.acquireSession(ws)` / `ref.acquire(bridge)`, which
|
|
2402
|
+
/// open-then-return — so a not-connected mistake is caught at BIND, not on the client's first query.
|
|
2403
|
+
bool get isConnected => parent.connectedSessions.contains(workspace);
|
|
2404
|
+
|
|
2405
|
+
/// Fail-at-bind guard (co2 DX ask): throw a CLEAR, actionable [StateError] if this workspace is not open
|
|
2406
|
+
/// yet, so a generated client can call it in its constructor and surface the mistake immediately instead of
|
|
2407
|
+
/// an opaque "session is not connected" on the first read/write.
|
|
2408
|
+
void assertConnected() {
|
|
2409
|
+
if (!isConnected) {
|
|
2410
|
+
throw StateError(
|
|
2411
|
+
'NomosSession("$workspace") is not connected — the workspace is not open on this device\'s plane. '
|
|
2412
|
+
'Open it first: `await bridge.acquireSession("$workspace")` (or `await ref.acquire(bridge)` for a '
|
|
2413
|
+
'born child), then bind the client to the returned session. A held-but-unopened session handle is '
|
|
2414
|
+
'valid to construct but cannot serve ops until connected.',
|
|
2415
|
+
);
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2365
2419
|
/// Sibling workspaces resolve on the shared plane — delegate to the parent's registry (never nest a
|
|
2366
2420
|
/// session inside a session).
|
|
2367
2421
|
@override
|
package/dart/pubspec.yaml
CHANGED
|
@@ -11,7 +11,7 @@ description: >-
|
|
|
11
11
|
Nomos 2 Dart frontend types — the hand-written kernel wire types plus the
|
|
12
12
|
code-generated per-domain aggregate + directive-payload types. (De)serializes
|
|
13
13
|
the EXACT serde JSON the Rust kernel + projection produce/consume.
|
|
14
|
-
version: 0.55.
|
|
14
|
+
version: 0.55.2
|
|
15
15
|
|
|
16
16
|
environment:
|
|
17
17
|
sdk: ^3.5.0
|
|
@@ -41,14 +41,21 @@ class FakeRunner implements BridgeTransport {
|
|
|
41
41
|
result = {'workspace': args['workspace']};
|
|
42
42
|
break;
|
|
43
43
|
case 'listWorkspaces':
|
|
44
|
+
// Realm truth: a CONNECTED workspace is mounted (holon live); a resident-but-unconnected one is
|
|
45
|
+
// parked custody. Reflect connects so the derived lifecycle `state` is observable in both postures.
|
|
46
|
+
final connected = sent
|
|
47
|
+
.where((m) => m['op'] == 'connect')
|
|
48
|
+
.map((m) => ((m['args'] as Map)['workspace']).toString())
|
|
49
|
+
.toSet();
|
|
50
|
+
final all = {...resident, ...connected};
|
|
44
51
|
result = {
|
|
45
52
|
'workspaces': [
|
|
46
|
-
for (final ws in
|
|
53
|
+
for (final ws in all)
|
|
47
54
|
{
|
|
48
55
|
'session': ws,
|
|
49
56
|
'workspace': ws,
|
|
50
|
-
'mounted':
|
|
51
|
-
'parked':
|
|
57
|
+
'mounted': connected.contains(ws),
|
|
58
|
+
'parked': !connected.contains(ws) && resident.contains(ws),
|
|
52
59
|
'syncPosture': 'local-only'
|
|
53
60
|
}
|
|
54
61
|
]
|
|
@@ -149,4 +156,64 @@ void main() {
|
|
|
149
156
|
expect(identical(viaSession, bridge.session('estate')), isTrue);
|
|
150
157
|
await bridge.dispose();
|
|
151
158
|
});
|
|
159
|
+
|
|
160
|
+
// ── SESSION-STATE DX cluster (co2): explicit lifecycle state + fail-at-bind + openable refs ──
|
|
161
|
+
|
|
162
|
+
test('listWorkspaces() reports explicit state: resident/parked before open, connected after', () async {
|
|
163
|
+
final runner = FakeRunner(resident: ['estate']); // realm returns mounted:false, parked:true
|
|
164
|
+
final bridge = NomosBridge(runner);
|
|
165
|
+
await bridge.ready;
|
|
166
|
+
|
|
167
|
+
// Before any connect: the plane-resident (parked) workspace surfaces with an explicit non-connected state.
|
|
168
|
+
var list = await bridge.listWorkspaces();
|
|
169
|
+
final before = list.firstWhere((w) => w['workspace'] == 'estate');
|
|
170
|
+
expect(before['state'], 'parked', reason: 'parked custody is not connected — the app must acquire it first');
|
|
171
|
+
|
|
172
|
+
// After connecting it, the SAME workspace reports connected.
|
|
173
|
+
await bridge.connect(workspace: 'estate', session: 'estate');
|
|
174
|
+
list = await bridge.listWorkspaces();
|
|
175
|
+
final after = list.firstWhere((w) => w['workspace'] == 'estate');
|
|
176
|
+
expect(after['state'], 'connected');
|
|
177
|
+
await bridge.dispose();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('NomosSession.isConnected + assertConnected: fail at BIND, not on first query', () async {
|
|
181
|
+
final runner = FakeRunner();
|
|
182
|
+
final bridge = NomosBridge(runner);
|
|
183
|
+
await bridge.ready;
|
|
184
|
+
|
|
185
|
+
// A held-but-unopened handle is valid to construct but not connected — assertConnected throws NOW.
|
|
186
|
+
final lazy = bridge.session('estate');
|
|
187
|
+
expect(lazy.isConnected, isFalse);
|
|
188
|
+
expect(lazy.assertConnected, throwsStateError);
|
|
189
|
+
|
|
190
|
+
await bridge.connect(workspace: 'estate', session: 'estate');
|
|
191
|
+
final live = bridge.session('estate');
|
|
192
|
+
expect(live.isConnected, isTrue);
|
|
193
|
+
expect(live.assertConnected, returnsNormally);
|
|
194
|
+
await bridge.dispose();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('NomosRef.acquire(bridge) opens a born/resident ref directly → a live session', () async {
|
|
198
|
+
final runner = FakeRunner(resident: ['estate-42']);
|
|
199
|
+
final bridge = NomosBridge(runner);
|
|
200
|
+
await bridge.ready;
|
|
201
|
+
|
|
202
|
+
// The shape a `.births()` outcome yields: outcome.bornRefs.single is a NomosRef on the born workspace.
|
|
203
|
+
final ref = NomosRef(workspace: 'estate-42', type: 'nomos.Workspace', id: 'estate-42');
|
|
204
|
+
final session = await ref.acquire(bridge); // no credentials — resident opens local-only
|
|
205
|
+
expect(session.workspace, 'estate-42');
|
|
206
|
+
expect(session.isConnected, isTrue);
|
|
207
|
+
expect(identical(session, bridge.session('estate-42')), isTrue);
|
|
208
|
+
await bridge.dispose();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test('NomosRef.acquire refuses a workspace-less ref (a bad locator fails HERE, not later)', () async {
|
|
212
|
+
final runner = FakeRunner();
|
|
213
|
+
final bridge = NomosBridge(runner);
|
|
214
|
+
await bridge.ready;
|
|
215
|
+
final bad = NomosRef(workspace: '', type: 'nomos.Workspace', id: 'x');
|
|
216
|
+
expect(() => bad.acquire(bridge), throwsArgumentError);
|
|
217
|
+
await bridge.dispose();
|
|
218
|
+
});
|
|
152
219
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@githolon/dsl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.85.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Nomos 2 domain-authoring DSL: aggregates + directives in TS, executed and encoded to the Rust kernel's wire shapes.",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
@@ -837,6 +837,21 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
837
837
|
` return ${className}(session, actor: actor${encPresent ? ", keyStore: keyStore" : ""}).._preflighted = true;`,
|
|
838
838
|
` }`,
|
|
839
839
|
``,
|
|
840
|
+
` /// OPEN-THEN-BIND (session_bound_clients.md ask 3) — the ONE-CALL path for a born/resident workspace:`,
|
|
841
|
+
` /// \`await ${className}.open(bridge, ref: outcome.bornRefs.single)\` (or \`workspace: 'estate-42'\`).`,
|
|
842
|
+
` /// It ACQUIRES the workspace's session on [bridge] first (an already-open one is reused; a device-`,
|
|
843
|
+
` /// resident/parked/just-born one opens local-only in place) and THEN binds — so a not-open workspace`,
|
|
844
|
+
` /// fails HERE, at open, never later on the client's first query. For a genuinely-absent cloud workspace,`,
|
|
845
|
+
` /// \`acquireSession(ws, ifAbsentCredentials: ...)\` it yourself then [bind].`,
|
|
846
|
+
` static Future<${className}> open(NomosBridge bridge, {NomosRef? ref, String? workspace, String? actor${encPresent ? ", NomosKeyStore? keyStore" : ""}}) async {`,
|
|
847
|
+
` final ws = ref?.workspace ?? workspace;`,
|
|
848
|
+
` if (ws == null || ws.isEmpty) {`,
|
|
849
|
+
` throw ArgumentError('${className}.open: pass a workspace: or ref: — the workspace to open and bind');`,
|
|
850
|
+
` }`,
|
|
851
|
+
` final session = await bridge.acquireSession(ws);`,
|
|
852
|
+
` return bind(session, actor: actor${encPresent ? ", keyStore: keyStore" : ""});`,
|
|
853
|
+
` }`,
|
|
854
|
+
``,
|
|
840
855
|
` /// The session workspace this client is bound to (from the [NomosSession] handle, when constructed with`,
|
|
841
856
|
` /// one) — the \`workspace\` of every read row's canonical [NomosRef]. '' on a bare (non-session) bridge.`,
|
|
842
857
|
` String get _ws {`,
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
*/
|
|
48
48
|
import { execFileSync } from "node:child_process";
|
|
49
49
|
import { expandCapabilityExports, isCapabilityDecl } from "./capability_exports.js";
|
|
50
|
-
import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
50
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
51
51
|
import { createRequire } from "node:module";
|
|
52
52
|
import path from "node:path";
|
|
53
53
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
@@ -684,6 +684,60 @@ function removeLegacyFlatDartLayout(dartOut: string, moduleNames: readonly strin
|
|
|
684
684
|
* analyzer-clean plug-and-play entrypoint (zero `show`/`hide`; framework-shared
|
|
685
685
|
* symbols dedup into `nomos_framework.dart`, so no export is ambiguous).
|
|
686
686
|
*/
|
|
687
|
+
/**
|
|
688
|
+
* COMPILE ATOMICITY (co2 dev report) — a failed compile MUST NOT delete the tenant's already-good generated
|
|
689
|
+
* packages. Both Dart emit paths used to `rmSync(genDir)` up-front and then write; a mid-emit throw (a codegen
|
|
690
|
+
* error, a flow-gate refusal surfaced during emit) left the dir WIPED or partial, so the dev could no longer
|
|
691
|
+
* `dart analyze` / open the app. Instead, each generated dir is renamed ASIDE to `<dir>.nomos-bak`, a fresh
|
|
692
|
+
* empty dir is created, and the emit writes into it. On success the whole emit `commit()`s (drops the backups);
|
|
693
|
+
* on ANY throw it `rollback()`s (removes the partial dir, restores the backup). The transactions are collected
|
|
694
|
+
* module-side and committed/rolled-back once around the whole Dart emit (the CLI is single-threaded per run).
|
|
695
|
+
*/
|
|
696
|
+
const _genDirTx: { commit(): void; rollback(): void }[] = [];
|
|
697
|
+
let _genDirTxHandlerInstalled = false;
|
|
698
|
+
/** Install the ONE finalizer. A compile refusal is `fail()` → `process.exit(1)`, which BYPASSES try/catch —
|
|
699
|
+
* so the only reliable place to finalize the transactional gen dirs is the process `exit` event (a clean
|
|
700
|
+
* exit commits, any non-zero exit rolls back). Sync only (rmSync/renameSync are sync — required in `exit`). */
|
|
701
|
+
function installGenDirTxHandler(): void {
|
|
702
|
+
if (_genDirTxHandlerInstalled) return;
|
|
703
|
+
_genDirTxHandlerInstalled = true;
|
|
704
|
+
process.on("exit", (code) => {
|
|
705
|
+
for (const t of _genDirTx) {
|
|
706
|
+
try {
|
|
707
|
+
if (code === 0) t.commit();
|
|
708
|
+
else t.rollback();
|
|
709
|
+
} catch {
|
|
710
|
+
/* best-effort finalize — never mask the real exit */
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
/** Begin a transactional regeneration of `genDir` (see {@link _genDirTx}): back the prior dir aside + create a
|
|
716
|
+
* fresh one, and register idempotent commit/rollback finalized at process exit. Replaces the old destructive
|
|
717
|
+
* `rmSync(genDir); mkdirSync(genDir)` so a mid-emit `fail()`/throw restores the prior good package. */
|
|
718
|
+
function beginAtomicGenDir(genDir: string): void {
|
|
719
|
+
installGenDirTxHandler();
|
|
720
|
+
const bak = `${genDir}.nomos-bak`;
|
|
721
|
+
rmSync(bak, { recursive: true, force: true }); // clear any stale backup from a previously-killed run
|
|
722
|
+
const hadPrior = existsSync(genDir);
|
|
723
|
+
if (hadPrior) renameSync(genDir, bak);
|
|
724
|
+
mkdirSync(genDir, { recursive: true });
|
|
725
|
+
let done = false;
|
|
726
|
+
_genDirTx.push({
|
|
727
|
+
commit() {
|
|
728
|
+
if (done) return;
|
|
729
|
+
done = true;
|
|
730
|
+
rmSync(bak, { recursive: true, force: true });
|
|
731
|
+
},
|
|
732
|
+
rollback() {
|
|
733
|
+
if (done) return;
|
|
734
|
+
done = true;
|
|
735
|
+
rmSync(genDir, { recursive: true, force: true });
|
|
736
|
+
if (hadPrior) renameSync(bak, genDir);
|
|
737
|
+
},
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
|
|
687
741
|
function emitDart(
|
|
688
742
|
modules: readonly DomainModule[],
|
|
689
743
|
dartOut: string,
|
|
@@ -698,8 +752,8 @@ function emitDart(
|
|
|
698
752
|
const sourceByFile = new Map<string, string>(); // fileBase → src, for precise alias-import selection
|
|
699
753
|
const genDir = path.join(dartOut, DART_GENERATED_SUBDIR);
|
|
700
754
|
removeLegacyFlatDartLayout(dartOut, modules.map((m) => m.name));
|
|
701
|
-
|
|
702
|
-
|
|
755
|
+
// Transactional: back the prior generated dir aside instead of destroying it (compile atomicity).
|
|
756
|
+
beginAtomicGenDir(genDir);
|
|
703
757
|
|
|
704
758
|
// 1. optional vendored framework support (barrel + src/), copied byte-for-byte except
|
|
705
759
|
// the barrel's schema_validation export (see DART_SUPPORT_SRC).
|
|
@@ -1073,8 +1127,8 @@ function emitDartPackages(
|
|
|
1073
1127
|
const pkgDir = (name: string): string => path.join(dartOut, name);
|
|
1074
1128
|
const freshGenDir = (name: string): string => {
|
|
1075
1129
|
const g = path.join(pkgDir(name), "lib", DART_GENERATED_SUBDIR);
|
|
1076
|
-
|
|
1077
|
-
|
|
1130
|
+
// Transactional: back the prior dir aside instead of destroying it (compile atomicity).
|
|
1131
|
+
beginAtomicGenDir(g);
|
|
1078
1132
|
return g;
|
|
1079
1133
|
};
|
|
1080
1134
|
|
|
@@ -2604,6 +2658,8 @@ async function main(): Promise<void> {
|
|
|
2604
2658
|
cfgDir,
|
|
2605
2659
|
typeof dartCfg === "object" && dartCfg.out !== undefined ? dartCfg.out : path.join(outDir, "dart"),
|
|
2606
2660
|
);
|
|
2661
|
+
// COMPILE ATOMICITY (co2): the Dart emit is transactional (beginAtomicGenDir) — a mid-emit fail()/throw
|
|
2662
|
+
// restores the prior good packages instead of leaving them wiped/partial. Finalized at process exit.
|
|
2607
2663
|
if (dartPackages !== undefined) {
|
|
2608
2664
|
const r = emitDartPackages(domainModules, dartOut, domainHash, packageInterface, dartPackages, dartNames, nomosCompat);
|
|
2609
2665
|
dartFiles = r.written;
|