chore: sanitize tests, add CONTEXT.md + ADRs, track tests/docs

This commit is contained in:
tommy0103
2026-07-08 16:11:12 +08:00
parent d5d5df46fa
commit 1a34245618
19 changed files with 3880 additions and 31 deletions
@@ -0,0 +1,31 @@
# Indexing splits into a shared parse core and binding-specific persist layers
**Context.** Obelisk had two divergent full indexers — `scripts/indexer.mjs`
(791 lines, `node:sqlite`, for the skill/runtime) and `app/indexer.js` (1170
lines, `better-sqlite3`, for the Electron app) — both parsing the same Claude and
Codex JSONL into the same schema. Keeping two implementations contradicts the
"single source of truth / infra" goal, but forcing one SQLite binding is also
bad: `node:sqlite` is zero-native-dep and ideal for a clone-and-run skill
artifact, while `better-sqlite3` is the battle-tested choice inside Electron.
**Decision.** Unify at the logic layer, not the binding layer. Extract a pure
**parse core** (`jsonl -> records`) with no database dependency, and keep two
thin **persist layers** (`node:sqlite` for skill/CLI, `better-sqlite3` for the
app) that consume the same records. The parse core is a **streaming iterator**
(`parseJsonl(file, fromLine)` yielding records), not a batched `records[]`, to
preserve the existing memory-friendly line-by-line indexing and the
`lines_processed` resume-from-line semantics recorded in `index_state`.
**Two indexing modes** share this parse core and differ only in trigger:
**daemon indexing mode** (app/CLI watches and keeps the index fresh) and
**passive pull mode** (skill indexes on invocation when no daemon is active).
They never write concurrently because the passive mode detects a fresh daemon via
heartbeat markers in `index_state` (**daemon arbitration**) and skips its own
indexing.
**Consequences.** Golden tests anchor on the parse core: feed a fixture JSONL,
assert the yielded record sequence — independent of binding. The refactor's real
work is disentangling parse from persist inside the current indexers, where
prepared-statement writes are today interleaved into the parse loop. The app's
richer incremental-discovery logic must be folded *into* the shared parse core,
not dropped.
@@ -0,0 +1,26 @@
# The runtime contract is two-tier, with api-reference.md authoritative
**Context.** Before the TypeScript migration and module extraction, we need to
pin what "the contract" is so refactoring cannot silently change observable
behavior. The four verbs (`build`/`search`/`query`/`attune`) are only the entry
surface; agents actually depend on the *return shapes* of the sandbox helpers
(`search`, `overview`, `memories`, …), which are already documented in
`references/api-reference.md` and relied on by every example in
`references/query-patterns.md`. Current behavior is good and there is no reason to
change it during migration.
**Decision.** Freeze the contract in two tiers. **Tier 1 (hard freeze, golden
tests):** the four-verb CLI I/O envelope (file/args → pretty JSON on stdout,
`{error, stack}` error envelope, exit codes) and the sandbox contract (`sql()`
read-only enforcement, `attune` exposing only `remember`/`forget`, the set of
globals/helpers available inside `query`/`attune`). **Tier 2 (locked to
api-reference.md):** each helper's documented return shape — not frozen forever,
but never allowed to drift silently; contract tests assert the live shape matches
`references/api-reference.md`, so changing a helper forces a doc change plus a
deliberate version bump. `references/api-reference.md` is therefore promoted from
description to authoritative contract, and Phase 1 becomes "make it authoritative
and enforce it," not "write a new contract doc."
**Consequences.** Behavior is preserved across the TS/module refactor by
construction: the golden and contract tests fail if any observable shape moves.
The cost is that helper shapes can no longer be reshaped casually mid-migration.
@@ -0,0 +1,22 @@
# Core is authored in TypeScript, shipped as precompiled ESM JavaScript
**Context.** The extracted Obelisk Core must serve two consumers — the ESM skill
runtime (`node:sqlite`) and the CommonJS Electron app (`better-sqlite3`) — while
the skill artifact must install with **zero build step** on the user's machine
(the clone-and-run, "low-friction skill" goal). Authoring in TS gives the infra
its checkable contracts, but raises how the compiled output is shipped and which
module format it targets.
**Decision.** Author all of Core in TypeScript and compile it ahead-of-time to
**ESM JavaScript plus `.d.ts`**. The skill/CLI runtime ships the *precompiled*
ESM JS, so installing the skill never runs a build. Rather than have Core
dual-publish CJS+ESM, the Electron main process migrates to ESM at Phase 5 so it
can `import` the same compiled Core. TypeScript source is the single source of
truth; the build step lives in the main repo (`build:skill`), never on the user's
machine.
**Consequences.** A one-time ESM migration of the Electron main process (Phase 5),
in exchange for no dual-build maintenance and a single module format across skill,
CLI, and app. The shipped skill artifact contains compiled JS, not TS. The
renderer (Vue) is out of scope and stays JavaScript. Phase 3's TS baseline only
adds root tooling (package.json, tsconfig, ESLint); it does not touch the app.
@@ -0,0 +1,22 @@
# The skill artifact ships readable compiled JS, deliberately not bundled
**Context.** Obelisk reads a user's entire local Claude Code and Codex history,
so auditability is the foundation of trust: before a user lets the skill loose on
their data, they must be able to read what it does. The obvious way to shrink a
clone-and-run skill artifact is to bundle/minify Core into a single `runtime.js`,
but that ships an opaque blob into `.claude/skills` / `.agents/skills`. The
"don't drag the whole repo into the user's skills dir" concern is real but
separate — it is solved by shipping *only Core*, not by bundling.
**Decision.** The skill artifact ships **readable, non-bundled, non-minified**
compiled JavaScript emitted straight from `tsc` (module structure and comments
preserved, ~1:1 with the TypeScript source), plus `schema.sql`, `SKILL.md`, and
`references/`. It excludes `app/`, `release/`, `renderer/`, Electron code, and
`tests/`, which is what keeps it small. Bundling into one file is deliberately
rejected: it trades auditability for marginal size, the wrong trade for a
history-reading tool. The public TS source in the main repo allows cross-checking.
**Consequences.** The installed skill is a few readable files rather than one
blob; a future contributor may be tempted to "optimize" by bundling — this ADR
records that the un-bundled form is intentional. Small artifact size comes from
scoping the artifact to Core, handled by `build:skill`, not from a bundler.