Releases: iyulab/Formbase
Release list
Formbase 0.17.1
A patch. Pairs with MorphDB 0.14.x, unchanged from 0.17.0. No contract change.
Fixed
- Packages carry the license text. Every package now ships
LICENSEat its root beside the
Apache-2.0license expression, so redistributing a package carries the license copy the license
requires and tooling that collects third-party notices finds the text. WatermarkLagTriggercounts a form type's own documents. A lag threshold above one compared the
watermark gap, but watermarks are shared by every form type, so other types' documents brought a
projection due early. It now counts this type's documents after the projected watermark, reading no
more than the threshold. The default threshold of one is unaffected.
Formbase 0.17.0
Pairs with MorphDB 0.14.x. A projection whose declaration has not changed reads only the documents
appended since its last run, with the result a rebuild would have, and a bound field names its value
column and, optionally, which record its value belongs to. Breaking for code that constructs or reads
EntityRef, for HTTP clients that send or read a bound field's target, and for implementers of
IProjectionStore and IProjectionState, which gain a member each.
Changed
- A bound field's target names its value column as
ValueField, notKeyField. The old name read
as a lookup key, and declarations were written that way —KeyFieldwas always the column the value
comes from.EntityRef(entity, valueField, lookupKey, viaField); on the wire,target.valueField
replacestarget.keyField. Declarations already stored by an earlier release read back unchanged.
RelationHint.KeyField(a relation's link field) is unaffected.
Added
- A bound field can say which record its value belongs to.
EntityRef.LookupKey(the field on the
target that identifies a record) andEntityRef.ViaField(the field of this declaration carrying that
key) — optional, and only as a pair. On the wire,target.lookupKeyandtarget.viaField. - A target naming a field that does not exist is refused. Every declaration writer (in-memory,
SQLite, PostgreSQL) refuses aViaFieldthat is not a field of the same declaration, and a
ValueFieldorLookupKeythat is not a field of the target form type when that form type is
declared (DeclaredTargets); the host answers400/problems/invalid-declaration. An undeclared
target is accepted unchecked. - A projection brings its table forward instead of rebuilding it when only documents were appended.
When the declaration, table and recorded state are unchanged since the last projection, a run reads
only the documents appended since its watermark: rows of records they correct or retire are removed,
their own rows inserted, and the recorded skips updated in place. The table andGET …/projection/skipsare what a rebuild would have produced; the cost of a run follows what was
appended, not the size of the raw stream. A first run, a changed declaration or table, a table left
in doubt by a failed run, and the first run after upgrading still rebuild.ProjectionResult.Mode
(andmodeonPOST …/projection:rebuildorincremental) says which happened; on an
incremental run the run's counts and skip lists cover the documents it read. IProjectionStore.ReplaceRowsAsync(remove rows by record key or document, then insert — idempotent)
andIProjectionState.ApplyProjectedDeltaAsync(compare-and-set on the recorded watermark, withdraw
skips by record or document, append new ones).ProjectionSkip.KeyandProjectionFieldSkip.Key
name the record a skipped document stood for;ProjectionStamp.SkipsKeyedsays whether they do.
SQLite and PostgreSQL state stores add the columns on first use.
Fixed
- Two projections of one form type started at once in one process no longer race: they run one after
the other. A run that finds the recorded projection moved under it marks it unverified, so the next
run rebuilds rather than serving a table both runs wrote to.
Formbase 0.16.0
Pairs with MorphDB 0.14.x. An optional field whose value cannot be converted no longer drops its
document, and a timestamp without an offset reads the same on every host. Breaking for code that
implements IProjectionState.
Changed
- An optional field whose value cannot be converted empties that field instead of dropping the
document. A nullable column holding a value its type cannot take ("next week"in aTimestamp, an
array in aText) used to skip the whole document, so one bad value hid every other value it carried
from every query. The row now lands with that column empty, and the emptied field is recorded (see
Added). A required column that fails the same way still skips the document, as documented: the row
cannot stand without it. Re-projecting an existing store can therefore insert rows that were skipped
before.
Added
- Emptied fields are recorded like skipped documents.
ProjectionFieldSkip(document, field,
reason) for each optional field a run emptied: inProjectionResult.SkippedFields, recorded with the
run byIProjectionState(GetFieldSkipsAsync,FormbaseEngine.GetProjectionFieldSkipsAsync) so it
survives a restart, and replaced by the next run. They are counted neither inSkipped(the row
landed) nor inAbsentFieldCounts(the document did carry a value). Host:skippedFieldson the
projection run response,skippedFieldsandfieldCountonGET /formtypes/{type}/projection/skips,
andlastRun.skippedFieldCounton the projection status. - Breaking for
IProjectionStateimplementers:SetProjectedAsynctakes the field skips next to
the skips, andGetFieldSkipsAsyncis new. The SQLite and PostgreSQL adapters keep them in a table of
their own (fb_projection_field_skips/projection_field_skips), created on first use in an
existing store.
Fixed
- A timestamp written without an offset is read as UTC, not in the host's zone. A date alone
("2026-01-15") or a date and time with no zone projected to a different instant on every machine
that ran the projection — a date alone became the previous day east of Greenwich. Such a value is now
that moment in UTC, so a date alone is its UTC midnight and reads back as the date written. Query
filter values follow the same rule, so a filter written like the documents matches them. A value with
an offset keeps it, as before.
Formbase 0.15.0
Pairs with MorphDB 0.14.x. A batch of documents is accepted in one durable write. Breaking for code
that implements IRawStore or IIntakeService.
Added
- Accepting a batch in one durable write.
FormbaseEngine.AcceptManyAsync(type, documents)takes
documents and retirements of one form type (IntakeDocument.Accept,IntakeDocument.Retire) and stores
them as one unit: when it returns all of them are durable, when it throws none is. Each accept on its own
waits for its own commit — on SQLite, a sync to disk per document — so filling a store one document at a
time is bound by that wait; a batch pays it once (10,000 documents into a SQLite file: about 13 times
faster). On PostgreSQL a batch's inserts also travel together, a thousand to a round trip, so a database
across a network is not waited on once per document. The batch's documents take consecutive watermarks in the order given. Idempotency keys work per
document, so a batch cut off and sent again stores nothing twice; a key already holding another request
refuses the whole batch withIdempotencyKeyReusedException, before anything is written.
Changed
- Breaking:
IRawStore.AppendManyAsync(type, appends)is new — a raw-store adapter implements it,
and must store the batch atomically, which is why there is no default implementation that appends one
at a time.RawAppend.DistinctandRawAppend.EnsureRepeatscarry the idempotency rule an adapter
checks before writing.IIntakeService.AcceptManyAsyncis new as well.
Formbase 0.14.1
Pairs with MorphDB 0.14.x. Fixes for durable declarations: a declaration can no longer destroy the raw
documents of a single SQLite file, and the durable stores keep what a declaration says.
Added
DeclaredTableName— the rules every declaration writer applies before storing (IsReserved,Same,
EnsureDeclarable,EnsureUnclaimed), andTableNameInUseException, raised when a declaration names a
table another form type already projects into. Over HTTP it answers409/problems/table-name-in-use.
Fixed
-
A declaration can no longer name a table in the engine's
fb_namespace. On a single SQLite file, which
keeps the raw documents, projection state and declarations beside the projected tables, a form type
declared intofb_raw_documentsreplaced the raw table on its first projection — every document of
every form type in the file was lost — andfb_field_hintsorfb_projection_statebroke the file the
same way.InMemoryFieldHintSource.Declare,SqliteFieldHintSource.DeclareAsyncand
PostgresFieldHintSource.DeclareAsyncnow throwArgumentExceptionfor a blank orfb_table name, the
host answers400/problems/invalid-declaration, and the SQLite projection store refuses to build or
drop anfb_table, so a declaration stored before this check still cannot reach one. -
Two form types can no longer be declared into one table. Each rebuilt the table from its own documents,
so a query of either read whichever projected last. The three declaration writers refuse the second
(names compared ignoring case) withTableNameInUseException, atomically with the write; redeclaring a
form type into its own table is unaffected. Declarations already stored that share a table keep
projecting as before, but neither can be redeclared into that table until the other moves. -
The durable declaration stores (
SqliteFieldHintSource,PostgresFieldHintSource) kept only a
declaration's table and fields: every declaration read back as version 1 with no relations. On the
durable host the second replacement of a form type was therefore refused as a version conflict, and
declared relations were gone after a restart — so achildrelation declared through that host never
reached MorphDB as a relation. Both stores now keepDeclarationVersionand
Relations; a table written by an earlier version is upgraded in place on first use, and the
declarations in it read back as version 1 with no relations — what they were stored as.
Versions and relations declared before this release were never stored, so an upgraded store reads
those declarations as version 1 with no relations until they are declared again (on the host, send
expectedDeclarationVersion: 1for that one replacement). -
A form type stored as JSON — the target of a bound field inside a durable declaration — read back as
the empty default, becauseFormTypeRefcould be written but not read. On the SQLite and PostgreSQL
stores a form type with a bound field (SnapshotorReferencewith aTarget) therefore could not be
projected at all: resolving the target's table failed with "Value must be set" — and on the durable
host, declaring one answered500. It now reads back as
the form type it was, including from declarations already on disk, so those form types project
without being declared again.
Changed
FormTypeRefserializes as its identifier string ("work-order") rather than as an object
({"Value":"work-order"}); both forms are read.- A document skipped because an array arrived for a Text column is told both ways out: declare the column
as Jsonb to keep the array as one value, or, when its items are rows of their own (a repeated section),
append each item as a document of its own form type — projection does not split an array into rows.
The skip used to name Jsonb only, which for a repeated section keeps every row in one column.
Formbase 0.14.0
Pairs with MorphDB 0.14.x. A correction is a new append — and now it can say which record it corrects. Breaking for code that
implements IRawStore or reads StoredDocument.Body, and for callers that passed a cancellation token
positionally to AcceptAsync/AppendAsync.
Added
- Record identity. A document may name the record it belongs to with a
RecordKey(an opaque
string, compared exactly, scoped to the form type):FormbaseEngine.AcceptAsync(type, body, recordKey: key), over HTTPPOST /formtypes/{type}/documents?recordKey=…. The projection shows each
record once — the document with the latest watermark — so a corrected record is one row and a count
counts it once. Documents without a key are records of their own, exactly as before. - Retiring a record.
FormbaseEngine.RetireAsync(type, key), over HTTP
DELETE /formtypes/{type}/records?recordKey=…, appends a retirement: a document with no body that
takes the record out of the projection while its earlier documents stay in the raw stream. Appending
under the key again brings the record back. Idempotency keys work for retirements as for intake. - Record keys are compared exactly and are not Unicode-normalized: the same text in two normalization
forms (a file name from one system in NFC, from another in NFD) names two records. Normalize keys that
come from file names or user input before sending them. RecordFold.Latest— the fold the projector applies (latest per key, retired keys dropped), public so
any reader of the raw stream can get records rather than appends.- Raw reads carry
recordKeyandretired(StoredDocument.Key,StoredDocument.IsRetirement). A
retirement reads back with a null body; readretiredrather than testing the body, since a document
whose content is the JSON valuenullhas a null body too. - Projected tables gain the
fb_record_keybookkeeping column. Record reads still return the declared
fields only.
Changed
- A durable host refuses to start — exit code
78, naming the setting — when MorphDB answers that the
configuredFormbase:MorphDb:ProjectIddoes not exist. It used to report ready and fail its first
projection with an unhandled error. A MorphDB that cannot be reached at startup does not stop the
start, as before. - Breaking:
IRawStore.AppendAsync(type, id, body, key = null, cancellationToken)takes the record key
before the cancellation token, andIRawStore.RetireAsyncis new — a raw-store adapter implements both.
IIntakeService.AcceptAsynclikewise takesrecordKeybefore the token, andIIntakeService.RetireAsync
is new. A call that passed the token positionally names it (cancellationToken: ct). - Breaking:
StoredDocument.Bodyis nullable — null for a retirement.StoredDocumentResponse.Body
is nullable over HTTP for the same reason, next to the newrecordKeyandretired. - An idempotency key reused under another record key, or for a retirement where it named a document (or
the reverse), is refused like a key reused with a different body. - Schema intelligence skips retirements when sampling a form type's documents.
- SQLite and PostgreSQL files and schemas from 0.13.x are upgraded in place on first use: the raw
table gains its record-key column (and, on SQLite, a retirement column), and every document it holds
reads back as a record of its own. On PostgreSQL the check runs before any change, so an up-to-date
schema takes no table lock at startup. An existing projected table gainsfb_record_keythe next time
it is rebuilt; until then nothing in it is keyed, because nothing appended before the upgrade was.
The upgrade is one-way. Once a record has been retired, 0.13.x can no longer read the form type's
raw stream (it answers500), and a projection it runs leaves the table empty. Back up the database
before upgrading if you may need to go back. - A bound field's
target.keyField(EntityRef.KeyField) is documented for what it is: the field on
the target form type whose value the bound field carries, not a key for finding the target record.
The API reference had shown it one way and nothing said which; nothing about its behaviour changes.
Dependencies
Microsoft.Extensions.AI.AbstractionsandMicrosoft.Extensions.AI.OpenAI10.10.1 (was 10.10.0), a patch.Formbase.MorphDbdepends onMorphDB.Client0.14.0 (was 0.13.1), and the live tests and the compose file
run theghcr.io/iyulab/morphdb:0.14.0image.
Formbase 0.13.0
Pairs with MorphDB 0.13.x, 0.13.1 or later. Record queries grow from equality to ranges, text
matching and empty-column tests, and the engine counts records per group. Formbase.Sqlite is new: the
whole engine's storage in one SQLite file, for a process with no database server. One breaking change
for code: QuerySpec.Filters is a list of FieldFilter (FieldFilter.Equal(column, value) is the old
entry), and an IProjectionStore adapter implements AggregateAsync. On the MorphDB store, a filter for
records whose column is empty used to miss them.
Added
- Record queries filter with ranges and text matching, not only equality. A
FieldFilternames a
column, an operator and a value:Equalon any column;GreaterThan,GreaterThanOrEqual,
LessThanandLessThanOrEqualon integer, decimal and timestamp columns;Containsand
StartsWith, ignoring case, on text columns;IsNullandIsNotNull(FieldFilter.IsNull(column)),
which take no value, on any column — "the records whose judgement is still blank". Filters all apply, and values are coerced to the
column's declared type as equality values already were, so a date range can be written as text. An
operator the column's type does not answer, a range or text match against null, or a value given to
IsNull/IsNotNull, is refused with
InvalidQueryException(its newInapplicableFiltersnames them) rather than answered with no rows.
Text ranges are left out on purpose: backends order text differently, and the same query must return
the same rows whichever store answers it. Formbase.Sqlite: the raw store, projection store, projection state and field hints in one SQLite
file (AddSqliteRawStoreandAddSqliteProjectionover one connection string — two different files
are refused), for a process with no database server. Raw watermarks are never reused, so a restarted
process finds its documents and its projections current. It passes the same contract tests as
the in-memory and MorphDB stores: decimals compare numerically (kept as exact text under a numeric
collation), instants compare as instants whatever their offset, and text matching ignores case beyond
ASCII. Declared relations are not materialized.FormbaseEngine.AggregateAsynccounts projected records, optionally per group (AggregateSpec:
GroupBycolumns and the same filters). Groups come back ordered by their keys, nulls first, with key
values as the declared column type, and the answer carriesStaleand refuses exactly as a query
does. Ungrouped, it is one count — zero when nothing matches.
Changed
- Breaking:
QuerySpec.Filtersis a list ofFieldFilterinstead of a column-to-value dictionary;
FieldFilter.Equal(column, value)is the old entry.IProjectionStoregainsAggregateAsync, which
an adapter implements by counting rows per group; the in-memory and MorphDB stores do, and both pass
the same contract tests. The HTTP record endpoint still takes equality filters only. Formbase.M3Ldepends onM3L.Native0.17.0 (was 0.15.0), which targets .NET 10 like Formbase. The
parsed declarations it reads are unchanged.
Fixed
- On the MorphDB store, an equality filter with a null value — "the records whose column is empty" —
was sent as a comparison with an empty string and missed the empty records. It is now sent as
MorphDB'sisnull, which needs MorphDB0.13.1or later; the in-memory store always answered it. - The compose bundle's PostgreSQL health check asks over TCP. Asked over the Unix socket, it reported
the database healthy while the image was still running its init scripts on a socket-only server
that restarts afterwards, so on a firstdocker compose upMorphDB could start against a server
that was about to go away.
Formbase 0.12.0
Pairs with MorphDB 0.12.x, unchanged. A durable schema now records the namespace it holds and
refuses a host serving another name, an idempotency key belongs to one request, and a refused
configuration ends the process with exit code 78. Two changes are visible to existing deployments:
hosts that shared one schema under different namespaces no longer both start (see Upgrading under
the namespace section of the API reference), and a document naming a property twice is refused.
Added
GET /settingsreportsstorage— the PostgreSQL schema and MorphDB project a durable host keeps
its data in (nullin-process). Two hosts reporting the same storage serve the same data, whatever
namespace each answers to; the API reference and README now say that several namespaces sharing one
PostgreSQL and one MorphDB need a schema and a project each.
Changed
-
A host that refuses its configuration — an unknown store profile, a durable profile missing a
setting, half a model configuration, a schema holding another namespace — exits with code78
(EX_CONFIG) and says why once. It used to end on an unhandled exception: the runtime's crash exit
code, and the message repeated under a stack trace. -
A document whose JSON names a property twice in one object is refused with
400
/problems/invalid-request(andDocumentBody.From/Parsethrow). It used to be accepted, and
the durable store kept only the last value while the in-process store kept both, so the body read
back depended on the store. -
A durable host's PostgreSQL schema records the namespace that first used it, and a host
configured with another namespace over that schema refuses to start. Two names over one schema
used to both start and serve each other's documents. A schema in use before this records the
first namespace to start over it, so a deployment with one namespace per schema sees no change.
A host whose database could not be reached at startup makes the check before its first request
and answers503/problems/namespace-unverifieduntil it passes; the readiness probe reports
it too. The MorphDB project is not checked: namespaces sharing one replace each other's projected
tables of the same name.If two namespaces already share one schema, decide which of them keeps it before upgrading, and
start that host first and alone: whichever starts first claims the schema, so hosts started
together are decided by which one got there first. Give every other namespace its own
Formbase:Schema(and MorphDB project). The documents those hosts wrote while sharing stay in the
claimed schema, under the namespace that claimed it — the raw stream does not record which host
wrote a document, so Formbase cannot separate them.
Fixed
- An
Idempotency-Keysent again with a different request — another form type, or the same form
type with a different body — is refused with422/problems/idempotency-key-reused. It used to
answer201while storing nothing: the key's document stayed as it was and the new body was
dropped. A retry of the accepted request still returns the same document; bodies are compared as
JSON values, so property order and whitespace do not matter.
Formbase 0.11.1
Pairs with MorphDB 0.12.x. A patch: no surface moves. The packages carry their XML documentation,
the README quick start compiles as written, and the Docker release no longer republishes a version
already in the registry.
Added
- The packages carry their XML documentation, so an IDE shows each member's comment from the package.
Fixed
- The README quick start compiles as written. It builds a
ServiceCollection, whose
BuildServiceProviderlives inMicrosoft.Extensions.DependencyInjection;Formbase.DependencyInjection
depends only on the container abstractions, and the install list did not name the implementation, so
a console app following the README failed withCS1061. The install list now names it (a host that
already has a container skips it), and CI compiles the quick start against freshly packed packages
and exactly the packages the README installs.
Changed
- The Docker release no longer republishes a version that is already in the registry. A push that
changedDirectory.Build.propswithout changing<Version>used to rebuild that version from the
pushed commit and move its image tags onto code never released under that number; the release now
checks the registry first, publishes nothing when the tag exists, and fails rather than guesses when
the registry cannot be asked. The NuGet release was already idempotent.
Dependencies
M3L.Nativeto 0.15.0. Its parse output lists metadata, custom-section and extension keys in
sorted order rather than in an order that changed from run to run; its new lookup-path check
(M3L-E022) is a validation diagnostic, and the hint adapter only parses, so a declaration it
accepted before is accepted now.MorphDB.Clientto 0.12.3, and the MorphDB server image indocker-compose.yml, the README and
the live fixtures to0.12.3(a patch: XML documentation and packaging). The live suite passes
unchanged against the published0.12.3image.
Formbase 0.11.0
Pairs with MorphDB 0.12.x. A minor: a form type's raw stream is now readable over HTTP, page by
page, and a projection that builds nothing says why — which changes the public
NotProjectedException constructor in Formbase.Core. Everything on the wire is additive. The M3L
hint adapter maps and records more of what a declaration says, on M3L.Native 0.14.0.
Added
GET /formtypes/{type}/documentsreads a form type's raw stream page by page, oldest first, after
a watermark cursor (after,limit— default 100, at most 1000). Until now the only raw read was a
single document by an id the caller had to already hold, and records carry only declared columns —
so across the HTTP boundary, fields nothing had declared could not be read at all. Each page
carriesrawHead, read before the page and never passed, so a caller is caught up exactly when the
last watermark it received equals it;limit=0reads the head alone. An out-of-rangelimitor a
negativeafteris refused with400rather than adjusted. InFormbase.Core,
FormbaseEngine.ReadDocumentsAsyncandDocumentPageare the same read for an embedding host.
Changed
-
The README's HTTP surface summary no longer says writing a declaration is not on the surface:
PUT
andDELETE /formtypes/{type}/declarationhave been served since 0.8.0. -
POST /formtypes/{type}/projectionsays why it built nothing. A run on a form type with no
declaration answered200withprojected: falseand three empty diagnostic lists — and empty
lists read as "nothing was lost", so the response looked like a successful run with nothing to
do. It now carriesnotProjectedReason:noDeclarationwhen only a declaration can give the form
type a shape,nothingToInferwhen schema intelligence is installed but had no documents to infer
from;nullwheneverprojectedistrue. Additive on the wire. -
The
/problems/not-projecteddetail names the one remedy that applies instead of offering two.
It used to say "declare field hints or trigger a projection" in every case, and with nothing
declared the second remedy is a projection run that projects nothing — a loop the server never
named. With nothing declared it now says to declare first; with a declared shape not yet built it
says to trigger a projection. The problemtypeis unchanged. InFormbase.Core,
NotProjectedExceptiontakes the state as a second constructor argument and exposes it as
HasSchema. -
The M3L hint adapter now maps the whole numeric catalog.
byte,shortandlongjoin
integerin the integer slot — that slot is emitted as a 64-bit column, so no rung of the ladder
loses a value — anddoubleandpercentagejoinfloat,decimalandmoneyin the decimal
slot. Until now a field declared with any of those five names was reported as an unmapped type
and degraded to a text column, which understated what the vocabulary can carry.binaryis
deliberately unchanged: it is the one catalog type with no slot to map to, so it still degrades
to text with a recorded gap. -
The M3L hint adapter counts two losses it used to let through unrecorded. A declared type
parameter (string(50),decimal(10,2)) is recorded as a constraint gap, and an array field —
which lands in a single JSONB column — is recorded with the element type and item nullability it
cannot carry. Neither behaviour changes: the same columns come out. What changes is that the gap
list, which exists to make the loss countable, no longer omits these two. -
The M3L hint adapter records the three composition and ownership constructs M3L 0.13.0 makes
structural. A model declared::aspect(Base)or::subtype(Base)is recorded with its base link,
the same way an inherited model already is; a field contributed by::extendkeeps its column
and is recorded with the owner it came from; and a model under a# Prefix:(or namespace) owner
is recorded with that owner, since the form type is still identified by the bare model name. The
hints themselves are unchanged — before 0.13.0 the base kinds were not models at all and were
skipped without a trace, so this also stops an aspect or subtype from being silently emitted as a
standalone table.
Documentation
- The compose bundle is documented as needing
--build, and its opening smoke check is now
GET /health/readyrather thanGET /settings. The host service is built from the tree, so
docker compose upon its own brings back whatever image the last build left — a checkout that
has moved on since then comes up serving the older one./settingsanswers200from any
build and so cannot tell the two apart; the readiness probe answers404on a build that
predates the store health checks. The container health check already asked the same question
from inside and a stale bundle never reported healthy, but nothing said to look there.
Dependencies
M3L.Nativeto 0.14.0. 0.11.0 adds a multi-file validation entry point (ValidateMultiand its
typed and result-returning siblings) alongside the existing multi-file parse, and 0.12.0 widens
the type catalog withbyte,shortanddoubleand statesfloatas 32-bit; the three new
type names are mapped by the hint adapter in this same release. 0.13.0 adds the# Prefix:owner
header,::extendblocks (merged into their target's fields, each carrying its origin) and the
::aspect/::subtypebase kinds, which are now ordinary models with a base rather than untyped
generic kinds — a document that used those three words as custom kinds changes meaning. The
adapter's handling of them is described above. 0.14.0 resolves enum inheritance into an enum's
values and tightens validation (a later hop of a lookup path without a reference, a value name
repeated across inheritance, an argument on an extend header); the hint adapter parses without
validating and maps enum types to text, so its output does not change.MorphDB.Clientto 0.12.2, and the MorphDB server image indocker-compose.yml, the README and
the live fixtures to0.12.2. The producer's releases since 0.12.0 are patches carrying dependency
rounds and documentation corrections; no wire member moves. The suite with the live MorphDB tests
passes unchanged against the published0.12.2image (330/330).Microsoft.NET.Test.Sdkto 18.10.1 (tests only).