contract run · 30-run.md · 31-pipeline.md · 32-derive.md · guide · card
The derive tier
Derive tasks process committed rows and append results through the ordinary write path. Implementers use this contract for task selection, engine bindings and output rows. Media tasks produce citable passages; link tasks extract document head fields and advertised images.
Derive tasks read parent rows, invoke an engine and write output rows:
select
The derive source selects eligible parent rows each tick and limits each run’s work.
required-keytestMissing or blanksource_tableorparent_id_columnraisesDeriveConfigKeyMissing, naming the key and pipeline. For built-in tasks, missing or blankengineormedia_columnraises the same error.no-store-roottestA source built without a store root or without its pipeline id raisesDeriveNoStoreRoot, naming which is absent.foreign-output-tabletestThe anti-join’s pipeline id comes from the build alone; a config key naming another pipeline’s output raisesDeriveForeignOutputTable.rows-per-runtestmax_rows_per_runtruncates the outstanding list after the anti-join, at 25 rows by default; zero or below resolves to that default.sampletesttestsample_rater keeps a host unit before the anti-join when the first eight bytes of SHA-256 over its key then task name, read big-endian and divided by 2^64, fall below r; r outside(0, 1]refuses asspec-invalid.Why
because a hash of the unit and the task decides membership without stored state, so every pass and every resumed pass selects one setsincetesttestsince, an RFC 3339 instant, keeps a host task’s parent rows whose_ingested_atfalls at or after it, before the anti-join; a row without a readable_ingested_atis dropped, and anothersincerefuses asspec-invalid.Why
because a bound on landing time limits a pass to recent parents, and an unstamped row has no landing time to comparefiltertesttesttestfilterkeeps a host task’s parent rows meeting every condition, a scalar testing equality and a list membership, before sampling; a column outside the key and declared columns, or a conditionfilter-valuesorfilter-budgetrefuses, refuses asspec-invalid.Why
because conditions over columns the unit already carries narrow a pass without another read, and a column the task never reads holds nothing to compareordertesttesttesttestorder_by, the key or a declared column thenascordesc, orders a host task’s outstanding units beforerows-per-run, nulls last and ties by unit key; selection holds at most twice that budget in units.Why
because a key tie-break gives every pass and every resumed pass one order, and cutting back whenever the buffer fills bounds memory below the parent countattempts-per-unittestmax_attemptsdefaults to 3 attempts per unit. A non-positive value selects that default.seconds-per-runtestmax_seconds_per_runbounds the serial unit loop at 300 s by default forlink_previewand carries no default fortranscribe.incomplete-unittestIf a parent row’s key or media column is null, empty or whitespace, the derive source raisesDeriveUnitIncomplete. The derive source skips the row and counts the row on the run record.Why
P4journaled-pulltestA derive pipeline configured to journal its pulls raisesDeriveJournaledPull.unmetered-granttestA derive pipeline declaring a shared-quota grant for a task other thanlink_previewor a host task declaringpurefalse raisesDeriveUnmeteredGrant.metered-clienttestAlink_previewpipeline opening a socket outside the mediated client raisesDeriveMeteredClient; every request it makes enters the run’s request ledger.anti-jointestEach tick recomputes the outstanding set: every parent row holding neither a passage nor a settled marker under its currentderivation-keyin the pipeline’s own output table, or, for a host task, its marker table.unit-parentstestA host task reads each joined table once, before the anti-join and before processing its first unit. Each unit carries every row whose join-column value equals that unit’s key. Rows landed after that read are absent.join-keytesttesttestA host task that joins tables also hashes each unit’s joined rows into itstask-versionkey, each table’s rows sorted over its read columns, so a joined row landing, changing or leaving re-derives that unit alone.join-keyedtestA task reads joined tables declaringprimary_keythroughdedup-view. Each unit joins one surviving row per key, excluding superseded versions.Why
because a raw file read hands a re-landed row in both versions, and the admitted read hands onejoin-absenttestA joined table the store holds no schema for fails the tick with aConfigfailure, settling no unit, so the next tick selects every unit again.Why
because an absent table read as empty settles each unitokon a partial join that the anti-join never revisitslatest-markertestA unit’s state under a key comes from its latest marker ordered by_ingested_at,_run_idand_row_seq. The highest attempt count does not determine that state.Why
because anemptymarker records 1 attempt, so ranking by count lets an older retry revive a settled unitkey-changetestRows under the current key landed before the unit’s latestokoremptylanding under another key count for nothing, so a key changed and changed back derives the unit again.derive-ordertestA derive pipeline reading another derive pipeline’s output table runs after that parent on the parent’s tick, regardless of declaration order or its own schedule.parent-outcometesttestA derived child starts only once its parent produces a completed run outcome, a failed one included, and then reads only the parent’s committed rows; a launch, wait or signal failure, or a worker dispatch error, stops the unit first.join-ordertesttestA host task joining another derive pipeline’s output table runs after that pipeline on its tick, asderive-orderorders a source table’s reader; among several such parents on one chain, it runs after the last.parents-divergetestA derive pipeline whose source-table and joined parents lie on no one dependent run raisesDeriveParentsDiverge, naming them, and arms none.Why
because a dependent run orders one chain from its head, so parents on two chains share no tickderive-cycletesttesttestA build whose derive source-table or join dependencies return to a pipeline raisesDeriveCycle, names every pipeline on the cycle and arms none.derive-after-conflicttestA derive child whoseafternames a pipeline other than the derive parent it runs after raisesDeriveAfterConflictand arms none.parent-scantestThe derive source scans parent tables of at most 1000000 rows in memory. For larger tables, the source streams through the read engine in batches, holding one batch in memory.dry-runtestpipeline run --dry-runon a derive pipeline prints its eligible, already-derived and outstanding unit counts from one selection, calling no engine and landing no row.available-selecttestA derive run carryingavailable_as_ofselects only parent rowsavailable-as-ofadmits at that bound, and each derived row lands its parent row’s availability instant.Why
because content derived from a later source record otherwise answers an earlier horizon
Scenarios
run.select.derive-order: WHEN configuration declares a scheduled child before its parent and the child reads the parent’s output, THEN the parent’s tick lands its new row before the child reads it.run.select.parent-outcome: WHEN a parent fails after earlier rows committed, THEN its child reads those committed rows in the same tick.run.select.join-key: WHEN a keyed note one unit joins is re-landed with new text, THEN the next tick derives that unit under a new key and leaves the other units’ markers standing.run.select.join-order: WHEN a scheduled host task declared first joins the words another derive pipeline lands, THEN one cycle hands it the words that pipeline landed in the same tick.
bind
Engine bindings select registered tasks, configure engine interfaces and constrain media access and returned values.
unbound-enginetestAn engine with no[derive.<name>]block raisesDeriveEngineUnbound, naming the pipeline, the engine, the file to edit and every bound engine.command-in-manifesttestABindingkeypreprocess,envorallow_hosts, or a step’scommand, inside[pipeline.source.config]raisesDeriveCommandInManifest.unknown-tasktesttasknamestranscribe, the default,link_preview, or a task the embedding binary registered; any other value raisesDeriveUnknownTask, listing the built-in and registered names.host-tasktestAn embedding binary registers each compiled derive task by name before build. Configuration selects that registered name without supplying a command. A duplicate built-in or registered task name raisesDeriveTaskNameTaken.join-repeatedtestA host task whose joins name one table twice raisesDeriveJoinRepeatedat registration.Why
because a unit holds one joined list per table, so the second join’s rows replace the first’spure-tasktesttesttesttesttestA host task is pure unless it declarespurefalse. The engine hands a pure task no client, and its request for one refuses asmetered-client, failing the tick; the engine intercepts no socket the task’s own code opens.Why
because compiled task code shares the engine’s process, so the client the engine hands it is the one reach the engine withholdsreach-clienttesttesttestA host task declaringpurefalse receives the run’s metered client for every unit; a run binding none refuses its request asmetered-client.reach-granttesttestUnder agrant, the metered client of a host task declaringpurefalse reserves one permit from the grant’s shared quota before each request and model call, so every pipeline naming that quota draws from one limit.model-endpointtesttestA host task declaringpurefalse names itsmodel-endpointwithendpointandmodel. The run’s metered client reaches that endpoint’s host alone, and each request it sends enters the run’s request ledger under the marker table’s run id.Why
because a compiled task shares the engine’s process, so the client the engine hands it is the one route the engine metersmodel-keystesttestendpointormodelbeside a built-in or pure task refuses asconfig-key, and either without the other refuses asspec-invalid.Why
because a task the engine hands no client reaches no endpoint, and an endpoint without a model names no callmodel-fencetesttestA host task’s model call sends the task’s template in the system role and each value it reads insidedata-fencemarkers carrying its provenance label, answering the content beside itsoutput-taintlabel.Why
because a parent row is third-party data, and a value outside the markers reads to a model as instructionoff-endpointtesttestA host task’s request to an address off its endpoint’s origin refuses asmetered-clientbefore any byte leaves, failing the tick whether or not the task returns the error.Why
because the refusal repeats on every retry, and a unit settling past it records a derivation its task never completeddriver-mismatchtestdriverisexecor"none"behindtranscribeandfetchbehindlink_preview, and a host task names noengine; any other pairing raisesDeriveDriverMismatch.endpoint-host-baretestAnendpoint_hostcarrying a path, query, port or scheme raisesDeriveEndpointHostNotBare.attach-unboundtestAnattachtable on afetchbinding, or on a binding declaring noendpoint_host, raisesDeriveAttachUnbound, naming the engine.Why
becauseexec-proxyattaches a credential toward one declared vendor host, and a fetch reader reaches third-party hostsremote-url-unsupportedtestAnhttporhttpsmedia value for an engine declining remote addresses raisesDeriveRemoteUrlUnsupported, naming thewhen = "media_is_url"preprocess step.media-unreadabletestA media value that is neither an address nor a readable local file raisesDeriveMediaUnreadable, failing that unit alone.media-roottestThe engine canonicalizes local media paths under the binding’smedia_root(declaration-base), defaulting to that declaration base. A path outside the root raisesDeriveMediaOutsideRootand fails only that unit.Why
because a parent row is third-party data, and an unconfined path reads any file the machine’s user canconfidence-rangetestAconfidenceoutside0.0..=1.0after adapter normalization raisesDeriveConfidenceOutOfRangeand lands null.upstream-excerpttestAnUpstreamexcerpt holds at most 4 KiB and no request body.advisory-zonetestA row zone taken from the binding’s advisoryzonekey raisesDeriveAdvisoryZone.locality-widertestAn engine whose declared locality is wider than the zones its source table admits raisesDeriveLocalityWiderat build, naming the engine, its locality and the table.Why
because a derived passage carries its parent’s content, and an engine outside the parent’s zones moves that content where the parent cannot go
exec
The executor runs operator-declared argument arrays as local processes with pinned binaries, restricted environments and bounded runtime and output.
shell-commandtestA string-valuedcommandraisesDeriveShellCommand;no-shellruns the argument array.env-nametestAn allowlist name that is not ASCII alphanumeric or underscore, or that leads with a digit, raisesDeriveEnvNameInvalid.env-credentialtesttestAnenvvalue embedding a${secret://<name>}placeholder raisesDeriveEnvCredential, naming the engine and variable; a step reaches its vendor throughexec-proxy.chain-deadlinetesttimeout_secsbounds one unit’s whole chain at 1800 s by default.captured-outputtestmax_output_bytesbounds each step’s captured standard output and error at 8 MiB by default.audit-entriestestA chain contributes at most 64 entries of captured output to the run record.step-error-excerpttestA failing step’s error text carries at most 4 KiB of its standard error.non-zero-exittestA step exiting non-zero raisesDeriveStepExitcarrying its bounded error text into the run audit.deadline-elapsedtestIf a chain exceeds its deadline, the executor raisesDeriveStepTimeout, naming the running step.output-captestThe executor counts and discards output past the bound without buffering. Crossing the bound kills the step and raisesDeriveOutputCapwith the byte count. The executor continues draining so the child does not block.silent-steptestA preprocess step exiting zero without writing its output file raisesDeriveStepProducedNothing.missing-binarytestAcommand[0]that is not an executable file on this machine raisesDeriveBinaryMissing, naming the binary and the step.unpinned-pathtestA path-formcommand[0]withoutsha256raisesDeriveUnpinnedPath, quoting the computed digest. A bare search-path name carries no digest.digest-mismatchtestA pinned file whose bytes differ from its recorded digest raisesDeriveDigestMismatch.no-shelltestA step runs as its argument array with no shell, in a process group of its own, under a cleared environment holding only the binding’senvtable.step-conditiontestA preprocess step runs only while itswhenholds:media_is_urlfor anhttporhttpsinput,engine_requires_pcm16_wavfor an input other than 16-bit PCM WAV; another name raisesDeriveStepConditionUnknown.Why
because a misspelled condition otherwise skips or runs its step silentlyverified-spawntestBefore each spawn, the executor re-reads the step’s binary and compares its bytes with the digest resolved at run start. A mismatch followsdigest-mismatch; matching bytes permit execution.engine-idtestAnexecengine id readsexec:<name>@<prefix>, the prefix being 12 chars of lowercase hex over every step’s binary digest and arguments.vendor-deadlinetestA vendor engine, a binding naming anendpoint_host, holdsrequest_timeout_secsas its engine step’s deadline, clipped to the timechain-deadlineleaves, so a stalled vendor fails its unit before the chain elapses.Why
because a chain deadline alone lets one stalled vendor call spend the unit’s whole budget with no step to name
fetch
The fetch driver follows third-party links with host and address checks, bounded redirects, document head scanning and image probes.
schemetestAn address whose scheme is neitherhttpnorhttpsraisesDeriveSchemeUnsupportedbefore any socket opens.address-literaltestA host written as an address literal raisesDeriveAddressLiteral.redirect-chaintestA redirect chain runs to at most 5 hops.binding-keytestABindingkeyenv,preprocess,engineormax_output_byteson a fetch binding raisesDeriveFetchBindingKey, naming the key.Why
because a silently ignored key leaves an operator believing a fetch carries a credential or runs a processdocument-prefixtestmax_document_bytesbounds the scanned document prefix at 1 MiB by default, dropping the remainder.probe-prefixtestmax_probe_bytesbounds one picture range request at 64 KiB by default.hop-timeouttestrequest_timeout_secsbounds one hop at 20 s by default.retry-after-defaulttestA429without a usableRetry-Aftermaps toRateLimitedcarrying 60 s.charsettestA declared character set other than UTF-8 raisesDeriveCharsetUnsupported, naming the value.not-utf8testIf a document declares no character set and fails UTF-8 validation, the fetch driver raisesDeriveBytesNotUtf8. The validator tolerates a character split at the byte bound.head-rowstestA link preview lands one passage for non-empty head title or description and one passage per distinct advertised head image, each carrying the scrubbed page URL and each image its probe status.Why
because the head and its image claims remain separately citable, including an image whose probe fails
Scenarios
run.fetch.head-rows: WHEN a page head declares a title, description and one image, THEN the output holds one text passage and one image passage with its probe status.
emit
Derive tasks emit content rows and completion markers with status, attempt counts, citation keys and provenance.
failure-off-tabletestPer-unit failure state recorded outside the output table raisesDeriveFailureOffTable.unit-statustestunit_statusisok,emptywhere the engine established nothing to derive,unavailablewhere it returned nothing and stated no reason, orfailedon a typed error; any other value raisesDeriveUnitStatusUnknown.reserved-discriminatortestA derive table declaring a column namedkindraisesDeriveReservedDiscriminator.settled-revivedtestA unit that settles under its current key while a tick derives it raisesDeriveSettledUnitRevived, and that tick lands none of its rows; a changed key revives nothing.attemptstestattemptsis the prior count under the row’s key plus one;unavailableandfailedstop atattempts-per-unit, andemptyreceives 1 attempts in total.unredacted-errortestA non-nulllast_errorwrite that has not passed address redaction raisesDeriveUnredactedError.primary-keytestA derive output table or host marker table withoutprimary_key["unit_ref", "derivation_key", "cue_seq"], or a host content table whose key does not open withunit_refandderivation_key, raisesDerivePrimaryKeyMissing.Why
because a re-derived unit otherwise lands its new passages over the originals, or beside them with no key telling them apartderivation-keytestEvery passage and marker row carriesderivation_key: lowercase hex SHA-256 over the engine id, the binding less its bounds andzone, the output table’s declared columns, and the parent row’s id, media value andderivation_key.stale-supersedestestA unit’s latestokoremptylanding supersedes rows landed earlier under another key. Those rows answer until that landing. The next fold drops the superseded rows and rebuilds the table’s sidecars without them.task-versiontestA host task’sderivation_keyhashes its registered name, itstask_versionand the parent row’s id, declared columns andderivation_key, so raising the version re-selects every settled unit.host-rowstestA host task’s content row carries its unit’sunit_ref,derivation_keyandtask_version,kindpassageandunit_statusok; every unit lands one marker,okover content rows,emptyover none.content-emptytestA host unit landing no row in a content table lands onekindmarker,unit_statusemptyrow there under its key, sostale-supersedesholds in every content table.Why
because a read and a fold supersede from one table’s rows alone, so a content table never seeing the unit’s newest key keeps its stale rows answeringcontent-marker-retention-clocktestA generated host marker copies a customretain-rowstimestamp from the parent row when the parent carries a same-named column.Why
because a generated empty marker must carry the declared clock before the content table accepts its landingoutput-tablestestA host-task pipeline declares exactly its task’s marker and content tables, and a unit returns rows for its content tables alone; either breach raisesDeriveOutputTablesMismatch, the second failing that unit alone.marker-lasttestA host-task run commits each content table under its own commit, then the marker table, and a content table failing stops the fire before its marker lands, so the unit re-runs and its rows collapse by key.version-retainedtestOn an output table declaringretain_versions,stale-supersedesholds per unit andtask_version, so rows under an earlier task version stay current under that version beside the newer version’s rows.marker-rowtestA unit yielding no passage lands one marker row,cue_seq-1 andkindmarker, carrying its status, attempts, last error and whether it retries.empty-documenttestOnly a WebVTT document whose blocks are its header, notes and styles establishes nothing to derive; empty output, or blocks none of which parse, landsunavailable.canceled-unittestA unit whose chain a run stop interrupts lands no row and charges no attempt; the pull endsCanceledoncechild-reaped.output-modalitytestA text passage extracted from a video parent carries_modalitytext, and an advertised image passage carriesimage, each undermodality.local-content-keytestA local media unit’sderivation-keyincludes a SHA-256 digest of canonical file bytes, so changed bytes under an unchanged path reselect the unit.derived-idtestEvery passage and marker row keyed asprimary-keycarriesderived_id, lowercase hex SHA-256 over itsunit_ref,derivation_keyandcue_seq, and a sidecar over such a table defaults itsid_columntoderived_id.Why
because a sidecar re-joins on one column, and a producer column outside the reserved underscore namespace needs no store change
Scenarios
run.emit.output-modality: WHEN a video parent yields a transcript passage and an advertised picture, THEN their output modalities aretextandimage.run.emit.local-content-key: WHEN a local file changes bytes under the same path, THEN the next tick selects its unit under another key.
parse-cues
The caption parser reads SubRip and WebVTT through one grammar and groups cues into bounded passages.
backward-cuetestIf a block starts before the last accepted block, the caption parser raisesDeriveCueOutOfOrderand drops the block.Why
P4passage-charstestA passage stays open while it holds fewer than 600 chars.passage-spantestA passage stays open while it spans less than 60 s.passage-bytestestOne passage’s text holds at most 8 KiB.passages-per-documenttestOne document yields at most 2000 rows of passages.grammartestSubRip and WebVTT read through one grammar: a block’s timing line, then its text lines joined by a space.
test-engine
The test-engine command builds one derive engine outside a pipeline and runs that engine against one file.
unsupported-drivertestAfetchengine named to the verb raisesDeriveTestEngineUnsupported.Why
because the verb drives a transcriber over media, and a link engine takes no media
Shapes
Three engine bindings at the top level of contextful.toml:
[derive.local-asr]
driver = "exec"
timeout_secs = 1800
max_output_bytes = 8388608
zone = "local:device" # advisory; the adapter writes the row value
[[derive.local-asr.preprocess]]
when = "engine_requires_pcm16_wav"
command = ["ffmpeg", "-i", "{input}", "-ar", "16000", "-ac", "1", "{output}"]
output_path = "{output_stem}.wav"
[derive.local-asr.engine]
command = ["speech-cli", "--model", "base", "--srt", "{input}"]
output_format = "srt"
output_path = "{output_stem}.srt"
[derive.local-asr.env]
SPEECH_MODEL = "base"
[derive.cloud-asr]
driver = "exec"
endpoint_host = "api.speech.example" # steps reach it through CONTEXTFUL_EGRESS_URL
[derive.cloud-asr.attach]
Authorization = "Bearer ${secret://speech-vendor}"
[derive.cloud-asr.engine]
command = ["speech-upload", "--server", "{input}"]
output_format = "srt"
[derive.card-reader]
driver = "fetch"
allow_hosts = ["example.com", "*.example.com"]
allow_image_hosts = ["*.cdn.example.net"]
request_timeout_secs = 20