Files
vdm/core/docs/proto-requests-m1.md
T
samiandClaude Sonnet 5 910ce4a638 core: add request docs for PROTO and PKG before the contract freeze
proto-requests-m1.md: freeze-blockers (error.code wire enum with the full
CORE failure taxonomy, TaskSummary.segments meaning, Segment field names)
separated from cheap follow-ups (decision event, credential return path,
checksum pattern); state-machine ownership split flagged as three-way ADR
material. pkg-requests-m1.md: uncomment add_subdirectory(core), pick a test
framework, guard VELOX_BUILD_FUZZ on Clang.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01HPPSGhiArbvQgwC2DNiURS
2026-09-09 19:02:53 +04:00

8.4 KiB

CORE → PROTO — requests before the VERSION freeze

Status: open. Raised by lane CORE against contracts/ at 1.0.0-draft. This is a request list, not a decision record. PROTO owns every item here. Once PROTO answers D1, that one becomes a three-way ADR (docs/adr/) per contracts/README.md rule 4.

Ranking is by contracts/README.md rule 4, not by intuition: new method or new optional field = minor, can land anytime; rename / remove / retype / default change = major + ADR.


Freeze-blockers — get these into 1.0.0 (major if we guess wrong)

B1. A frozen wire enum for TaskSummary.error.code

TaskSummary.error.code is currently a bare integer. CORE cannot import a protocol header (layering rule — core/ never sees JSON/RPC), so CORE has its own failure taxonomy regardless of what PROTO picks; DAEMON owns the projection from one to the other. The ask is not "who owns the enum" — it is: freeze a wire enum with enough cardinality that DAEMON's mapping from the CORE taxonomy below is lossless.

If the wire enum is coarser than this table, DAEMON has to collapse distinct engine failures into one code and the GUI can't tell "server file changed" from "checksum mismatch". Retyping error.code later is a major bump.

CORE failure taxonomy (this is vdm::Error in core/include/vdm/util/error.hpp):

CORE Error Meaning retryable carries httpStatus
canceled user/daemon cancelled no no
resolve_failed DNS resolution failed yes no
connect_failed TCP connect failed yes no
tls_failed TLS handshake / cert rejected no no
connection_reset peer reset mid-transfer yes no
timeout connect or idle/stall timeout yes no
too_many_redirects redirect limit hit no no
http_client_error 4xx not otherwise classified no yes
http_server_error 5xx yes yes
auth_required 401 / 407 — task pauses, not terminal no yes
forbidden 403 after the referrer-retry (docs/04 §7) no yes
not_found 404 no yes
range_not_satisfiable 416 — triggers re-probe/re-split yes yes
gone 410 / expired signed URL, no mirror left no yes
server_file_changed 200 where 206 expected, or If-Range / ETag drift (docs/04 §5) no yes
content_length_mismatch bytes received ≠ declared length yes no
checksum_mismatch post-download hash ≠ expected no no
disk_full ENOSPC no no
io_error pwrite / fallocate / fdatasync failed no no
path_rejected outside allowed roots or not writable no no
permission_denied EACCES on the destination no no
meta_corrupt .veloxpart.meta failed CRC / bad magic no no
meta_version_unsupported meta file from a newer engine no no
probe_failed probe could not determine file info maybe yes
unsupported_url_scheme not http/https no no
max_retries_exhausted backoff loop gave up (data names the last underlying Error) no maybe
internal invariant violation / bug no no

Concretely: a string enum on error.code mirroring these names is simplest to keep lossless; if it stays an integer, please reserve grouped ranges (e.g. 1xx network, 2xx http, 3xx local-io, 4xx metadata, 5xx internal) with room to grow inside each.

B2. Freeze the meaning of TaskSummary.segmentsmajor, retype-of-meaning

docs/04 §4/§3 require the effective value (after the per-host cap and the non-resumable→1 demotion) to reach the UI. TaskSummary.segments currently has no stated meaning. Please freeze it as "effective segment count in use right now", not the requested value. This is the only genuinely freeze-blocking half of the buffer/segment story — pinning a field's meaning after 1.0.0 is a major bump under rule 4. It stands alone; do not defer it together with B2a below.

B2a. A readable effective buffer size — minor, but note the trap

Split out from B2 on purpose so PROTO can't use the cheap half to defer the major half.

bufferBytes is write-only: accepted by download.add and download.update.patch, readable on no type. docs/04 §4 says the clamped / "silently reduced to fit" value is "reported back to the UI", so CORE will expose an effective value and PROTO needs a frozen field to carry it. Adding an optional field is minor under rule 4 — except that TaskSummary is additionalProperties: false, so a daemon cannot tack it on as an extension; it has to be in the schema or it's dropped by every conformance replay. Name and placement are PROTO's call (effectiveBufferBytes on TaskSummary / TaskDetail, or a dedicated readback). Land it whenever — it just can't be improvised later.

B3. Freeze Segment.schema.json field names

Segment is already on PROTO's type list (contracts/README.md) and TaskDetail "includes segments[]", so this is mostly field-name matching — but do it before freeze or each fix is a rename = major.

CORE will emit per segment:

CORE field Type Note
index int ≥ 0 event.task.progress currently says i. Pick one name for both.
start int ≥ 0 absolute byte offset, inclusive
end int ≥ 0 absolute byte offset, exclusive — range is [start, end)
completed int ≥ 0 bytes written in this range so far
speedBps int ≥ 0 current per-segment rate
state enum connecting | downloading | stalled | complete | failed

Asks: (a) reconcile i vs index — one spelling in both the Segment type and the event.task.progress payload; (b) confirm the half-open [start, end) convention in the schema description so DAEMON and GUI don't off-by-one the last byte; (c) confirm the segment state enum values.


Do not hold the freeze for these

All minor under rule 4 (new methods / new events / a pattern on an existing optional string). contracts/README.md rule 5 wants contract changes "cheap and frequent" — raise these now, land them as small follow-up PRs, but do not gate 1.0.0 on them. Each is needed within M1.

F1. A "needs user decision" carrier — new event + method, minor

docs/04 §5 mandates a "File on server has changed — restart download?" prompt, and §7 has 416/stale-metadata. The event surface has only event.notify + task states. Either:

  • confirm state: paused + an event.notify {level:"warn", …} is the intended carrier (CORE reports server_file_changed, DAEMON pauses + notifies, GUI offers restart), or
  • add event.task.decision {taskId, kind, options[]} + download.decide {taskId, choice}.

CORE needs to know which, but a string comparison on error.code covers the engine side either way, so this doesn't block freeze.

F2. A credential return path for 401/407 — new method, minor, M1-needed

event.auth.required exists; there is no method to hand credentials back to a paused task. session logins / Secret Service is M3, but "401 handled" is in CORE's M1 DoD. Requesting download.provideAuth {taskId, username, password, save?}{ok}. Secrets go to the Secret Service, never SQLite/logs (CLAUDE.md §4) — save just tells DAEMON whether to persist.

F3. checksum string format — pattern on an existing optional field, borderline

download.add {checksum?} has no shape. CORE's verifier will accept "<algo>:<hex>" with algo ∈ {md5, sha1, sha256} (appendix lists MD5/SHA-256). Adding a pattern to an existing optional string is close to free; PROTO's call whether it's worth a fixture now or a doc note.


Three-way ADR material (after PROTO answers)

D1. State-machine ownership split — CORE / DAEMON / PROTO

docs/04 §1 hands CORE the lifecycle, but queued and every scheduler transition are DAEMON's. Proposed split, to be recorded as an ADR once all three agree:

  • CORE owns: probing → connecting → downloading ⇄ paused → retry_wait → assembling → verifying → complete | failed, plus cancelled from any state.
  • DAEMON owns: new, queued, and pause-for-schedule (enters CORE's paused).
  • Shared: paused is reachable from either side; both must be idempotent about it.

This needs an ADR regardless of the answer (contracts/README.md rule 4: the lifecycle enum is a frozen type and any change to who drives it is a documented decision).