simulo-interfaces 0.12.0__tar.gz → 0.14.0__tar.gz

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 (56) hide show
  1. {simulo_interfaces-0.12.0/src/simulo_interfaces.egg-info → simulo_interfaces-0.14.0}/PKG-INFO +1 -1
  2. {simulo_interfaces-0.12.0 → simulo_interfaces-0.14.0}/pyproject.toml +1 -1
  3. {simulo_interfaces-0.12.0 → simulo_interfaces-0.14.0}/src/simulo/interfaces/authoring/record.py +3 -3
  4. {simulo_interfaces-0.12.0 → simulo_interfaces-0.14.0}/src/simulo/interfaces/platform/__init__.py +116 -0
  5. {simulo_interfaces-0.12.0 → simulo_interfaces-0.14.0}/src/simulo/interfaces/platform/artifacts.py +201 -44
  6. {simulo_interfaces-0.12.0 → simulo_interfaces-0.14.0}/src/simulo/interfaces/platform/asset_catalog.py +57 -49
  7. {simulo_interfaces-0.12.0 → simulo_interfaces-0.14.0}/src/simulo/interfaces/platform/enums.py +3 -3
  8. simulo_interfaces-0.14.0/src/simulo/interfaces/platform/policy_bundle.py +2100 -0
  9. {simulo_interfaces-0.12.0 → simulo_interfaces-0.14.0}/src/simulo/interfaces/platform/runs.py +47 -6
  10. {simulo_interfaces-0.12.0 → simulo_interfaces-0.14.0}/src/simulo/interfaces/platform/submit.py +37 -4
  11. {simulo_interfaces-0.12.0 → simulo_interfaces-0.14.0/src/simulo_interfaces.egg-info}/PKG-INFO +1 -1
  12. {simulo_interfaces-0.12.0 → simulo_interfaces-0.14.0}/src/simulo_interfaces.egg-info/SOURCES.txt +1 -0
  13. {simulo_interfaces-0.12.0 → simulo_interfaces-0.14.0}/MANIFEST.in +0 -0
  14. {simulo_interfaces-0.12.0 → simulo_interfaces-0.14.0}/PYPI.md +0 -0
  15. {simulo_interfaces-0.12.0 → simulo_interfaces-0.14.0}/setup.cfg +0 -0
  16. {simulo_interfaces-0.12.0 → simulo_interfaces-0.14.0}/src/simulo/interfaces/__init__.py +0 -0
  17. {simulo_interfaces-0.12.0 → simulo_interfaces-0.14.0}/src/simulo/interfaces/authoring/__init__.py +0 -0
  18. {simulo_interfaces-0.12.0 → simulo_interfaces-0.14.0}/src/simulo/interfaces/authoring/actuator.py +0 -0
  19. {simulo_interfaces-0.12.0 → simulo_interfaces-0.14.0}/src/simulo/interfaces/authoring/asset.py +0 -0
  20. {simulo_interfaces-0.12.0 → simulo_interfaces-0.14.0}/src/simulo/interfaces/authoring/entity.py +0 -0
  21. {simulo_interfaces-0.12.0 → simulo_interfaces-0.14.0}/src/simulo/interfaces/authoring/light.py +0 -0
  22. {simulo_interfaces-0.12.0 → simulo_interfaces-0.14.0}/src/simulo/interfaces/authoring/materials.py +0 -0
  23. {simulo_interfaces-0.12.0 → simulo_interfaces-0.14.0}/src/simulo/interfaces/authoring/physics.py +0 -0
  24. {simulo_interfaces-0.12.0 → simulo_interfaces-0.14.0}/src/simulo/interfaces/authoring/pose.py +0 -0
  25. {simulo_interfaces-0.12.0 → simulo_interfaces-0.14.0}/src/simulo/interfaces/authoring/primitives.py +0 -0
  26. {simulo_interfaces-0.12.0 → simulo_interfaces-0.14.0}/src/simulo/interfaces/authoring/prop.py +0 -0
  27. {simulo_interfaces-0.12.0 → simulo_interfaces-0.14.0}/src/simulo/interfaces/authoring/robot.py +0 -0
  28. {simulo_interfaces-0.12.0 → simulo_interfaces-0.14.0}/src/simulo/interfaces/authoring/scene.py +0 -0
  29. {simulo_interfaces-0.12.0 → simulo_interfaces-0.14.0}/src/simulo/interfaces/authoring/sensor.py +0 -0
  30. {simulo_interfaces-0.12.0 → simulo_interfaces-0.14.0}/src/simulo/interfaces/authoring/terrain.py +0 -0
  31. {simulo_interfaces-0.12.0 → simulo_interfaces-0.14.0}/src/simulo/interfaces/authoring/visual.py +0 -0
  32. {simulo_interfaces-0.12.0 → simulo_interfaces-0.14.0}/src/simulo/interfaces/authoring/world.py +0 -0
  33. {simulo_interfaces-0.12.0 → simulo_interfaces-0.14.0}/src/simulo/interfaces/exceptions.py +0 -0
  34. {simulo_interfaces-0.12.0 → simulo_interfaces-0.14.0}/src/simulo/interfaces/ids.py +0 -0
  35. {simulo_interfaces-0.12.0 → simulo_interfaces-0.14.0}/src/simulo/interfaces/platform/app.py +0 -0
  36. {simulo_interfaces-0.12.0 → simulo_interfaces-0.14.0}/src/simulo/interfaces/platform/asset.py +0 -0
  37. {simulo_interfaces-0.12.0 → simulo_interfaces-0.14.0}/src/simulo/interfaces/platform/callbacks.py +0 -0
  38. {simulo_interfaces-0.12.0 → simulo_interfaces-0.14.0}/src/simulo/interfaces/platform/debug.py +0 -0
  39. {simulo_interfaces-0.12.0 → simulo_interfaces-0.14.0}/src/simulo/interfaces/platform/domain.py +0 -0
  40. {simulo_interfaces-0.12.0 → simulo_interfaces-0.14.0}/src/simulo/interfaces/platform/manifest.py +0 -0
  41. {simulo_interfaces-0.12.0 → simulo_interfaces-0.14.0}/src/simulo/interfaces/platform/runtime.py +0 -0
  42. {simulo_interfaces-0.12.0 → simulo_interfaces-0.14.0}/src/simulo/interfaces/platform/volume.py +0 -0
  43. {simulo_interfaces-0.12.0 → simulo_interfaces-0.14.0}/src/simulo/interfaces/py.typed +0 -0
  44. {simulo_interfaces-0.12.0 → simulo_interfaces-0.14.0}/src/simulo/interfaces/runtime/__init__.py +0 -0
  45. {simulo_interfaces-0.12.0 → simulo_interfaces-0.14.0}/src/simulo/interfaces/runtime/anomaly.py +0 -0
  46. {simulo_interfaces-0.12.0 → simulo_interfaces-0.14.0}/src/simulo/interfaces/runtime/components.py +0 -0
  47. {simulo_interfaces-0.12.0 → simulo_interfaces-0.14.0}/src/simulo/interfaces/runtime/env.py +0 -0
  48. {simulo_interfaces-0.12.0 → simulo_interfaces-0.14.0}/src/simulo/interfaces/runtime/player.py +0 -0
  49. {simulo_interfaces-0.12.0 → simulo_interfaces-0.14.0}/src/simulo/interfaces/runtime/policy.py +0 -0
  50. {simulo_interfaces-0.12.0 → simulo_interfaces-0.14.0}/src/simulo/interfaces/runtime/scenario.py +0 -0
  51. {simulo_interfaces-0.12.0 → simulo_interfaces-0.14.0}/src/simulo/interfaces/runtime/task.py +0 -0
  52. {simulo_interfaces-0.12.0 → simulo_interfaces-0.14.0}/src/simulo/interfaces/runtime/tensors.py +0 -0
  53. {simulo_interfaces-0.12.0 → simulo_interfaces-0.14.0}/src/simulo/interfaces/runtime/trainer.py +0 -0
  54. {simulo_interfaces-0.12.0 → simulo_interfaces-0.14.0}/src/simulo_interfaces.egg-info/dependency_links.txt +0 -0
  55. {simulo_interfaces-0.12.0 → simulo_interfaces-0.14.0}/src/simulo_interfaces.egg-info/requires.txt +0 -0
  56. {simulo_interfaces-0.12.0 → simulo_interfaces-0.14.0}/src/simulo_interfaces.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simulo-interfaces
3
- Version: 0.12.0
3
+ Version: 0.14.0
4
4
  Summary: Type contracts for the Simulo SDK. Installed automatically as a dependency of the simulo package.
5
5
  Author-email: Simulo Team <team@simulo.ai>
6
6
  License: BSD-3-Clause
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "simulo-interfaces"
7
- version = "0.12.0"
7
+ version = "0.14.0"
8
8
  description = "Type contracts for the Simulo SDK. Installed automatically as a dependency of the simulo package."
9
9
  readme = "PYPI.md"
10
10
  requires-python = ">=3.11"
@@ -88,10 +88,10 @@ ProfileName = Literal["core", "standard", "full"]
88
88
  VALID_PROFILES: Tuple[str, ...] = ("core", "standard", "full")
89
89
 
90
90
  # Profile defaults table. Each profile lists ONLY the fields it turns on; any
91
- # field absent from a profile's dict defaults to False. SRD §10.1 mandates
91
+ # field absent from a profile's dict defaults to False. MCAP recording mandates
92
92
  # Phase 1 robot/contact/sensor/video stay off; the `full` row below holds the
93
93
  # line by not enabling those streams in Phase 1. Wave 2 may flip them on
94
- # (plan §6 Wave 2 task F) — that edit is intentionally scoped to this table.
94
+ # when enabled — that edit is intentionally scoped to this table.
95
95
  #
96
96
  # Wave Policy-Eval-Completeness (2026-06-21) — three changes to defaults:
97
97
  #
@@ -161,7 +161,7 @@ _PROFILE_DEFAULTS: Dict[str, Dict[str, bool]] = {
161
161
  "include_terminations": True,
162
162
  "include_events": True,
163
163
  # Wave 2 — Phase 2 streams enabled in the `full` profile per plan
164
- # §6 Wave 2 task F + SRD §7 line 409. Phase 3 streams (video, depth,
164
+ # The full profile includes Phase 2 streams. Phase 3 streams (video, depth,
165
165
  # lidar) and `include_collisions` stay opt-in even under `full` to
166
166
  # avoid surprising users with high-volume captures.
167
167
  "include_robot_state": True,
@@ -16,7 +16,10 @@ from simulo.interfaces.platform.artifacts import (
16
16
  MAX_OUTPUT_FLAG_CODE_CHARS,
17
17
  MAX_OUTPUT_FLAG_DETAIL_CHARS,
18
18
  MAX_OUTPUTS_BYTES,
19
+ OUTPUT_DROPPED_DETAILS,
19
20
  OUTPUT_FLAG_ANOMALY,
21
+ OUTPUT_FLAG_OUTPUT_DROPPED,
22
+ OUTPUTS_UNDELIVERED_CODE,
20
23
  WORKER_JOB_ARTIFACTS_PRESIGN_ROUTE_TEMPLATE,
21
24
  WORKER_JOB_ARTIFACTS_ROUTE_TEMPLATE,
22
25
  Artifact,
@@ -109,6 +112,60 @@ from simulo.interfaces.platform.manifest import (
109
112
  PreflightResponse,
110
113
  RuntimeManifest,
111
114
  )
115
+ from simulo.interfaces.platform.policy_bundle import (
116
+ EXPORT_VALIDATION_JOB_TYPE,
117
+ JOB_EXPORTS_ROUTE_TEMPLATE,
118
+ MAX_POLICY_BUNDLE_UNCOMPRESSED_BYTES,
119
+ MAX_POLICY_NAME_TOKEN_CHARS,
120
+ POLICY_BUNDLE_ACTION_SCHEMA_FILE,
121
+ POLICY_BUNDLE_ARCHIVE_NAME_TEMPLATE,
122
+ POLICY_BUNDLE_ARTIFACT_KIND,
123
+ POLICY_BUNDLE_FILES,
124
+ POLICY_BUNDLE_MANIFEST_FILE,
125
+ POLICY_BUNDLE_MODEL_ONNX_FILE,
126
+ POLICY_BUNDLE_MODEL_PT_FILE,
127
+ POLICY_BUNDLE_NORMALIZATION_FILE,
128
+ POLICY_BUNDLE_OBSERVATION_SCHEMA_FILE,
129
+ POLICY_BUNDLE_OPTIONAL_FILES,
130
+ POLICY_BUNDLE_README_FILE,
131
+ POLICY_BUNDLE_REQUIRED_FILES,
132
+ POLICY_BUNDLE_REQUIREMENTS_FILE,
133
+ POLICY_BUNDLE_ROOT_DIR_TEMPLATE,
134
+ POLICY_BUNDLE_SCHEMA_VERSION,
135
+ POLICY_BUNDLE_TEST_VECTORS_FILE,
136
+ POLICY_BUNDLE_VERIFY_FILE,
137
+ POLICY_EQUIVALENCE_ABS_TOLERANCE,
138
+ POLICY_EQUIVALENCE_REL_TOLERANCE,
139
+ POLICY_GOLDEN_VECTOR_COUNT,
140
+ POLICY_NAME_TOKEN_FALLBACK,
141
+ POLICY_TENSOR_DTYPES,
142
+ POLICY_TEST_VECTOR_EXPECTED_OUTPUTS_KEY,
143
+ POLICY_TEST_VECTOR_INPUTS_KEY,
144
+ SUPPORTED_POLICY_BUNDLE_SCHEMA_MAJORS,
145
+ UNKNOWN,
146
+ PolicyActionComponent,
147
+ PolicyActionSchema,
148
+ PolicyBounds,
149
+ PolicyBundleManifest,
150
+ PolicyEquivalenceSpec,
151
+ PolicyExportFailureReason,
152
+ PolicyExportFormat,
153
+ PolicyExportValidation,
154
+ PolicyExportVerdict,
155
+ PolicyHistorySpec,
156
+ PolicyNormalization,
157
+ PolicyObservationComponent,
158
+ PolicyObservationSchema,
159
+ PolicySourceSpec,
160
+ PolicyTensorSpec,
161
+ Unknown,
162
+ is_supported_policy_bundle_schema,
163
+ policy_bundle_archive_name,
164
+ policy_bundle_root_dir,
165
+ policy_bundle_schema_major,
166
+ policy_name_token,
167
+ validate_policy_bundle,
168
+ )
112
169
  from simulo.interfaces.platform.runs import (
113
170
  JOB_CANCEL_ROUTE_TEMPLATE,
114
171
  JOB_LIST_SCOPES,
@@ -201,6 +258,9 @@ __all__ = [
201
258
  "MAX_OUTPUT_FLAG_CODE_CHARS",
202
259
  "MAX_OUTPUT_FLAG_DETAIL_CHARS",
203
260
  "OUTPUT_FLAG_ANOMALY",
261
+ "OUTPUT_FLAG_OUTPUT_DROPPED",
262
+ "OUTPUT_DROPPED_DETAILS",
263
+ "OUTPUTS_UNDELIVERED_CODE",
204
264
  "DECLARED_OUTPUT_MISSING_CODE",
205
265
  # job-run contract (run record schema + jobs HTTP API shape)
206
266
  "TERMINAL_JOB_STATUSES",
@@ -322,6 +382,62 @@ __all__ = [
322
382
  "ConvertedArchive",
323
383
  "AssetValidationFinalize",
324
384
  "AssetPublishOptions",
385
+ # portable policy export bundle contract (issue #654) — schema version,
386
+ # bundle layout, naming rule, export routes/job type, equivalence
387
+ # defaults, enums, explicit-unknown marker, manifest dataclasses, the
388
+ # export-validation result, and the cross-file coherence check
389
+ "POLICY_BUNDLE_SCHEMA_VERSION",
390
+ "SUPPORTED_POLICY_BUNDLE_SCHEMA_MAJORS",
391
+ "policy_bundle_schema_major",
392
+ "is_supported_policy_bundle_schema",
393
+ "POLICY_BUNDLE_MODEL_ONNX_FILE",
394
+ "POLICY_BUNDLE_MODEL_PT_FILE",
395
+ "POLICY_BUNDLE_MANIFEST_FILE",
396
+ "POLICY_BUNDLE_OBSERVATION_SCHEMA_FILE",
397
+ "POLICY_BUNDLE_ACTION_SCHEMA_FILE",
398
+ "POLICY_BUNDLE_NORMALIZATION_FILE",
399
+ "POLICY_BUNDLE_TEST_VECTORS_FILE",
400
+ "POLICY_BUNDLE_VERIFY_FILE",
401
+ "POLICY_BUNDLE_REQUIREMENTS_FILE",
402
+ "POLICY_BUNDLE_README_FILE",
403
+ "POLICY_BUNDLE_FILES",
404
+ "POLICY_BUNDLE_REQUIRED_FILES",
405
+ "POLICY_BUNDLE_OPTIONAL_FILES",
406
+ "MAX_POLICY_BUNDLE_UNCOMPRESSED_BYTES",
407
+ "POLICY_TEST_VECTOR_INPUTS_KEY",
408
+ "POLICY_TEST_VECTOR_EXPECTED_OUTPUTS_KEY",
409
+ "POLICY_BUNDLE_ARTIFACT_KIND",
410
+ "POLICY_BUNDLE_ROOT_DIR_TEMPLATE",
411
+ "POLICY_BUNDLE_ARCHIVE_NAME_TEMPLATE",
412
+ "MAX_POLICY_NAME_TOKEN_CHARS",
413
+ "POLICY_NAME_TOKEN_FALLBACK",
414
+ "policy_name_token",
415
+ "policy_bundle_root_dir",
416
+ "policy_bundle_archive_name",
417
+ "JOB_EXPORTS_ROUTE_TEMPLATE",
418
+ "EXPORT_VALIDATION_JOB_TYPE",
419
+ "POLICY_EQUIVALENCE_ABS_TOLERANCE",
420
+ "POLICY_EQUIVALENCE_REL_TOLERANCE",
421
+ "POLICY_GOLDEN_VECTOR_COUNT",
422
+ "POLICY_TENSOR_DTYPES",
423
+ "PolicyExportFormat",
424
+ "PolicyExportVerdict",
425
+ "PolicyExportFailureReason",
426
+ "PolicyExportValidation",
427
+ "Unknown",
428
+ "UNKNOWN",
429
+ "PolicyTensorSpec",
430
+ "PolicyBounds",
431
+ "PolicyHistorySpec",
432
+ "PolicySourceSpec",
433
+ "PolicyEquivalenceSpec",
434
+ "PolicyObservationComponent",
435
+ "PolicyActionComponent",
436
+ "PolicyBundleManifest",
437
+ "PolicyObservationSchema",
438
+ "PolicyActionSchema",
439
+ "PolicyNormalization",
440
+ "validate_policy_bundle",
325
441
  # protocols
326
442
  "RuntimeProtocol",
327
443
  "AssetProtocol",
@@ -118,6 +118,29 @@ migration). Instead ``JobRecord.outputs`` carries an optional
118
118
  even if the transition loses a race) and partial recordings are dropped —
119
119
  cancel-uploaded checkpoints are intentionally **unsummarized**. A decision,
120
120
  not an oversight.
121
+ * **A registered artifact reaches the general job-artifacts road on the
122
+ ``completed`` path only** — never on cancel, timeout, or failure. Also a
123
+ decision, and the reason it is not the worker's to reverse: the plan IS the
124
+ drained registration set, and the registry is drained **after the job body
125
+ returns** (``runner``'s ``--outputs-out`` write is on its success path only,
126
+ mirroring ``--result-out``). A cancelled, timed-out or crashed run never
127
+ reaches that drain, so no plan file exists and there is nothing to upload;
128
+ the worker's ``status == completed`` gate makes that explicit and
129
+ fail-closed rather than reading whatever sidecar happens to be on disk from
130
+ an earlier attempt. The two older roads look asymmetric here because they
131
+ are **sniffed** rather than planned — ``_gather_recordings`` /
132
+ ``_gather_models`` scan the run directory, so they still find (and, for
133
+ checkpoints, still upload) files a failed run left behind. This road has no
134
+ sniffer by construction.
135
+
136
+ The user-visible consequence, stated rather than buried: a ``report`` or
137
+ ``log`` that a FAILING run registers to explain itself is **not delivered**.
138
+ That is the price of "a partial file is not an honest artifact"; the file
139
+ stays on the worker's disk. The worker deliberately emits no warning for it
140
+ either — with no plan file it cannot distinguish "registered nothing" from
141
+ "registered, never drained", so the only available warning is one that would
142
+ fire on every failing job. A job that must hand back diagnostics on failure
143
+ should write them **before** the work that can fail and return normally.
121
144
 
122
145
  The ``/artifacts`` projection is a read-time union, not a home
123
146
  --------------------------------------------------------------
@@ -133,9 +156,12 @@ and the narrow ``job_artifacts`` table (everything registered via
133
156
  appears in the union **exactly once**. The projection reuses the
134
157
  platform-standard paginated envelope (``items``/``total``/``page``/``limit``/
135
158
  ``pages``; default ``limit=20``, max 100, out-of-range → 422) and the standard
136
- error shape.
159
+ error shape — see :mod:`simulo.interfaces.platform.runs` for the full
160
+ contract, including the ``pages >= 1`` invariant that holds even for
161
+ ``total == 0``.
137
162
  """
138
163
 
164
+ import re
139
165
  from collections.abc import Mapping
140
166
  from dataclasses import dataclass, field
141
167
 
@@ -282,6 +308,74 @@ MAX_OUTPUT_FLAG_DETAIL_CHARS = 256
282
308
  #: comes from the persisted-rows projection, never from the flag).
283
309
  OUTPUT_FLAG_ANOMALY = "anomaly"
284
310
 
311
+ #: :attr:`OutputFlag.code` announcing that at least one output the run
312
+ #: REGISTERED never became an artifact — the uploader refused it. ``detail``
313
+ #: carries the refusal reason, one of :data:`OUTPUT_DROPPED_DETAILS`, and the
314
+ #: producer emits ONE flag per DISTINCT reason (the :data:`OUTPUT_FLAG_ANOMALY`
315
+ #: rule: a flag states a property of the run, never an inventory of files).
316
+ #: The worker emits it; the CLI renders it.
317
+ #:
318
+ #: WHY IT EXISTS. A registration the platform refuses is a defect in the JOB —
319
+ #: it named a path the platform will never accept — not a transient condition.
320
+ #: Every other trace of it is unreachable: the refusal is written to the
321
+ #: WORKER's own stderr, while the log the platform uploads is the CHILD's.
322
+ #: Without this flag such a run is reported a plain ``completed`` having
323
+ #: delivered less than it claimed, with nothing anywhere for a user to see.
324
+ #:
325
+ #: SCOPE. This flag rides the ``complete`` body, which the control plane keeps
326
+ #: for a ``completed`` job only. It therefore cannot describe a run that
327
+ #: delivered NOTHING AT ALL — that run is not ``completed``, it is terminal
328
+ #: :data:`OUTPUTS_UNDELIVERED_CODE`. The two signals are mutually exclusive by
329
+ #: construction, and between them every refused output is reported.
330
+ OUTPUT_FLAG_OUTPUT_DROPPED = "output_dropped"
331
+
332
+ #: The closed ``detail`` vocabulary of :data:`OUTPUT_FLAG_OUTPUT_DROPPED` — why
333
+ #: one registered output was refused:
334
+ #:
335
+ #: * ``malformed`` — the registration's ``name``/``path`` was not a string;
336
+ #: * ``unresolvable`` — the path could not be resolved on the worker;
337
+ #: * ``outside_roots`` — the resolved path lies outside every directory the
338
+ #: platform points a job's writes at. Uploading it anyway would let a
339
+ #: registration name any file the worker can read, so the refusal is the
340
+ #: guard working, and the flag is how the run says so;
341
+ #: * ``no_pipeline`` — the declared kind has no artifact home to land in (a
342
+ #: checkpoint whose filename carries no role, or a kind outside
343
+ #: :class:`~simulo.interfaces.platform.enums.ArtifactKind`).
344
+ OUTPUT_DROPPED_DETAILS: tuple[str, ...] = (
345
+ "malformed",
346
+ "unresolvable",
347
+ "outside_roots",
348
+ "no_pipeline",
349
+ )
350
+
351
+ #: Terminal ``status_reason`` of a run whose body exited 0 but which delivered
352
+ #: NO artifact at all: every output it registered was refused (see
353
+ #: :data:`OUTPUT_DROPPED_DETAILS`) and it produced no recording and no
354
+ #: checkpoint either. Emitted by the worker, rendered by the CLI.
355
+ #:
356
+ #: Such a run is reported ``failed``, not ``completed``. Green-with-nothing is
357
+ #: the worse of the two outcomes: a job whose whole purpose is one file, shown
358
+ #: successful beside an empty artifact list, gives nobody a reason to look —
359
+ #: whereas a job marked failed still has its log, and the run cost the same
360
+ #: either way.
361
+ #:
362
+ #: The condition is deliberately "delivered nothing", not "every registered
363
+ #: output was refused". A run whose checkpoints or recordings landed DID
364
+ #: deliver something, and stays ``completed`` carrying
365
+ #: :data:`OUTPUT_FLAG_OUTPUT_DROPPED` for what it lost — failing it would throw
366
+ #: away hours of real training over one mis-registered path.
367
+ #:
368
+ #: THE RESULT SURVIVES. ``GET /v1/jobs/{id}/result`` serves the result of a
369
+ #: run reported under this code, exactly as it does for a completed run — the
370
+ #: one status other than ``completed`` for which a result is stored at all.
371
+ #: The same argument that keeps a run with surviving checkpoints ``completed``
372
+ #: applies to the dict the body returned: this run's code RAN, its metrics are
373
+ #: as real as any completed run's, and only its FILES were refused. Discarding
374
+ #: them too would destroy work the refusal never touched. The typed
375
+ #: ``outputs`` summary is NOT preserved the same way — it describes the
376
+ #: artifacts, which is precisely what this code already says.
377
+ OUTPUTS_UNDELIVERED_CODE = "outputs_undelivered"
378
+
285
379
  #: Terminal ``status_reason`` code of a job whose declared **non-optional**
286
380
  #: output was missing after the run body succeeded (the runner's validation —
287
381
  #: see the module docstring). Emitted by the runner, rendered by the CLI.
@@ -365,6 +459,88 @@ def _validate_artifact_name(owner: str, name: str) -> None:
365
459
  raise ValueError(f"{owner} must not be a dots-only relative-path token, got {_truncated(name)}.")
366
460
 
367
461
 
462
+ #: The SERVER's finalize-time filename rule, mirrored — the second, STRICTER
463
+ #: gate an artifact ``name`` must clear before its bytes may be uploaded.
464
+ #:
465
+ #: Canonical implementation: ``simulo_control_plane.storage.base._KEY_SEGMENT_RE``,
466
+ #: applied to ``ArtifactFinalizeRequest.name`` (and ``ModelFinalizeRequest.name``)
467
+ #: through ``storage.base.is_safe_filename_segment`` — anything else is a 422.
468
+ #: Mirrored here rather than imported for the reason ``jobs/constants.py``
469
+ #: records at length: the control-plane build is self-contained and this
470
+ #: contract package is NOT installed in its deploy image, so the dependency
471
+ #: cannot run in that direction.
472
+ #:
473
+ #: **Why a mirror is worth carrying.** The upload order is presign → PUT →
474
+ #: finalize, so a name that clears :func:`_validate_artifact_name` and then
475
+ #: fails the server's rule has ALREADY written bytes to object storage when the
476
+ #: 422 arrives: the artifact is lost, the object is permanently unreferenced
477
+ #: (no row ever names it), and a job may repeat that up to
478
+ #: ``simulo.outputs.MAX_REGISTERED_OUTPUTS`` times at unbounded size. Measured
479
+ #: divergences — names :func:`_validate_artifact_name` ACCEPTS and this rule
480
+ #: rejects: ``"my report.html"`` (space), ``"résumé.pdf"`` (non-ASCII),
481
+ #: ``"eval (1).json"`` (parentheses), ``".hidden"`` (leading dot).
482
+ #:
483
+ #: Those are ordinary things to call a file, and the reachable path is the
484
+ #: PUBLIC one: ``simulo.save_artifact("my report.html", path, kind="report")``
485
+ #: passes the constructor and then orphans its bytes. A worker that checks
486
+ #: BEFORE the presign turns that into an honest, named refusal on the warning
487
+ #: channel instead of an unreferenced object.
488
+ #:
489
+ #: NOT affected, measured rather than assumed: the policy-export bundle name.
490
+ #: ``policy_bundle.policy_name_token`` reduces a job title to ``[a-z0-9._-]``
491
+ #: with a ``[a-z0-9]`` first character — deliberately, citing this very gate —
492
+ #: so ``résumé-arm`` composes ``r-sum-arm-policy-best.zip``, which this rule
493
+ #: ACCEPTS. That is a second, independent guard on one caller; this one is the
494
+ #: general gate every other registered name needs.
495
+ #:
496
+ #: The drift guard is ``tests/integration/test_contract_mirror.py`` — the one
497
+ #: environment holding both packages. It compares this pattern against the
498
+ #: control plane's real compiled regex AND runs both implementations over a
499
+ #: table of names, so a future divergence goes red there rather than orphaning
500
+ #: objects in production.
501
+ SAFE_FILENAME_SEGMENT_PATTERN = r"^[A-Za-z0-9][A-Za-z0-9._-]*\Z"
502
+ _SAFE_FILENAME_SEGMENT_RE = re.compile(SAFE_FILENAME_SEGMENT_PATTERN)
503
+
504
+
505
+ def is_safe_filename_segment(value: str) -> bool:
506
+ """``True`` when ``value`` is a name the control plane's finalize accepts.
507
+
508
+ Deliberately the same NAME as the control plane's own helper
509
+ (``storage.base.is_safe_filename_segment``), so the two read as the one
510
+ rule they are. Anchored with ``\\Z`` and not ``$`` for the reason recorded
511
+ on the server's copy: Python's ``$`` also matches just before a trailing
512
+ newline, so ``"rollout.mcap\\n"`` would wrongly pass.
513
+
514
+ NOT a replacement for :func:`_validate_artifact_name`, which stays the
515
+ contract's own construction-time rule (cross-platform separators, control
516
+ characters, dots-only tokens, the UTF-8 byte ceiling, and the type check).
517
+ This is the narrower *server-acceptance* question, asked by an uploader
518
+ before it spends bytes.
519
+ """
520
+ return bool(value) and bool(_SAFE_FILENAME_SEGMENT_RE.match(value))
521
+
522
+
523
+ def _validate_finalizable_artifact_name(owner: str, name: str) -> None:
524
+ """Reject any ``name`` the server's finalize would answer 422 for.
525
+
526
+ Both gates, in the order an uploader must apply them: the contract's own
527
+ :func:`_validate_artifact_name` first (it owns the type check and the
528
+ length-before-echo discipline, so an over-length or non-``str`` value is
529
+ reported by its size/type and never reproduced), then
530
+ :func:`is_safe_filename_segment`.
531
+
532
+ Every rejection is a ``ValueError`` whose message names the offending value
533
+ (clamped by :func:`_truncated` — an uploader's message is worker-log
534
+ content and must never amplify its own input) and says which rule it broke.
535
+ """
536
+ _validate_artifact_name(owner, name)
537
+ if not is_safe_filename_segment(name):
538
+ raise ValueError(
539
+ f"{owner} must be a single safe filename segment the control plane's finalize will accept "
540
+ f"(letters, digits, '.', '_', '-'; must start with a letter or digit), got {_truncated(name)}."
541
+ )
542
+
543
+
368
544
  def _validate_artifact_path(owner: str, path: str) -> None:
369
545
  """Reject any ``path`` that could not name a real file for any consumer.
370
546
 
@@ -516,54 +692,33 @@ class ArtifactDeclaration:
516
692
 
517
693
  @dataclass(frozen=True, slots=True, kw_only=True)
518
694
  class Artifact:
519
- """One registered job output — the execution-time record of a real file.
520
-
521
- This is the type ``simulo.save_artifact(name, path)`` registers (tier 2)
522
- and the type tier-0 producers register as they write files (the anomaly
523
- monitor registers each capture session with its actual generated
524
- filename — PR-B4). A registration always names a file the run really
525
- produced at a path that really exists, which is exactly what
526
- distinguishes it from :class:`ArtifactDeclaration` (the pre-run shape,
527
- no path field): the drained registration set is the plan side of the
528
- worker's ``plan ∪ sniff`` union, keyed by ``Path.resolve()`` of
529
- :attr:`path` — declarations never enter that union. It is an authoring
530
- value, not a wire row — it validates at construction so mistakes surface
531
- on the author's machine (the
532
- :class:`~simulo.interfaces.runtime.anomaly.DebugOnAnomaly` precedent);
533
- the persisted/served row is :class:`ArtifactRecord`.
695
+ """A file registration created while a job is running.
696
+
697
+ ``simulo.save_artifact(name, path)`` builds this value and validates it
698
+ before adding it to the job's local output registry. It describes a real
699
+ file at a real path, but it is not confirmation that the file was uploaded.
700
+ The current managed platform does not publish general registered files for
701
+ download. Use a job result for small JSON values, a named volume for files
702
+ another job needs, or the supported model and recording workflows for
703
+ downloadable output.
534
704
  """
535
705
 
536
706
  name: str
537
- """Logical name — the single filename the platform stores, lists, and
538
- saves downloads under. Never a path: separators (including the Windows
539
- drive ``:``), control characters, dots-only and whitespace-only tokens,
540
- empty and over-:data:`MAX_ARTIFACT_NAME_BYTES` names are rejected at
541
- construction (a traversal-shaped name would otherwise become the
542
- filename a client writes a download to — see
543
- :func:`_validate_artifact_name` for the full rule and the download-site
544
- obligation)."""
707
+ """Logical filename for the registration. It must be one filename, never
708
+ a path. Separators, control characters, dots-only and whitespace-only
709
+ values, empty values, and names over
710
+ :data:`MAX_ARTIFACT_NAME_BYTES` are rejected."""
545
711
 
546
712
  path: str
547
- """Filesystem path of the produced file in the job's working tree at
548
- execution time. The worker's ``plan ∪ sniff`` de-duplication key is this
549
- path **resolved** (``Path.resolve()``), the plan's kind winning.
550
-
551
- Validated at construction for TYPE (a ``str``), LENGTH
552
- (≤ :data:`MAX_ARTIFACT_PATH_BYTES`), non-emptiness, and absence of a NUL
553
- byte — the four things that make a value unusable as a path *by any
554
- consumer*, checked here so the consumer does not discover them mid-upload.
555
- Deliberately NOT validated for shape or location: a path is legitimately
556
- absolute and may point anywhere the job can write, so *where* it points is
557
- contained at the uploader (see the ``simulo.outputs`` module warning), not
558
- here."""
713
+ """Filesystem path of the produced file while the job is running.
714
+ Construction checks that it is a non-empty, NUL-free string within
715
+ :data:`MAX_ARTIFACT_PATH_BYTES`. The path may be absolute or relative."""
559
716
 
560
717
  kind: ArtifactKind = ArtifactKind.FILE
561
- """What this output is (see :class:`~simulo.interfaces.platform.enums.ArtifactKind`).
562
- ``save_artifact`` registers :attr:`~simulo.interfaces.platform.enums.ArtifactKind.FILE`
563
- by default; tier-0 producers stamp the callback's implied kind when they
564
- register (the anomaly monitor stamps ``anomaly_capture``). A plain wire
565
- string is normalized to the enum at construction; an unknown kind string
566
- is a ``ValueError`` on the author's machine."""
718
+ """What kind of file this is. The default is
719
+ :attr:`~simulo.interfaces.platform.enums.ArtifactKind.FILE`. You may pass
720
+ an ``ArtifactKind`` value or its string form; unknown values raise
721
+ ``ValueError``."""
567
722
 
568
723
  def __post_init__(self) -> None:
569
724
  object.__setattr__(self, "kind", ArtifactKind(self.kind))
@@ -621,8 +776,10 @@ class ArtifactRecord:
621
776
  (a checkpoint is NOT a trained model — Rule #8, stated in the
622
777
  :mod:`~simulo.interfaces.platform.domain` module docstring and on its
623
778
  ``Checkpoint``/``TrainedModel`` types).
624
- ``trained_model`` has no producer today and is only ever stamped
625
- explicitly by a future exporter, never inferred from a ``models`` row;
779
+ ``trained_model`` DOES have a producer — the worker's general
780
+ job-artifacts road (``worker._upload_job_artifact``), whose rows live in
781
+ ``job_artifacts`` — and it is only ever stamped explicitly, by the job
782
+ that registers the bundle, never inferred from a ``models`` row;
626
783
  * rows projected from ``job_recordings`` map a legacy NULL kind to
627
784
  :attr:`~simulo.interfaces.platform.enums.ArtifactKind.RECORDING` —
628
785
  never to ``replay_output`` (see the enum docstrings)."""