nmarshall commited on
Commit
6dbc461
·
1 Parent(s): 7e35e0b

gameplay: resolve duration command persistence

Browse files
crates/urt-gameplay/src/bin/urt-filter-commands.rs CHANGED
@@ -149,6 +149,111 @@ fn main() {
149
  let out = remove_filter_command(&mut rows, &text, issuer, &mut ring).unwrap();
150
  println!("{}", command_json(out, &rows, n, ring, 0));
151
  }
 
152
  _ => panic!("unknown policy"),
153
  }
154
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  let out = remove_filter_command(&mut rows, &text, issuer, &mut ring).unwrap();
150
  println!("{}", command_json(out, &rows, n, ring, 0));
151
  }
152
+ "duration-export" => println!("{}", duration_export(&w)),
153
  _ => panic!("unknown policy"),
154
  }
155
  }
156
+
157
+ fn exported_json(p: Option<urt_gameplay::filter_lifecycle::FilterExport>) -> String {
158
+ let Some(p) = p else {
159
+ return r#"{"events":[],"formats":[],"buffer":"7374616c65"}"#.into();
160
+ };
161
+ use urt_gameplay::filter_lifecycle::FilterFileEffect;
162
+ let events = p
163
+ .effects()
164
+ .iter()
165
+ .map(|e| match e {
166
+ FilterFileEffect::Print(t) => format!(r#"{{"op":"print","text":"{}"}}"#, hex(t)),
167
+ FilterFileEffect::Open { path, mode } => {
168
+ format!(r#"{{"op":"open","path":"{}","mode":{}}}"#, hex(path), mode)
169
+ }
170
+ FilterFileEffect::Write { handle, bytes } => format!(
171
+ r#"{{"op":"write","handle":{},"text":"{}"}}"#,
172
+ handle,
173
+ hex(bytes)
174
+ ),
175
+ FilterFileEffect::Close(h) => format!(r#"{{"op":"close","handle":{}}}"#, h),
176
+ })
177
+ .collect::<Vec<_>>()
178
+ .join(",");
179
+ let formats = p
180
+ .lines
181
+ .iter()
182
+ .map(|l| format!("\"{}\"", hex(l)))
183
+ .collect::<Vec<_>>()
184
+ .join(",");
185
+ format!(
186
+ r#"{{"events":[{}],"formats":[{}],"buffer":"{}"}}"#,
187
+ events,
188
+ formats,
189
+ hex(&p.text)
190
+ )
191
+ }
192
+ fn duration_export(w: &[u32]) -> String {
193
+ let mut p = 4;
194
+ let address = blob(w, &mut p);
195
+ let duration = blob(w, &mut p);
196
+ assert_eq!(p, w.len());
197
+ let fields = [13, w[0], 0, 4, 5, 126, 6, 0, 0];
198
+ let issuer = if w[1] == u32::MAX {
199
+ None
200
+ } else {
201
+ Some(w[1] as i32)
202
+ };
203
+ let mut ring = w[2];
204
+ let mut rows = [
205
+ FilterRecord {
206
+ mask: u32::MAX,
207
+ comparison: 0x04030201,
208
+ metadata: 77,
209
+ },
210
+ FilterRecord {
211
+ mask: u32::MAX,
212
+ comparison: 0x04030201,
213
+ metadata: 88,
214
+ },
215
+ ];
216
+ let out = resolved_filter_duration(
217
+ &mut rows, &address, &duration, &fields, issuer, &mut ring, w[3],
218
+ )
219
+ .unwrap();
220
+ let exported = out
221
+ .export
222
+ .map(|p| exported_json(Some(p)))
223
+ .unwrap_or_default();
224
+ let out = out.command;
225
+ use urt_gameplay::filter_loading::FilterDurationEffect;
226
+ let events = out
227
+ .effects
228
+ .iter()
229
+ .map(|e| match e {
230
+ FilterDurationEffect::Console(t) => {
231
+ format!(r#"{{"op":"console","text":"{}"}}"#, hex(t))
232
+ }
233
+ FilterDurationEffect::Send { recipient, text } => format!(
234
+ r#"{{"op":"send","recipient":{},"text":"{}"}}"#,
235
+ recipient,
236
+ hex(text)
237
+ ),
238
+ FilterDurationEffect::Time => r#"{"op":"time"}"#.into(),
239
+ FilterDurationEffect::Metadata { text, value } => format!(
240
+ r#"{{"op":"metadata","text":"{}","metadata":{},"save":1}}"#,
241
+ hex(text),
242
+ value
243
+ ),
244
+ })
245
+ .collect::<Vec<String>>()
246
+ .join(",");
247
+ let updates = out
248
+ .update
249
+ .map(|p| children_json(&[p.into()]))
250
+ .unwrap_or_default();
251
+ format!(
252
+ r#"{{"events":[{}],"updates":[{}],"ring":{},"rows":[{}],"exported":[{}]}}"#,
253
+ events,
254
+ updates,
255
+ ring,
256
+ rows.into_iter().map(row_json).collect::<Vec<_>>().join(","),
257
+ exported
258
+ )
259
+ }
crates/urt-gameplay/src/filter_commands.rs CHANGED
@@ -340,3 +340,90 @@ mod remove_tests {
340
  assert_eq!(ring, 7);
341
  }
342
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
340
  assert_eq!(ring, 7);
341
  }
342
  }
343
+
344
+ #[derive(Clone, Debug, PartialEq, Eq)]
345
+ pub struct ResolvedFilterDuration {
346
+ pub command: crate::filter_loading::FilterDuration,
347
+ pub export: Option<crate::filter_lifecycle::FilterExport>,
348
+ }
349
+ /// Compose252213/252127/252500, resolving a successful save request into exact
350
+ /// export bytes/effects while retaining captured native time/handle replies.
351
+ pub fn resolved_filter_duration(
352
+ records: &mut [FilterRecord],
353
+ address: &[u8],
354
+ duration: &[u8],
355
+ fields: &[u32; 9],
356
+ issuer: Option<i32>,
357
+ ring: &mut u32,
358
+ handle: u32,
359
+ ) -> Result<ResolvedFilterDuration, Error> {
360
+ let mut staged = records.to_vec();
361
+ let mut counter = *ring;
362
+ let command = crate::filter_loading::filter_duration(
363
+ &mut staged,
364
+ address,
365
+ duration,
366
+ fields,
367
+ issuer,
368
+ &mut counter,
369
+ )?;
370
+ let export = if command.update.as_ref().is_some_and(|p| p.export) {
371
+ crate::filter_lifecycle::export_filters(&staged, handle)?
372
+ } else {
373
+ None
374
+ };
375
+ records.copy_from_slice(&staged);
376
+ *ring = counter;
377
+ Ok(ResolvedFilterDuration { command, export })
378
+ }
379
+ #[cfg(test)]
380
+ mod duration_export_tests {
381
+ use super::*;
382
+ #[test]
383
+ fn first_duplicate_updates_then_both_export_in_order_through_zero_handle() {
384
+ let row = FilterRecord {
385
+ mask: u32::MAX,
386
+ comparison: 0x04030201,
387
+ metadata: 7,
388
+ };
389
+ let mut rows = [row, FilterRecord { metadata: 8, ..row }];
390
+ let mut ring = 9;
391
+ let p = resolved_filter_duration(&mut rows, b"1.2.3.4", b"5", &[0; 9], None, &mut ring, 0)
392
+ .unwrap();
393
+ let export = p.export.unwrap();
394
+ assert_eq!(export.text, b"1.2.3.4:5\n1.2.3.4:8\n");
395
+ assert_eq!(export.handle, 0);
396
+ assert_eq!(ring, 9);
397
+ assert!(p.command.update.unwrap().export);
398
+ }
399
+ #[test]
400
+ fn usage_miss_and_unsupported_domains_do_not_export_or_partially_mutate() {
401
+ let row = FilterRecord {
402
+ mask: u32::MAX,
403
+ comparison: 0x04030201,
404
+ metadata: 7,
405
+ };
406
+ let mut rows = [row];
407
+ let mut ring = u32::MAX;
408
+ assert!(
409
+ resolved_filter_duration(&mut rows, b"", b"5", &[0; 9], Some(2), &mut ring, 7)
410
+ .unwrap()
411
+ .export
412
+ .is_none()
413
+ );
414
+ assert_eq!(ring, 0);
415
+ assert!(
416
+ resolved_filter_duration(&mut rows, b"2.3.4.5", b"5", &[0; 9], None, &mut ring, 7)
417
+ .unwrap()
418
+ .export
419
+ .is_none()
420
+ );
421
+ assert_eq!(rows, [row]);
422
+ assert!(
423
+ resolved_filter_duration(&mut rows, &[b'1'; 128], b"5", &[0; 9], None, &mut ring, 7)
424
+ .is_err()
425
+ );
426
+ assert_eq!(rows, [row]);
427
+ assert_eq!(ring, 0);
428
+ }
429
+ }
docs/FINDINGS.md CHANGED
@@ -1849,3 +1849,5 @@ RE-956: Twenty-four actual253245 wrappers compose actual253296 removal and sourc
1849
  RE-957: Rust matches24 original removal-wrapper cases, preserving exact client/console usage, formatter wrap, first-match duplicate/sentinel mutations, child diagnostics and conditional export. The API leaves caller table length and userinfo selector ownership untouched. Unit tests cover empty-input child suppression, duplicate metadata retention, ordinary parser failure and atomic unsupported-token rejection. Native effects remain captured plans. [Evidence](research/RUST_FILTER_REMOVE_COMMAND.md).
1850
 
1851
  RE-958: Eighty actual252213/252127/252500 compositions resolve the prior captured export request into source-generated bytes and ordered print/open/write/close effects. Duplicate metadata updates only the first row, then export serializes both rows in order. Usage, invalid parsing and absent matches skip export. Supplied zero handles still reach write/close; time/network/file execution remains captured or supplied. [Evidence](research/QVM_FILTER_DURATION_EXPORT.md).
 
 
 
1849
  RE-957: Rust matches24 original removal-wrapper cases, preserving exact client/console usage, formatter wrap, first-match duplicate/sentinel mutations, child diagnostics and conditional export. The API leaves caller table length and userinfo selector ownership untouched. Unit tests cover empty-input child suppression, duplicate metadata retention, ordinary parser failure and atomic unsupported-token rejection. Native effects remain captured plans. [Evidence](research/RUST_FILTER_REMOVE_COMMAND.md).
1850
 
1851
  RE-958: Eighty actual252213/252127/252500 compositions resolve the prior captured export request into source-generated bytes and ordered print/open/write/close effects. Duplicate metadata updates only the first row, then export serializes both rows in order. Usage, invalid parsing and absent matches skip export. Supplied zero handles still reach write/close; time/network/file execution remains captured or supplied. [Evidence](research/QVM_FILTER_DURATION_EXPORT.md).
1852
+
1853
+ RE-959: Rust matches80 original duration/update/export compositions, deriving exact file payloads and typed effects from a successful metadata save request. Duplicate order, zero handles, usage/miss export suppression and formatter counters are retained. Unit tests cover first-row update with two-row export and atomic unsupported-domain rejection. Native time/file/network execution remains supplied or captured. [Evidence](research/RUST_FILTER_DURATION_EXPORT.md).
docs/PROVISIONAL_TODO.md CHANGED
@@ -1899,3 +1899,5 @@ RE-944–953 completed bounded time/expiry/loading/duration and export/reload co
1899
  - [x] RE-957 Rust remove-filter wrapper and source usage effects ([evidence](research/RUST_FILTER_REMOVE_COMMAND.md)).
1900
 
1901
  - [x] RE-958 Original duration command with metadata and export resolved ([evidence](research/QVM_FILTER_DURATION_EXPORT.md)).
 
 
 
1899
  - [x] RE-957 Rust remove-filter wrapper and source usage effects ([evidence](research/RUST_FILTER_REMOVE_COMMAND.md)).
1900
 
1901
  - [x] RE-958 Original duration command with metadata and export resolved ([evidence](research/QVM_FILTER_DURATION_EXPORT.md)).
1902
+
1903
+ - [x] RE-959 Rust duration command with resolved persistence plan ([evidence](research/RUST_FILTER_DURATION_EXPORT.md)).
docs/RESEARCH_LEDGER.md CHANGED
@@ -2501,3 +2501,7 @@ Rust matches24 original removal-wrapper cases, preserving exact client/console u
2501
  ## 2026-10-28 — RE-958 Original duration command with metadata and export resolved
2502
 
2503
  Eighty actual252213/252127/252500 compositions resolve the prior captured export request into source-generated bytes and ordered print/open/write/close effects. Duplicate metadata updates only the first row, then export serializes both rows in order. Usage, invalid parsing and absent matches skip export. Supplied zero handles still reach write/close; time/network/file execution remains captured or supplied. Focused `cargo xtask test-group qvm-filter-duration-export` passes with deterministic report regeneration, relevant regressions and storage/documentation/whitespace checks. Full suite deferred to the tenth-commit boundary. Original assets unchanged; native IO effects remain captured.
 
 
 
 
 
2501
  ## 2026-10-28 — RE-958 Original duration command with metadata and export resolved
2502
 
2503
  Eighty actual252213/252127/252500 compositions resolve the prior captured export request into source-generated bytes and ordered print/open/write/close effects. Duplicate metadata updates only the first row, then export serializes both rows in order. Usage, invalid parsing and absent matches skip export. Supplied zero handles still reach write/close; time/network/file execution remains captured or supplied. Focused `cargo xtask test-group qvm-filter-duration-export` passes with deterministic report regeneration, relevant regressions and storage/documentation/whitespace checks. Full suite deferred to the tenth-commit boundary. Original assets unchanged; native IO effects remain captured.
2504
+
2505
+ ## 2026-10-28 — RE-959 Rust duration command with resolved persistence plan
2506
+
2507
+ Rust matches80 original duration/update/export compositions, deriving exact file payloads and typed effects from a successful metadata save request. Duplicate order, zero handles, usage/miss export suppression and formatter counters are retained. Unit tests cover first-row update with two-row export and atomic unsupported-domain rejection. Native time/file/network execution remains supplied or captured. Focused `cargo xtask test-group rust-filter-duration-export` passes with deterministic report regeneration, relevant regressions and storage/documentation/whitespace checks. Full suite deferred to the tenth-commit boundary. Original assets unchanged; native IO effects remain captured.
docs/REVERSE_ENGINEERING_TODO.md CHANGED
@@ -1936,3 +1936,5 @@ RE-944–953 recover bounded time, expiry, loading, duration and persistence rou
1936
  - [x] RE-957 Rust remove-filter wrapper and source usage effects ([evidence](research/RUST_FILTER_REMOVE_COMMAND.md)).
1937
 
1938
  - [x] RE-958 Original duration command with metadata and export resolved ([evidence](research/QVM_FILTER_DURATION_EXPORT.md)).
 
 
 
1936
  - [x] RE-957 Rust remove-filter wrapper and source usage effects ([evidence](research/RUST_FILTER_REMOVE_COMMAND.md)).
1937
 
1938
  - [x] RE-958 Original duration command with metadata and export resolved ([evidence](research/QVM_FILTER_DURATION_EXPORT.md)).
1939
+
1940
+ - [x] RE-959 Rust duration command with resolved persistence plan ([evidence](research/RUST_FILTER_DURATION_EXPORT.md)).
docs/research/README.md CHANGED
@@ -1471,3 +1471,5 @@ The extension collector adds five focused tests for native tail calls, command-r
1471
  - [RE-957: Rust remove-filter wrapper and source usage effects](RUST_FILTER_REMOVE_COMMAND.md).
1472
 
1473
  - [RE-958: Original duration command with metadata and export resolved](QVM_FILTER_DURATION_EXPORT.md).
 
 
 
1471
  - [RE-957: Rust remove-filter wrapper and source usage effects](RUST_FILTER_REMOVE_COMMAND.md).
1472
 
1473
  - [RE-958: Original duration command with metadata and export resolved](QVM_FILTER_DURATION_EXPORT.md).
1474
+
1475
+ - [RE-959: Rust duration command with resolved persistence plan](RUST_FILTER_DURATION_EXPORT.md).
docs/research/RUST_FILTER_DURATION_EXPORT.md ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ # Rust duration command with resolved persistence plan
2
+
3
+ RE-959. Focused validation: `cargo xtask test-group rust-filter-duration-export`. [Machine report](rust-filter-duration-export.json).
4
+
5
+ Rust matches80 original duration/update/export compositions, deriving exact file payloads and typed effects from a successful metadata save request. Duplicate order, zero handles, usage/miss export suppression and formatter counters are retained. Unit tests cover first-row update with two-row export and atomic unsupported-domain rejection. Native time/file/network execution remains supplied or captured.
docs/research/rust-filter-duration-export.json ADDED
The diff for this file is too large to render. See raw diff
 
docs/research/rust-health-impulse.json CHANGED
@@ -2985,7 +2985,7 @@
2985
  "exit_code": 2
2986
  }
2987
  ],
2988
- "rust_unit_tests": 213,
2989
  "source_report": "docs/research/qvm-health-impulse.json",
2990
  "source_report_sha256": "625fd8064004ea6dbdf398d51d0af844d9c635ff04c0a0790886dddb6ab81e71"
2991
  }
 
2985
  "exit_code": 2
2986
  }
2987
  ],
2988
+ "rust_unit_tests": 215,
2989
  "source_report": "docs/research/qvm-health-impulse.json",
2990
  "source_report_sha256": "625fd8064004ea6dbdf398d51d0af844d9c635ff04c0a0790886dddb6ab81e71"
2991
  }
scripts/analyze_rust_filter_duration_export.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Compare recovered Rust policy with original QVM fixtures."""
2
+ import hashlib,json,os,subprocess
3
+ from pathlib import Path
4
+ from analyze_qvm import write_json
5
+ from rust_lifecycle_support import result
6
+
7
+ def main():
8
+ subprocess.run(['cargo','build','--offline','--package','urt-gameplay','--bin','urt-filter-commands'],check=True,capture_output=True)
9
+ binary=Path(os.environ.get('CARGO_TARGET_DIR','target'))/'debug'/'urt-filter-commands'
10
+ path=Path('docs/research/qvm-filter-duration-export.json');data=path.read_bytes();probes=[]
11
+ for p in json.loads(data)['probes']:
12
+ probes.append(dict(source={k:p[k] for k in ['events', 'updates', 'ring', 'rows', 'exported']},rust=result(binary,'duration-export',encode(p['input']))))
13
+ write_json(Path('docs/research/rust-filter-duration-export.json'),dict(source_report=str(path),source_report_sha256=hashlib.sha256(data).hexdigest(),probes=probes,limits='Finite source/Rust contract parity; native time/file/network replies remain supplied or captured.'))
14
+
15
+ def encode(a):
16
+ ip=bytes.fromhex(a['address']);d=bytes.fromhex(a['duration']);return [a['now'],a['client'],a['ring'],a['handle'],len(ip),*ip,len(d),*d]
17
+ if __name__=='__main__':main()
scripts/test_rust_filter_duration_export.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import hashlib,json,unittest
2
+ from pathlib import Path
3
+ class PortTests(unittest.TestCase):
4
+ def test_original_parity(self):
5
+ r=json.loads(Path('docs/research/rust-filter-duration-export.json').read_text());self.assertGreaterEqual(len(r['probes']),12)
6
+ for p in r['probes']:self.assertEqual(p['source'],p['rust'])
7
+ def test_identity(self):
8
+ r=json.loads(Path('docs/research/rust-filter-duration-export.json').read_text());self.assertEqual(hashlib.sha256(Path(r['source_report']).read_bytes()).hexdigest(),r['source_report_sha256'])
9
+ if __name__=='__main__':unittest.main()
xtask/checks.tsv CHANGED
@@ -3354,3 +3354,21 @@ report scripts/analyze_qvm_filter_duration_export.py docs/research/qvm-filter-du
3354
  edge docs/research/qvm-filter-duration-export.json scripts/test_qvm_filter_duration_export.py
3355
  group qvm-filter-duration-export scripts/analyze_qvm_filter_duration_export.py
3356
  edge scripts/analyze_qvm_filter_duration_export.py scripts/analyze_qvm_resumer_audit.py
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3354
  edge docs/research/qvm-filter-duration-export.json scripts/test_qvm_filter_duration_export.py
3355
  group qvm-filter-duration-export scripts/analyze_qvm_filter_duration_export.py
3356
  edge scripts/analyze_qvm_filter_duration_export.py scripts/analyze_qvm_resumer_audit.py
3357
+ report scripts/analyze_rust_filter_duration_export.py docs/research/rust-filter-duration-export.json
3358
+ edge docs/research/rust-filter-duration-export.json scripts/test_rust_filter_duration_export.py
3359
+ group rust-filter-duration-export scripts/analyze_rust_filter_duration_export.py
3360
+ edge scripts/analyze_rust_filter_duration_export.py scripts/analyze_qvm_resumer_audit.py
3361
+ edge Cargo.toml scripts/analyze_rust_filter_duration_export.py
3362
+ edge Cargo.lock scripts/analyze_rust_filter_duration_export.py
3363
+ edge crates/urt-gameplay/Cargo.toml scripts/analyze_rust_filter_duration_export.py
3364
+ edge crates/urt-gameplay/src/lib.rs scripts/analyze_rust_filter_duration_export.py
3365
+ edge crates/urt-gameplay/src/filter_loading.rs scripts/analyze_rust_filter_duration_export.py
3366
+ edge crates/urt-gameplay/src/filter_lifecycle.rs scripts/analyze_rust_filter_duration_export.py
3367
+ edge crates/urt-gameplay/src/native_policy.rs scripts/analyze_rust_filter_duration_export.py
3368
+ edge crates/urt-gameplay/src/bin/urt-filter-loading.rs scripts/analyze_rust_filter_duration_export.py
3369
+ edge crates/urt-gameplay/src/qvm_format.rs scripts/analyze_rust_filter_duration_export.py
3370
+ edge docs/research/qvm-filter-duration-export.json scripts/analyze_rust_filter_duration_export.py
3371
+ group rust-filter-duration-export scripts/analyze_rust_health_impulse.py
3372
+ group gameplay scripts/analyze_rust_filter_duration_export.py
3373
+ edge crates/urt-gameplay/src/filter_commands.rs scripts/analyze_rust_filter_duration_export.py
3374
+ edge crates/urt-gameplay/src/bin/urt-filter-commands.rs scripts/analyze_rust_filter_duration_export.py