@miadi/annotate-core
v0.2.0
Published
Annotate a document by block or by exact words, signed, and hand the review on, without a framework: the vocabulary, the annotator's behaviour as plain functions, quote and author resolution, an HTML-string renderer, a vanilla DOM mount with the iOS-safe
Readme
@miadi/annotate-core
Annotate a document by block or by exact words, sign each mark, see everyone's marks, and hand the review on. No framework, no dependencies.
In the Miadi stack, this is where a person's judgment on agent work is recorded. A reviewer marks the parts of a reply or a document that matter, each mark says who made it, and the finished review can be handed back to the agent that wrote the text.
Works with:
@miadi/annotate-ui: the React host over the same core. It renders the same markup, and a parity test holds the two together.@miadi/tide: fetches an agent's reply from a terminal pane and delivers the review back to it.
Entry points:
@miadi/annotate-core: the vocabulary, the annotator's state model,resolveQuote,resolveAuthor, andrenderAnnotatorHtml, an HTML-string renderer@miadi/annotate-core/dom:mountAnnotatorfor a page without a framework, pluscaptureSelection,autoGrowandauthorMemory@miadi/annotate-core/http:httpAnnotateTransport, a transport over routes the host names@miadi/annotate-core/styles.css: the stylesheet. Colours come from the theme as CSS custom properties.
Nothing here holds a token or knows a URL of its own.
A page without a framework
<link rel="stylesheet" href="/vendor/annotate-core/annotator.css">
<div id="annotator"></div>
<script type="module">
import { mountAnnotator } from "/vendor/annotate-core/dom.js";
import { httpAnnotateTransport } from "/vendor/annotate-core/http.js";
const transport = httpAnnotateTransport({
urls: {
load: "/api/annotate/load",
mark: "/api/annotate/mark",
marks: (doc) => `/api/annotate/marks?doc=${encodeURIComponent(doc)}`,
},
});
const annotator = mountAnnotator(document.getElementById("annotator"), {
transport,
authorKey: "my-app.annotate.author",
});
annotator.load();
</script>dist/ imports its siblings with relative .js paths, so serving the package's
dist/ directory as static files is enough (express.static(dirname + "/dist")).
The transport
interface AnnotateTransport {
load(): Promise<AnnotateDocument>; // { id, text, blocks, reopened?, note? }
mark(request: MarkRequest): Promise<MarkOutcome>;
marks(): Promise<StoredMark[]>;
deliver?(options: { submit: boolean }): Promise<DeliverOutcome>;
}Write your own, or use httpAnnotateTransport. It sends { doc, ...request }
to the mark route and treats a JSON body carrying error as a failure. Without
deliver, the deliver bar is not rendered.
Selecting words on a phone
iOS clears a text selection as soon as anything outside it is touched, so a
button cannot read the selection when it is tapped. captureSelection records
it while it is made, keeps it for graceMs (default 1500) after it collapses,
and returns a QuoteSelector (exact, plus up to 32 characters of prefix and
suffix from the block). The mount wires it for you. Pass graceMs to change
the window.
On the server
resolveQuote(text, selector)returns the one range the selector names, or{ ambiguous: n }, or{ missing: true }. It never guesses.resolveAuthor({ signed, asserted, typed, entered })applies one precedence: a signed token, then an identity the host vouches for, then a valid typed author. A signed request whose body names someone else comes back withrefused, unless it saysentered: true: then the typed name is kept and the signer is recorded as the one who carried it. Evidence (tokens, headers) stays with the host.- The decision carries
basis(signed,assertedortyped: how the author was known) andby(whose request carried the mark, ornull), kept apart the way a commit keeps its author apart from its committer. A host stores them on the mark asauthor_basis,author_byandauthor_by_name; the annotator shows a typed name as "Ava (entered by William)" or "X (not verified)". Signed in, a person reaches the typed fields with "enter another name", and the mark waits until both fields are filled.
Theme and labels
theme takes colours and a monospace stack (DEFAULT_THEME is dark, with a gold
accent). labels takes every visible string. Functions such as
deliver: (n) => ... receive their counts. Every text colour of the default theme reads at
4.5:1 on every surface, and a test holds it there.
