Commits
Each publish event now writes a fresh `lol.mlf.package/<tid>` record
instead of clobbering a single `com.atproto.lexicon.schema/lol.mlf.package`.
The schema definition and the manifest instance are different things
in different collections, so they stop colliding when an mlf project
dogfoods `lol.mlf.package` as a user lexicon.
- mlf-atproto: new tid module (sortable base32 TID generator, CAS
loop for monotonicity)
- mlf-publish/manifest: drop the schema envelope; instance carries
`$type = "lol.mlf.package"` and only instance fields
- mlf-cli/publish: write manifest to `lol.mlf.package/<tid>`
- mlf-cli/unpublish: list the manifest collection, pick the newest
by TID, delete everything it lists + the full publish log
Moves third-party deps used by more than one crate into the workspace
table. Members reference them with '<name>.workspace = true' (or the
extended form when they need extra features), so version bumps happen
in one place.
Single-use deps stay in their member Cargo.toml.
Add a "Plugins" section to the docs with two subsections — "DNS
Providers" and "Code Generators" — and one page per plugin. Each DNS
page covers: install command, options-schema fields, provider-specific
account setup (IAM policy for route53, token scopes for cloudflare,
per-domain API toggle for porkbun, tier requirement for godaddy, IP
whitelist + replace-everything caveat for namecheap, service-account
roles + ADC fallback for google), interactive + non-interactive login
invocations, and documented quirks. Each codegen page covers: usage,
the full type-mapping table, field-syntax / optional-vs-required
handling, doc-comment preservation, and known gaps.
Template changes: page.html and section.html both render a two-level
nested sidebar (docs section → subsection → subsubsection), with
clickable titles at each level. Previously only one level of nesting
was supported. A small SCSS block adds nav-subsection styles that
match the existing nav-section look at a deeper indent.
37 total pages in the docs tree, up from 26.
Dogfood: describe the publish manifest record type using MLF itself.
lexicons/lol/mlf/package.mlf defines a `record package` with a
`self {}` doc block carrying the lexicon-level description, plus two
supporting object types (PublishedItem and ResolvedDependency) that
keep the record's array shapes readable.
Repo-root mlf.toml declares [package].name = "lol.mlf", points
[source] at ./lexicons, and configures a single [[output]] for rust
writing to ./mlf-generated-lexicon/src/generated. We deliberately
don't emit lexicon JSON to the repo — the publish flow produces that
shape when it actually ships records to a PDS; keeping a static copy
would just be a stale duplicate.
Generated code lives in its own workspace crate, mlf-generated-lexicon.
Its src/lib.rs stitches each generated file under a module path that
mirrors the lexicon NSID (so lol.mlf.package shows up as
mlf_generated_lexicon::lol::mlf::package). Crates that want typed
access depend on it like any other workspace member — no build-script
magic, no include! from "up here".
mlf-publish::manifest imports Package / PublishedItem /
ResolvedDependency from the new crate and builds the instance payload
via serde instead of hand-rolled JSON macros. Any drift between the
.mlf schema and the code that builds manifests now surfaces at
compile time. The `com.atproto.lexicon.schema` envelope fields
($type, lexicon, id, description, defs) stay hand-assembled: the
atproto meta-schema only requires `lexicon: integer`, so there's
nothing useful to generate a type from, and the envelope shape is
guarded by the check_meta_schema validator.
Auth via a service-account JSON key (content, not a file path) or
Application Default Credentials when the plugin runs on GCE/Cloud Run
with attached identity. Uses gcp_auth to fetch bearer tokens per op.
Cloud DNS addresses records by (managedZone, name, type); the atomic
Changes endpoint takes additions+deletions in one request, so upsert
sends delete-old + add-new together. Zone resolution walks parent
labels against /projects/{id}/managedZones and matches against the
zone's dnsName (trailing-dot-aware).
Options schema: project_id (non-secret, required) and
service_account_json (secret, optional, falls back to ADC when
omitted).
Namecheap's XML-over-GET legacy API. Every call hits
xml.response with ApiUser/ApiKey/UserName/ClientIp params; production
access requires whitelisting the caller's IP in Namecheap's admin
panel (the plugin surfaces the common "IP is not in the whitelist"
response cleanly as IpNotWhitelisted rather than an opaque HTTP 200
with an error body).
setHosts replaces every DNS record on the zone, so upsert/delete
round-trip through getHosts, modify-in-memory, setHosts. Zone lookup
walks parent domains against domains.getList. Options schema takes
api_user, api_key, user_name (defaults to api_user), and a
required non-secret client_ip so the user can set it correctly per
environment.
Uses GoDaddy's Authorization: sso-key KEY:SECRET header. Records are
addressed by (domain, type, relative-name); apex records use @ per
GoDaddy's convention. PUT /domains/{z}/records/TXT/{name} replaces
every record at (type, name) — exactly the single-value shape the
_lexicon TXT needs. login does a cheap GET /domains?limit=1 to
validate credentials and surface 403s from hobbyist-tier accounts
cleanly.
Porkbun's API is POST-only with apikey + secretapikey carried in the
request body. Zone lookup walks parent labels against /domain/listAll
since Porkbun has no explicit "which zone covers this name" endpoint.
TXT ops use the retrieveByNameType / editByNameType / deleteByNameType
variants — editByNameType replaces every record at (subdomain, TXT)
in one call, which matches the single-value _lexicon shape exactly.
login pings /ping to verify the credentials and report the caller's
whitelisted IP.
A workspace root is an mlf.toml with a [workspace] section and no
[package] — members = ["app", "shared"] lists per-member
subdirectories, each with its own [package]. Settings declared under
[workspace.publish] and [workspace.dependencies] cascade into any
member that doesn't override them (section-level replacement, not
field-level merging).
ProjectKind::load distinguishes the two shapes and returns either a
single ResolvedPackage or the full member list. mlf publish dispatches
based on that: single package → existing flow; workspace → publishes
every member in declaration order. --package <name> filters to one
member by its [package].name.
Each member runs through the full validator + DNS reconcile + apply +
manifest pipeline independently — every member produces its own
lol.mlf.package record. RemoteState::load_at lets the orchestrator
target a specific package root without relying on CWD, so running
from the workspace root publishes every member cleanly.
Second official DNS plugin, proving the subprocess protocol is
provider-agnostic. Wraps aws-sdk-route53 for the four ops — zone
lookup by DNS name (walks parent domains and hits
list_hosted_zones_by_name), list_txt, upsert_txt, delete_txt. Route 53
lacks per-record IDs so (name, type) is the identity; upserts use a
single UPSERT ChangeResourceRecordSets. Credential schema: access_key
+ secret_key required, session_token (for STS) and region
(default us-east-1) optional.
TXT values are quoted + escape-sequence'd on the wire; a hand-rolled
unquote handles the trailing \" edge case that a naive
trim_matches would corrupt.
Deletes every lexicon the workspace published, using the lol.mlf.package
manifest as the list of NSIDs — so we never guess which records on the
PDS belong to this workspace. Refuses to proceed if no manifest exists.
After record deletes, the manifest record itself is removed.
Interactive confirmation by default; --yes skips. Each deleteRecord
is idempotent (already-gone records are logged and skipped). Records
whose NSID isn't a descendant of [package].name are skipped even if
they appear in the manifest — guards against a hand-edited manifest
that names foreign NSIDs. DNS TXT records are intentionally left in
place so re-publishing doesn't require re-provisioning DNS.
New pure-logic crate mlf-publish with four modules:
- breaking: detect removed fields, changed types, optional→required
transitions, and removed defs between the remote record and the
local one. Matches indigo/lexlint's rules.
- validate: meta-schema check (is this a structurally-valid
com.atproto.lexicon.schema record?) and scope check (is every local
NSID under [package].name?). Both return Vec<Finding>.
- manifest: build the lol.mlf.package record with sorted
published/resolvedDependencies arrays + publishedAt/tool provenance.
The record's own CID (computed by the PDS on putRecord) is the
durable identifier for this publish event.
- plan: compute the minimal action set (Put/Update/Delete) from local
vs remote CID maps, ignoring remote records outside the package's
scope.
MlfConfig gains a [publish] section: enabled (default true), dns
plugin name (required when publishing), manifest toggle (default true),
and a breaking_changes policy (deny/warn/allow, default deny).
mlf-cli grows a publish command that wires everything together:
loads workspace + remote state (from R3), runs validators, computes
the plan, optionally stops on --dry-run, reconciles DNS via the
configured plugin (creating missing TXT records, refusing to overwrite
mismatched ones without --force), authenticates against the PDS, and
applies the actions. --non-interactive fails rather than prompting;
--force overrides breaking-change aborts and DNS mismatches.
Ephemeral --pds-<field> / --dns-<plugin>-<field> overrides let CI
publish in one shot without a login step.
Plaintext TOML credential storage at ~/.config/mlf/credentials.toml
(global) and <workspace>/.mlf/credentials.toml (project-local, opt-in
via --project, meant for CI). Files are written mode 0600 on Unix.
One [pds] section carrying handle + app_password + did/pds cache; one
[dns.<plugin>] section per configured DNS provider whose fields come
from the plugin's options_schema and are preserved on round-trip.
mlf login [pds|dns <plugin>] validates credentials before writing —
PDS login runs handle→DID→PDS resolution via mlf-atproto::identity
(adding resolve_handle_to_did with DNS+well-known fallback) and calls
createSession; DNS plugin login spawns the plugin binary, collects
schema fields via --flag=value → prompt fallback, forwards them via
init, and asks the plugin to validate via its login op. Credentials
returned from login replace whatever we collected so plugins can
normalise them.
mlf logout [pds|dns [<plugin>]] mirrors the login tree and rewrites
the credentials file minus the targeted sections. --non-interactive
on login errors with the exact flag name for any missing field
instead of prompting.
Create the subprocess plugin protocol and the runtime that drives
plugins from the host side. Line-delimited JSON over stdin/stdout:
hello handshake (with options_schema, capabilities), init (for
host-assembled credentials), typed ops per plugin kind, and a
multi-turn ask/answer/ack mechanism for flows that can't reduce to a
simple schema (OAuth device codes, magic-link confirmations). The host
does capability gating before sending ops and refuses ask messages
under a DenyInteractiveUi for --non-interactive callers.
Plugin-side scaffolding lives in plugin.rs as a small Server that
binds to stdio and wraps the per-message bookkeeping, so individual
plugins implement only their op dispatch.
Ship mlf-dns-cloudflare as the first real plugin — a binary that
translates the five DNS ops (login/resolve_zone/list_txt/upsert_txt/
delete_txt) into Cloudflare API calls via reqwest. Options schema is
one secret field, api_token. Zone lookup walks parent domains so
_lexicon.forum.example.com finds the example.com zone automatically.
End-to-end tests use tokio::io::duplex pipes to wire a Server and a
PluginHandle to each other in-process and cover the handshake,
capability gating, happy-path round-trips, multi-turn flows,
non-interactive refusal, and early-exit handling.
Introduce mlf-cli/src/remote_state.rs, a shared pipeline that walks
the workspace, generates Lexicon JSON for every module, wraps each as
a com.atproto.lexicon.schema record, computes its DAG-CBOR CID, and
fetches whatever's currently published on the PDS. classify() sorts
every NSID into Unchanged/New/Changed/Removed; remote records outside
[package].name are left alone so shared-account-per-package setups
don't get stepped on. mlf status renders the summary plus per-authority
DNS readiness; mlf diff prints unified JSON diffs via the similar
crate for any NSID the status would touch.
Also fixes a spec-compliance gap in mlf-atproto::identity: per the
ATProto lexicon spec, an NSID's publishing authority is the NSID
minus the final name segment (so com.example.forum.post and
com.example.forum.thread share _lexicon.forum.example.com).
authority_of_nsid + authority_dns_name + RealDnsResolver::resolve_authority_did
implement the correct drop-final rule used by the new status/diff
path; the fetcher's existing helpers are kept unchanged for
backwards compatibility.
Introduce a required [package] section whose name field declares the
package's NSID prefix. Every .mlf file's namespace must be equal to or
descended from it; a mis-scoped file is a miette-rendered error in
mlf check rather than a silent pass. mlf init prompts for the name
interactively (and derives a placeholder from the directory under
--yes); mlf fetch's implicit mlf.toml auto-create is dropped since a
package name would need its own prompt — users mlf init first.
Split protocol-level primitives (DID/NSID/DNS identity resolution,
generic XRPC HTTP client, session management, record CRUD wrappers,
client-side DAG-CBOR CID computation) out of mlf-lexicon-fetcher into
a new mlf-atproto crate. The fetcher is refactored to consume it; its
public API is preserved. This is the plumbing foundation for
mlf-publish, mlf-plugin-host, and the DNS provider plugin binaries.
Files usually have newlines at the end. Especially when they are
edited manually. In order to prevent pure whitespace changes on
newline vs. no newline at the end of the file, always add a newline.
If a type definition has a doc domment, it wasn't included as description
of that type.
Example:
```
/// A forum thread
record thread {
title!: title,
}
/// Thread title
def type title = string constrained {
maxLength: 200,
minLength: 1,
};
```
Would lead to just
```json
"title": {
"type": "string",
"maxLength": 200,
"minLength": 1
}
```
With this commit it contains the `Thread title` as description:
```
"title": {
"type": "string",
"description": "Thread title",
"maxLength": 200,
"minLength": 1
}
`mlf generate lexicon` has a `--flat` option to generate a lexicon file
without any nesting. So intead of generating `com/example/forum/thread.json`,
it generates `com.example.forum.thread.json`.
This commit adds the same `--flat` option to generate MLF files via
`mlf generate mlf`, hence generating `com.example.forum.thread.mlf`
instead of `com.example.forum.thread.mlf`.
The share button didn't include the file path, hence you couldn't
correctly share the name of your Lexicon. Now you can.
Each publish event now writes a fresh `lol.mlf.package/<tid>` record
instead of clobbering a single `com.atproto.lexicon.schema/lol.mlf.package`.
The schema definition and the manifest instance are different things
in different collections, so they stop colliding when an mlf project
dogfoods `lol.mlf.package` as a user lexicon.
- mlf-atproto: new tid module (sortable base32 TID generator, CAS
loop for monotonicity)
- mlf-publish/manifest: drop the schema envelope; instance carries
`$type = "lol.mlf.package"` and only instance fields
- mlf-cli/publish: write manifest to `lol.mlf.package/<tid>`
- mlf-cli/unpublish: list the manifest collection, pick the newest
by TID, delete everything it lists + the full publish log
Add a "Plugins" section to the docs with two subsections — "DNS
Providers" and "Code Generators" — and one page per plugin. Each DNS
page covers: install command, options-schema fields, provider-specific
account setup (IAM policy for route53, token scopes for cloudflare,
per-domain API toggle for porkbun, tier requirement for godaddy, IP
whitelist + replace-everything caveat for namecheap, service-account
roles + ADC fallback for google), interactive + non-interactive login
invocations, and documented quirks. Each codegen page covers: usage,
the full type-mapping table, field-syntax / optional-vs-required
handling, doc-comment preservation, and known gaps.
Template changes: page.html and section.html both render a two-level
nested sidebar (docs section → subsection → subsubsection), with
clickable titles at each level. Previously only one level of nesting
was supported. A small SCSS block adds nav-subsection styles that
match the existing nav-section look at a deeper indent.
37 total pages in the docs tree, up from 26.
Dogfood: describe the publish manifest record type using MLF itself.
lexicons/lol/mlf/package.mlf defines a `record package` with a
`self {}` doc block carrying the lexicon-level description, plus two
supporting object types (PublishedItem and ResolvedDependency) that
keep the record's array shapes readable.
Repo-root mlf.toml declares [package].name = "lol.mlf", points
[source] at ./lexicons, and configures a single [[output]] for rust
writing to ./mlf-generated-lexicon/src/generated. We deliberately
don't emit lexicon JSON to the repo — the publish flow produces that
shape when it actually ships records to a PDS; keeping a static copy
would just be a stale duplicate.
Generated code lives in its own workspace crate, mlf-generated-lexicon.
Its src/lib.rs stitches each generated file under a module path that
mirrors the lexicon NSID (so lol.mlf.package shows up as
mlf_generated_lexicon::lol::mlf::package). Crates that want typed
access depend on it like any other workspace member — no build-script
magic, no include! from "up here".
mlf-publish::manifest imports Package / PublishedItem /
ResolvedDependency from the new crate and builds the instance payload
via serde instead of hand-rolled JSON macros. Any drift between the
.mlf schema and the code that builds manifests now surfaces at
compile time. The `com.atproto.lexicon.schema` envelope fields
($type, lexicon, id, description, defs) stay hand-assembled: the
atproto meta-schema only requires `lexicon: integer`, so there's
nothing useful to generate a type from, and the envelope shape is
guarded by the check_meta_schema validator.
Auth via a service-account JSON key (content, not a file path) or
Application Default Credentials when the plugin runs on GCE/Cloud Run
with attached identity. Uses gcp_auth to fetch bearer tokens per op.
Cloud DNS addresses records by (managedZone, name, type); the atomic
Changes endpoint takes additions+deletions in one request, so upsert
sends delete-old + add-new together. Zone resolution walks parent
labels against /projects/{id}/managedZones and matches against the
zone's dnsName (trailing-dot-aware).
Options schema: project_id (non-secret, required) and
service_account_json (secret, optional, falls back to ADC when
omitted).
Namecheap's XML-over-GET legacy API. Every call hits
xml.response with ApiUser/ApiKey/UserName/ClientIp params; production
access requires whitelisting the caller's IP in Namecheap's admin
panel (the plugin surfaces the common "IP is not in the whitelist"
response cleanly as IpNotWhitelisted rather than an opaque HTTP 200
with an error body).
setHosts replaces every DNS record on the zone, so upsert/delete
round-trip through getHosts, modify-in-memory, setHosts. Zone lookup
walks parent domains against domains.getList. Options schema takes
api_user, api_key, user_name (defaults to api_user), and a
required non-secret client_ip so the user can set it correctly per
environment.
Uses GoDaddy's Authorization: sso-key KEY:SECRET header. Records are
addressed by (domain, type, relative-name); apex records use @ per
GoDaddy's convention. PUT /domains/{z}/records/TXT/{name} replaces
every record at (type, name) — exactly the single-value shape the
_lexicon TXT needs. login does a cheap GET /domains?limit=1 to
validate credentials and surface 403s from hobbyist-tier accounts
cleanly.
Porkbun's API is POST-only with apikey + secretapikey carried in the
request body. Zone lookup walks parent labels against /domain/listAll
since Porkbun has no explicit "which zone covers this name" endpoint.
TXT ops use the retrieveByNameType / editByNameType / deleteByNameType
variants — editByNameType replaces every record at (subdomain, TXT)
in one call, which matches the single-value _lexicon shape exactly.
login pings /ping to verify the credentials and report the caller's
whitelisted IP.
A workspace root is an mlf.toml with a [workspace] section and no
[package] — members = ["app", "shared"] lists per-member
subdirectories, each with its own [package]. Settings declared under
[workspace.publish] and [workspace.dependencies] cascade into any
member that doesn't override them (section-level replacement, not
field-level merging).
ProjectKind::load distinguishes the two shapes and returns either a
single ResolvedPackage or the full member list. mlf publish dispatches
based on that: single package → existing flow; workspace → publishes
every member in declaration order. --package <name> filters to one
member by its [package].name.
Each member runs through the full validator + DNS reconcile + apply +
manifest pipeline independently — every member produces its own
lol.mlf.package record. RemoteState::load_at lets the orchestrator
target a specific package root without relying on CWD, so running
from the workspace root publishes every member cleanly.
Second official DNS plugin, proving the subprocess protocol is
provider-agnostic. Wraps aws-sdk-route53 for the four ops — zone
lookup by DNS name (walks parent domains and hits
list_hosted_zones_by_name), list_txt, upsert_txt, delete_txt. Route 53
lacks per-record IDs so (name, type) is the identity; upserts use a
single UPSERT ChangeResourceRecordSets. Credential schema: access_key
+ secret_key required, session_token (for STS) and region
(default us-east-1) optional.
TXT values are quoted + escape-sequence'd on the wire; a hand-rolled
unquote handles the trailing \" edge case that a naive
trim_matches would corrupt.
Deletes every lexicon the workspace published, using the lol.mlf.package
manifest as the list of NSIDs — so we never guess which records on the
PDS belong to this workspace. Refuses to proceed if no manifest exists.
After record deletes, the manifest record itself is removed.
Interactive confirmation by default; --yes skips. Each deleteRecord
is idempotent (already-gone records are logged and skipped). Records
whose NSID isn't a descendant of [package].name are skipped even if
they appear in the manifest — guards against a hand-edited manifest
that names foreign NSIDs. DNS TXT records are intentionally left in
place so re-publishing doesn't require re-provisioning DNS.
New pure-logic crate mlf-publish with four modules:
- breaking: detect removed fields, changed types, optional→required
transitions, and removed defs between the remote record and the
local one. Matches indigo/lexlint's rules.
- validate: meta-schema check (is this a structurally-valid
com.atproto.lexicon.schema record?) and scope check (is every local
NSID under [package].name?). Both return Vec<Finding>.
- manifest: build the lol.mlf.package record with sorted
published/resolvedDependencies arrays + publishedAt/tool provenance.
The record's own CID (computed by the PDS on putRecord) is the
durable identifier for this publish event.
- plan: compute the minimal action set (Put/Update/Delete) from local
vs remote CID maps, ignoring remote records outside the package's
scope.
MlfConfig gains a [publish] section: enabled (default true), dns
plugin name (required when publishing), manifest toggle (default true),
and a breaking_changes policy (deny/warn/allow, default deny).
mlf-cli grows a publish command that wires everything together:
loads workspace + remote state (from R3), runs validators, computes
the plan, optionally stops on --dry-run, reconciles DNS via the
configured plugin (creating missing TXT records, refusing to overwrite
mismatched ones without --force), authenticates against the PDS, and
applies the actions. --non-interactive fails rather than prompting;
--force overrides breaking-change aborts and DNS mismatches.
Ephemeral --pds-<field> / --dns-<plugin>-<field> overrides let CI
publish in one shot without a login step.
Plaintext TOML credential storage at ~/.config/mlf/credentials.toml
(global) and <workspace>/.mlf/credentials.toml (project-local, opt-in
via --project, meant for CI). Files are written mode 0600 on Unix.
One [pds] section carrying handle + app_password + did/pds cache; one
[dns.<plugin>] section per configured DNS provider whose fields come
from the plugin's options_schema and are preserved on round-trip.
mlf login [pds|dns <plugin>] validates credentials before writing —
PDS login runs handle→DID→PDS resolution via mlf-atproto::identity
(adding resolve_handle_to_did with DNS+well-known fallback) and calls
createSession; DNS plugin login spawns the plugin binary, collects
schema fields via --flag=value → prompt fallback, forwards them via
init, and asks the plugin to validate via its login op. Credentials
returned from login replace whatever we collected so plugins can
normalise them.
mlf logout [pds|dns [<plugin>]] mirrors the login tree and rewrites
the credentials file minus the targeted sections. --non-interactive
on login errors with the exact flag name for any missing field
instead of prompting.
Create the subprocess plugin protocol and the runtime that drives
plugins from the host side. Line-delimited JSON over stdin/stdout:
hello handshake (with options_schema, capabilities), init (for
host-assembled credentials), typed ops per plugin kind, and a
multi-turn ask/answer/ack mechanism for flows that can't reduce to a
simple schema (OAuth device codes, magic-link confirmations). The host
does capability gating before sending ops and refuses ask messages
under a DenyInteractiveUi for --non-interactive callers.
Plugin-side scaffolding lives in plugin.rs as a small Server that
binds to stdio and wraps the per-message bookkeeping, so individual
plugins implement only their op dispatch.
Ship mlf-dns-cloudflare as the first real plugin — a binary that
translates the five DNS ops (login/resolve_zone/list_txt/upsert_txt/
delete_txt) into Cloudflare API calls via reqwest. Options schema is
one secret field, api_token. Zone lookup walks parent domains so
_lexicon.forum.example.com finds the example.com zone automatically.
End-to-end tests use tokio::io::duplex pipes to wire a Server and a
PluginHandle to each other in-process and cover the handshake,
capability gating, happy-path round-trips, multi-turn flows,
non-interactive refusal, and early-exit handling.
Introduce mlf-cli/src/remote_state.rs, a shared pipeline that walks
the workspace, generates Lexicon JSON for every module, wraps each as
a com.atproto.lexicon.schema record, computes its DAG-CBOR CID, and
fetches whatever's currently published on the PDS. classify() sorts
every NSID into Unchanged/New/Changed/Removed; remote records outside
[package].name are left alone so shared-account-per-package setups
don't get stepped on. mlf status renders the summary plus per-authority
DNS readiness; mlf diff prints unified JSON diffs via the similar
crate for any NSID the status would touch.
Also fixes a spec-compliance gap in mlf-atproto::identity: per the
ATProto lexicon spec, an NSID's publishing authority is the NSID
minus the final name segment (so com.example.forum.post and
com.example.forum.thread share _lexicon.forum.example.com).
authority_of_nsid + authority_dns_name + RealDnsResolver::resolve_authority_did
implement the correct drop-final rule used by the new status/diff
path; the fetcher's existing helpers are kept unchanged for
backwards compatibility.
Introduce a required [package] section whose name field declares the
package's NSID prefix. Every .mlf file's namespace must be equal to or
descended from it; a mis-scoped file is a miette-rendered error in
mlf check rather than a silent pass. mlf init prompts for the name
interactively (and derives a placeholder from the directory under
--yes); mlf fetch's implicit mlf.toml auto-create is dropped since a
package name would need its own prompt — users mlf init first.
Split protocol-level primitives (DID/NSID/DNS identity resolution,
generic XRPC HTTP client, session management, record CRUD wrappers,
client-side DAG-CBOR CID computation) out of mlf-lexicon-fetcher into
a new mlf-atproto crate. The fetcher is refactored to consume it; its
public API is preserved. This is the plumbing foundation for
mlf-publish, mlf-plugin-host, and the DNS provider plugin binaries.
If a type definition has a doc domment, it wasn't included as description
of that type.
Example:
```
/// A forum thread
record thread {
title!: title,
}
/// Thread title
def type title = string constrained {
maxLength: 200,
minLength: 1,
};
```
Would lead to just
```json
"title": {
"type": "string",
"maxLength": 200,
"minLength": 1
}
```
With this commit it contains the `Thread title` as description:
```
"title": {
"type": "string",
"description": "Thread title",
"maxLength": 200,
"minLength": 1
}
`mlf generate lexicon` has a `--flat` option to generate a lexicon file
without any nesting. So intead of generating `com/example/forum/thread.json`,
it generates `com.example.forum.thread.json`.
This commit adds the same `--flat` option to generate MLF files via
`mlf generate mlf`, hence generating `com.example.forum.thread.mlf`
instead of `com.example.forum.thread.mlf`.