AI Decision System
Replayable, offline-verifiable records for AI-assisted go/no-go decisions: experiment promotions, typed model answers and human-approved actions.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기메이커가 쓴 소개는 위의 한 줄이 전부입니다. 더 자세한 내용은 저장소 README에서 읽을 수 있습니다. 저장소 README ↗
README 전체를 불러오는 중입니다.
README 전문 보기 ↗Animated, steppable explainers for code changes. A zero-dependency JS port of
manim's core model that renders to SVG: a live DOM in the browser, strings in
Cloudflare Workers or Node. Every step is addressable (?scene=&step=), and
every scene has a text form and a structured trace for agents.
git clone https://github.com/ivarvong/lantern && cd lantern # not on npm yet; zero runtime dependencies
Then:
The repo is TypeScript throughout. Node 24 runs the tools and tests as .ts directly
(type stripping, so only erasable syntax); tsc compiles the library to dist/.
npm install
npm run dev # http://127.0.0.1:5173/?scene=pyex-limits, reloads on save, keeps your step
npm test # ~8 s; FULL=1 (npm run test:full) checks every frame
npm run check # tsc over every project, emits dist/
A scene is a class with an async construct(). Steps are markers; this.state is
what the explanation is about, and it is what traces record:
import { Scene, Rectangle, Text, Labeled, Arrow, FadeIn, Create, RIGHT, TEAL, GOLD } from '../src/index.ts';
export class CacheScene extends Scene {
override async construct() {
const st = (this.state = { cache: false });
await this.step('before');
const handler = new Labeled(new Rectangle({ width: 3, height: 1, stroke: TEAL }), new Text('handler'));
const db = new Labeled(new Rectangle({ width: 2, height: 1, stroke: GOLD }), new Text('D1'));
db.nextTo(handler, RIGHT, 2);
await this.play(new FadeIn(handler), new FadeIn(db));
await this.play(new Create(new Arrow(handler.shape.right(), db.shape.left(), { buff: 0.1 })));
await this.step('after', { pr: 482 });
st.cache = true;
// ...
}
}
Register it in scenes/index.ts, then:
npm run see -- my-scene after # the frame as text, plus the step's state diff
npm run shot -- my-scene # contact sheet: the end of every step, as a PNG
npm run verify:motion -- my-scene # teleports, pops, cuts, flicker
Diagrams can be text: flowchart(mermaidSource) parses a Mermaid flowchart, lays it out
and returns shapes you can animate (docs/FLOWCHART.md). Scenes can also be
data: sceneFromDoc(doc) plays a JSON document of nodes, edges,
code and ops (docs/DOC-FORMAT.md).
Float32Array point buffers (stride 2) plus a style record.
Every geometric operation maps a function over the family's points: points are the truth.[anchor, (h1, h2, anchor)*], so curve
n starts at point 3n. The serializer and the alignment code share that one contract.Write, and Transform between strings morphs glyph to glyph.
Nothing depends on installed fonts, so every rasterizer draws the same thing.begin().
Transform first aligns structure (null copies, de Casteljau subdivision) so both ends
have equal shape; each frame is then a lerp, field by field, style included.dt = 1/fps and hands itself to a sink, which decides
what a frame is: StringSink collects SVG, DomSink patches a live <svg> on rAF time,
MotionSink samples boxes for the verifier, TraceSink records state at step boundaries.
Output is deterministic.t with frames skipped (manim's skip_animations): the
loop is fixed-dt and the endpoints are fixed at begin(), so a seek is byte-identical to a
full run. Tests assert this for every scene.Coordinates are manim's: the frame is 16 × 9 units, origin at the center, y up
(UP = [0, 1], angles counterclockwise). The flip to SVG's y-down happens in one place,
where path data is written (VMobject.roundedRelative).
src/ the library: no dependencies, no runtime assumptions (browser, Workers, Node)
index.ts the public API, curated, types included; anything not exported here is internal
dist/ src/ compiled by tsc: ESM .js, .d.ts, maps. What the package exports (npm run build:lib)
scenes/ example scenes and the registry (index.ts)
app/ the player (player.ts, DomSink) and the Worker (worker.ts): the only runtime-specific code
tools/ dev server, build, inspection, verifiers, perf tools (Node; Chromium via playwright-core)
test/ node --test; golden-traces.json hashes each scene's trace (UPDATE_GOLDEN=1 to bless)
vendor/ the glyph shaper bundle (tools/vendor-glyph.ts), its types, and glyph.pin.json (sha256, checked in CI)
docs/ PERF.md, VERIFY.md, DOC-FORMAT.md
| how | |
|---|---|
| live, in a page | app/player.ts: ?scene=&step= deep links, step mode, copy link |
| one frame as SVG | renderAt(Scene, t, new SnapshotSink({ width, background })), Worker GET /frame.svg, or POST /render |
| one frame as text | renderText(scene), npm run see, or Worker GET /text |
| one frame as elements | renderElements(scene), or Worker GET /elements.json |
| state at every step | traceScene(Scene), or Worker GET /trace.json |
| steps and duration | indexScene(Scene), or Worker GET /index.json |
| every frame | new StringSink({ width, background }), Worker POST /render with "format": "frames", or npm run render |
Deploy: npm run deploy (builds out/site/, then cf deploy). npm run worker:dry-run builds and validates the Worker without credentials or an upload; npm run worker:dev serves the Worker and player locally with cf dev. Worker settings live in cloudflare.config.ts; vite.config.ts points the static assets at out/site/.
The Worker in app/ exposes the library. A registered demo is a GET. A scene you describe in JSON is a POST, and it can use the same shapes, animations, and outputs as a class in TypeScript. The browser player (DomSink) is the live view; HTTP returns strings.
POST /render takes { "scene", "output" }. POST /frame.svg, /text, /elements.json, /trace.json, and /index.json take the scene itself, and the path chooses the output. Query step, t, w, and background work on those shortcuts the same way they do on a GET.
{
"scene": {
"fps": 4,
"objects": [
{ "id": "box", "kind": "square", "side": 1.4, "style": { "stroke": "teal", "fill": "teal", "fillOpacity": 0.15 } },
{ "id": "label", "kind": "text", "text": "cache", "fontSize": 0.35 }
],
"steps": [
{ "name": "in", "plays": [{ "animations": [{ "kind": "fadeIn", "target": "box" }, { "kind": "write", "target": "label" }], "runTime": 0.5 }] }
]
},
"output": { "format": "svg", "width": 640, "background": "#112233", "select": { "kind": "end" } }
}
output.format is svg, frames, text, elements, trace, or index. svg and frames take width (an integer from 32 to 1280, default 1280) and background (default #1a1a1a). Those are the SvgOptions fields. The serializer itself accepts an integer width from 1 to 4096 and a #rgb or #rrggbb background, and rejects any other value before it is written. svg, text, and elements take select: { "kind": "end" }, { "kind": "time", "t": 1.5 }, or { "kind": "step", "name": "in" }. frames returns every frame, up to 40. trace and index describe the whole scene.
A colour is a palette name (white, black, grey, grey_d, blue, blue_d, teal, green, yellow, gold, red, maroon, purple, orange) or #rgb / #rrggbb. none is a paint, not a background. Object kinds are rectangle, square, circle, dot, line, polygon, arc, arcBetween, arrow, curvedArrow, text, decimal, code, codePanel, title, readout, group, labeled, card, flowchart, and route. Animation kinds are fadeIn, fadeOut, create, write, indicate, flash, transform, moveAlong, animate, focus, unfocus, group, and lagged. An animate call is shift, moveTo, scale, rotate, setFill, setStroke, or setOpacity. Rates are linear, smooth, rushInto, rushFrom, thereAndBack, easeInQuad, easeOutQuad, easeOutBack, and wiggle.
Flowchart paint comes from theme. The source must not use style, classDef, linkStyle, or :::.
The body is at most 64 KiB. A scene has at most 40 objects, 24 steps, and 180 frames (40 when the output is frames). A request that fails those checks is a 400 { "error", "errors": [{ "path", "message" }], "help": "/llms.txt" }. An unknown demo is a 404. A body over the limit is a 413.
Responses send access-control-allow-origin: *. The API is unauthenticated and sets no cookies; a response is a public demo or the caller's own render, so any origin may read it. Preflight allows content-type and accept. SVG responses also send x-content-type-options: nosniff and content-security-policy: default-src 'none'; style-src 'unsafe-inline'; script-src 'none'. POST responses are no-store. GET frames from the registry stay public, max-age=3600.
GET /llms.txt is the same contract, with the demo names and their steps.
npm run dev | source tree as native ES modules on 127.0.0.1, live reload |
npm run build | single-file player pages in out/ (esbuild, ~80 ms) |
npm test / test:full | sampled frames / every frame |
npm run check | tsc -b over every project (library, app, tools and tests), emit dist/, compile a TypeScript consumer against it |
npm run build:lib | dist/ from scratch; prepack runs it |
npm run verify | motion verifier, then the playback gate in Chromium (docs/VERIFY.md) |
npm run bench | every scene timed against bench/baseline.json; fails on a reproduced regression |
MANIM_DIR=… npm test | also diffs rate functions and curve subdivision against manim's own code |
npm run see, shot, render | look at a scene: text, contact sheet, SVG frames |
npm run trace, profile, seek, ab, hitch | performance (docs/PERF.md) |
npm run fonts | re-subset the embedded fonts (needs pip install fonttools brotli) |
TransformMatchingShapes): glyphs pair by index, not by shape.transform attributes: every move rewrites points.