Skip to content

taste

Provenance

  • Source: .spec/spexcode/.plugins/skills/taste/spec.md
  • Source SHA-256: 9cf4cbbb584223286eb2800ec97e2fcbb3ef65016925d815327276ceb44f08a8

taste

raw source

SpexCode is self-referential — we build a spec-driven tool with the spec-driven tool, so the principles that guide its development must themselves live in the spec tree, not only in a chat that can be compacted away. These are the durable "tastes" (品味) the maintainer has stated while building it. Preserve the raw intent; let the campaign distill + refine, and let the rules that should govern every agent graduate into .plugins surface:system nodes (the way [[memory-hygiene]] already did).

The principles, in the maintainer's own framing:

  1. Harness-agnostic, zero-friction adoption. The ideal path is npm install spexcode → spex init → the user launches their own claude/codex, with NO further human operation, NO pollution of global claude/codex, and NO overwrite of the user's existing CLAUDE.md / AGENTS.md content.
  2. Deterministic, simple, UNIFIED system — not tons of special cases. Favor one mechanism over many if/else branches.
  3. Spend complexity only to BUY it back. A user need earns a code change ONLY when satisfying it reduces system complexity, or you have a clever way to make it a complexity-reducer — never "one more if-else." Don't add complexity trading for a need.
  4. The project folder holds only human-readable things. Abstract internal runtime (manifests, hashes, locks) is hidden into the global store; what stays in-tree must be prose a human would accept.
  5. Self-launch is the MAIN BODY. A user on the most naive Claude Code / a directly-launched Codex — NO dashboard, NO server — must still get the FULL experience (prompt, hooks, every mechanism) through spex init/spex spec lint + materialize→auto-discovery. The dashboard is one consumer that REDUCES to that path plus a minimal governed delta. Adapting to this "unmanaged" usage is the forcing function that makes the architecture robust and unified — the point is NOT to add if-else per usage mode (that loses the forcing).
  6. Memory hygiene ([[memory-hygiene]]): never write session/role-specific content or identity markers to the project-keyed memory — by instruction, not programmatic control (no 画蛇添足).
  7. Use fresh-context agents to brainstorm, confront, debate, and divide labor — they are less overfit, carry a naive taste, and dare to challenge the architecture. They are also a complexity probe: if a fresh agent can't understand the spec↔code relationship, grasp what the code is for, or round-trip spec→code→spec, that misalignment IS the measurement. Don't drag a huge context and brute-force solo.
  8. Read the docs, read the source, run experiments. Many mechanisms are only understood after experimenting; the codex source reveals the least-convoluted, most-claude-unified implementation. Don't reason from assumption.
  9. Milestone merges, not big-bang — land work in versioned milestones so a rollback is cheap.
  10. One frontend design language; unify the icons; NO emoji (e.g. retire the attachment 📎).
  11. Keep finding behavior-equivalent but simpler / more-unified approaches, and sanity-check every change against these existing principles.
  12. Self-reference: sediment this guidance into the spec / .plugins / source so it is never lost.
  13. YATU (You As The User): measure through the real product surface a user touches, not an internal helper.
  14. Capabilities enter the ecosystem through the pillars we already stand on (git / agent harness / test framework) — before adopting a new protocol or dependency, check whether an existing pillar already delivers it indirectly. The canonical case: LSP-grade code intelligence reaches SpexCode through the harness (agents navigate code; the LLM judge reads semantics) and symbol-level history through git (log -L, hunk-header funcname drivers) — so SpexCode never speaks the LSP protocol itself. A capability worth having usually has a pillar-native form; integrating it directly is how tools bloat.
  15. Each pillar gets exactly ONE adapter seam. The maintainer's framing: "harness adapter, language adapter, test framework adapter — 我们最终会拥有这三方面的适配". Harness adapter exists (harness.ts + launchers + materialize); language adapter is the anchor/coverage extraction seam; test-framework adapter is where eval evidence producers will plug in. Every seam shares one shape: an interface + an ordered registry + per-instance DATA rows (a new harness/language/runner is a row, not a branch), loud degradation when a tier is unavailable — and product semantics never learn which adapter sits on the other side.
  16. Don't invent what the pillar isn't prepared for. The maintainer's framing: "不要擅自发明 git 没有准备 好的东西". Stated when rejecting an explicit node-identity field to make rename tracking provable: git detects renames by content similarity rather than recording them, so an id field would erect a second identity model beside git's object model — two truths to keep in sync, and no way to know which to believe the first time they disagree. The consequence is accepted deliberately: we live inside git's rename semantics, and therefore the acceptance bar is "agrees with git's rename events across the whole tree", not "provably correct in some model git does not have". This is [[taste]] 14 applied to the seam itself — a pillar's limits enter the ecosystem along with its capabilities.
  17. Where the application-level call is genuinely unclear, let implementation simplicity decide. The maintainer's framing: "对于这种应用层拿不准的地方,我们就从自己的实现角度考虑,哪种实现最简单就怎么来". Not a tie-breaker to reach for early — it applies precisely when the product argument has been made in good faith and stays balanced. Then the honest question is which option costs less mechanism, and the answer is often that one of them is already the behaviour and costs zero lines.
  18. Stabilise first, then measure the shape of the cost — not just its size. The maintainer's framing: "先保证稳定,然后计算一下复杂度,是线性还是平方级,和 git 历史长度是否有关,还是说只和文件数目有关". A wall-clock number tells you a run was slow; the scaling dimension tells you which lever exists. The same 4-second lint means opposite things if it grows with history (nothing local can fix it) versus with source bytes (memoisation applies) versus with node count (the walk is wrong). Measure the dimension before designing the fix, and never buy a cache before knowing whether the work is repeated at all.

  19. Separate what is PERMANENT from what is CURRENT, and let the invalidation scope be derived, not assigned. Git history is append-only: what a commit changed, what trailers it carries, what a merge itself authored are permanent properties of that commit — they can be accumulated once and never recomputed. What those facts mean today — which node owns a path after renames, which window a version opens — is a question about the current tree, and is cheap. The costly half is permanent; the tip-relative half is cheap. A read that recomputes the permanent half from scratch pays O(history) forever, so the project gets slower every day it is worked on — measured here as ~0.33 ms per commit across two full-history walks. The same confusion at the cache layer produces a worse failure: when a rebuild costs more than the interval of the watchdog that guards it, every finished rebuild is invalidated before it can be served, and a safety net becomes a latency amplifier — the fix is to make the rebuild cheap, never to lengthen the interval, which only moves the threshold. The corollary is about scope: derived data spread across several projections (board, review snapshot, session eval revision) has no single place declaring what depends on what, so an invalidation scope ends up hand-assigned by whoever traced the call chain last — and the honest default becomes "invalidate everything". Cheap correctness there comes from letting a projection declare its inputs, so the scope follows from the declaration; until it does, narrowing a scope by inspection is a correctness change disguised as a performance one, and must be proven field-by-field against a full recompute. Keep the slow, obviously-correct full recompute in the repo as the specification, and hold every faster path to byte-equality against it.

  20. What falls out at a seam is picked up, never designed for. The maintainer's framing, invoking Liang Wenfeng's spirit: on the road to AGI, the things you can get along the way you simply take. The three layers ([[spexcode]]) are the main body and are judged by their own criteria; how natural or accessible the L0/L1 seam happens to be — the "borderland" — is secondary. Small capabilities fall out of it for free (a session with no board membership still has a readable mailbox; a read still works with no backend running; a subscription may or may not be persisted because it carries no truth). Take them: they cost nothing, and the fact that they fall out at all is evidence the seam sits where it should. But a borderland convenience is never a criterion and never a reason to move the seam — its real value is the quiet pressure it puts on the architecture to stay honest. The failure this guards against is promoting the byproduct to the definition: restating the layering as a horizontal read-versus-own cut classifies operations and silently destroys the adoption ladder, which is the thing that makes each layer worth having on its own.

  21. Our real failure mode is misidentifying the data source, and it never announces itself. The two operating rules, in the maintainer's framing: "先读答案自己带的元数据,再 diff 它的正文" and "一个坏的提取器 会给出最令人安心的那种假答案". This is knowledge about how we go wrong, so it is governed like any other contract. A reasoning error runs into a contradiction and gets caught; reading the wrong source returns a clean, internally consistent, confidently wrong answer, and nothing in the loop objects. Measured in one evening, four times, none caught by a human: /api/graph's body read while ignoring the x-spexcode-graph: stale, refreshing header it attaches to itself, turning one mid-rebuild sample into a claimed permanent product hole; a comparison keyed on a top-level overlays field the board does not have (its keys are identity/issuesStamp/nodes/sessions, overlays live under nodes[].overlays), so both sides returned zero and the script reported "identical"; an isolated Vite server that ignored ?api= and served its default backend, so two eval readings measured the wrong process; and a 43-node/0.487-zoom reading taken as a whole-tree sample when it was already the drill-down's folded output, which defined a feature for a phenomenon that does not exist. So: before diffing a body, read what the answer says about itself — its freshness label, its own key set, which endpoint actually served it. And prefer the product's own instrument to a measurement built for the occasion: a PATROL-REPAIR count of zero answers the causal question ("was any watcher blind?") that a 1-second stopwatch poller can only circle around, and four separate experiments that evening were made unnecessary by first asking whether the product already had the answer. A cheap extractor is not the cheap half of the job; it is where the expensive mistake lives.

  22. Prose is not a free edit: you can hold a tree's proof and then move the contract that proof was against. The maintainer's framing: "散文不是免费的编辑:你可以握着一棵树的证明,然后把那个证明所针对的 契约挪走。" Freshness has more than one axis, and the rule we already wrote down only guards one of them: "commit the verified tree, then file" catches the code moving after the proof, and says nothing about the contract moving after the proof. Measured, self-caught by the session that did it: it filed a passing reading, then softened the wording of the same scenario in eval.md, and both readings went stale on the scenario axis — in its own words, "I was holding the proof of that tree, and then I moved the contract that proof was against." The reason this one gets its own entry rather than an amendment to the code-axis rule is that the two feel nothing alike: editing code, you know you are moving the thing under test; editing wording feels like polishing a document, and scenarioHash disagrees. Its sibling is the same shape wearing a different costume — a sampling choice that silently decides the claim. One-build-per-fresh-process does not merely hide an in-process memo, it inverts the conclusion: 507ms cold against 12–15ms warm is a factor of forty, so "a cache buys ~0 here" and "this is the headline finding" were separated by nothing but how the sample was taken; the corpus picked to prove it could not exhibit the defect at all (0 of 1144 distinct reading codeShas reachable from that HEAD, therefore zero selector queries reaching the engine). Hence the method rule that came out of it: on a loaded box a fixed wall-clock threshold is not a claim — compare load-matched pairs, and put the falsifiable half of the claim on something load-independent. The stopwatch is the symptom; the count of git child processes is the assertion.

  23. An expected value that can be derived must be derived — hardcoding it once opens a second source of truth. Say the fact once; derive it everywhere else. Two independent instances landed in one evening, arrived at from opposite directions, and both are guards rather than fixes: docs-quickstart.test.ts reconstructs the expected harness list from the live NATIVE_HARNESS_IDS registry, so registering a harness without updating the three surfaces that teach adoption (both READMEs' Quick start, spex guide's setup page) fails there — measured, on the author of this entry, within the hour of adding a new harness; and session-create-cli.test.ts parses cli.ts with the TypeScript AST to derive three sets (the VALUE_FLAGS literal, every flag('x') value read, every rejectUnknownFlags allowlist) and assert their relationship with no flag name written in the test at all, so the next person who adds a valued flag and forgets the distant table fails on the right assertion instead of receiving a confident wrong sentence about a different flag. That second failure mode is the whole point: a restated list does not fail loudly when it drifts, it fails elsewhere, wearing the costume of an unrelated bug. This is the same bone as [[taste]] 16 — two truths will disagree eventually, and on that day nothing tells you which one to believe — and it is what [[taste]] 3 looks like when complexity is genuinely bought back: an AST test is more code than a string array, and it retires a class of defect rather than one instance of it. The discipline it pairs with is 22's, but the two are not the same kind: 22 is temporal (do not move the contract after the proof) and a human has to remember it, while this one is structural and enforces itself — a derived expectation catches you without anyone remembering anything. Two cautions keep it honest: derive from the authoritative home, because deriving from a copy silently promotes the copy; and the rule is about expectations, not about outlawing constants — a fact with exactly one home is fine where it lives, the defect is the second place that restates it.

expanded spec

This node is the seed. The de-drift campaign distills these into a sharper checklist (the "20 tastes" + the issue-selection criteria), audits the tree against them, and graduates the agent-governing ones into .plugins surface:system so every launched agent inherits them. Until then, this node is the durable record — read it when a design decision needs the project's own taste, and add to it (raw source first) when the maintainer states a new one.