
Hai Ssot
- 5 installs
- 277 repo stars
- Updated June 11, 2026
- hylarucoder/hai-stack
Diagnoses single-source-of-truth violations across code seams and produces a numbered findings report with file:line evidence and a treatment-recipe table.
About
Hunts down single-source-of-truth violations across ten symptom classes (multi-source literals, shape proliferation, redundant conversions, re-implemented derivations, and more) and produces a numbered findings report with severity and treatment recipes. A developer uses it when they suspect duplicated definitions or drift between layers.
- Detects ten SSOT symptom classes across type-system-unreachable seams
- Routes each finding to a treatment recipe with honest false-positive adjudication
Hai Ssot by the numbers
- 5 all-time installs (skills.sh)
- +1 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #881 of 1,352 Code Review & Quality skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/hylarucoder/hai-stack --skill hai-ssotAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 5 |
|---|---|
| repo stars | ★ 277 |
| Last updated | June 11, 2026 |
| Repository | hylarucoder/hai-stack ↗ |
What it does
Diagnoses single-source-of-truth violations across code seams and produces a numbered findings report with file:line evidence and a treatment-recipe table.
Files
Hai SSOT
For Chinese readers, see SKILL.zh_CN.md. The English SKILL.md is the execution source of truth.
Overview
Hunt down places where one fact, one shape, or one word has more than one authoritative home — or where one name secretly serves several facts. Produce a findings report that an engineer can execute from: every finding numbered, evidenced, honestly adjudicated, and routed to a concrete disposition. The skill is a diagnostic with a strong opinion about treatment, not a linter.
The Core Law
SSOT violations cluster, almost without exception, at boundaries the type system cannot reach: language↔database (string literals in raw SQL and CHECK constraints), language↔wire (hand-written schemas mirroring the producing types), layer↔layer (untyped payload envelopes — Record<string, unknown>, map[string]any, bare dicts — keyed by raw strings), code↔docs, production↔fixture. Inside one compiler's reach, multi-source dies naturally — a second definition is a compile error or an obvious dead symbol. Outside it, multi-source is the equilibrium state: every producer hand-builds, every consumer hand-gropes, every layer re-copies the strings it needs.
Two corollaries that direct the hunt:
1. Start at the cross-stack seams; don't burn time grepping for intra-language duplication. 2. A test that pins two copies equal ("parity test", "pin test", "currency test") is a flag: either the copy should not exist (eliminate it, then delete the test), or both sides are genuinely real artifacts that cannot share a source (then the test IS the correct treatment). Locate every such test early — each one marks either a violation or a treatment already applied.
The Ten Symptom Classes
Read references/detection-cookbook.md for concrete search recipes per class before sweeping.
| # | Symptom | One-line definition | Canonical tell |
|---|---|---|---|
| 1 | Multi-source literals | One wire string / enum value defined independently in N places | Same quoted literal in two modules; a private const shadowing a public one |
| 2 | Shape proliferation | One concept carried by N type/schema shapes across seams | Hand-written schema on one side of a wire mirroring the producing type on the other; typed→untyped "regressions" where a typed object gets flattened back into a string-keyed map at a seam |
| 3 | Word overload | One word meaning N different things (the mirror of #1) | The domain's most valuable word (e.g. "memory" as product moat) also used for infrastructure ("in-memory"); a term with 2-3 documented senses |
| 4 | Legacy-vocabulary mapping layers | Old vocabulary survives inside display/fixture mapping functions after a wire rename — and the mappings themselves get copy-pasted | The same old→new word map appearing in two files; fixtures asserting retired vocabulary |
| 5 | Dual-pathway behavior forks | The same operation behaves differently depending on which entry path/host ran it | A CLI path skipping the safety pipeline the server path runs; one caller passing explicit-empty where another gets defaults |
| 6 | Scattered defaults | The same fallback value born independently at multiple layers | A default directory/timeout/limit defined as a constant in one layer AND as an inline fallback in another, neither referencing the other |
| 7 | Pure-subset shape pairs | Type B = type A minus k fields, plus a field-by-field copy converter | A converter function that only copies fields; the "information" carried by the second shape is merely hiding fields |
| 8 | Same-name-different-shape | Two exported types with the same name in the same semantic domain but different fields | RunSnapshot in two sibling modules meaning related-but-different things |
| 9 | Redundant conversion chains | One concept reshaped at multiple hops along a single call path, where the intermediate shapes add no information | A value converted A→B→C on its way through layers; round-trips (A→B→A); typed→string→typed relays where a value is serialized and re-parsed inside one process. Kill question per hop: "what information does this shape add?" — no answer means the hop merges |
| 10 | Re-implemented derivations | The same rule — validation, normalization, parsing, a derived field — implemented independently at N layers, each a drift point | The same regex/threshold/branching duplicated with small diffs; a date string parsed at three layers; isActive/displayName computed differently in two views |
Severity logic: a violation that has already caused a production symptom (silent empty rendering, correctness bug, wrong cursor) outranks everything; next, violations on persisted or user-visible wire; then cross-team/cross-stack seams; intra-module duplication last.
Honest Adjudication — what NOT to flag
Findings are leads, not verdicts. Run these checks before a finding enters the report; record exonerated candidates in a "not counted" note so the next sweeper doesn't re-litigate:
- Module-qualified generic names are idiomatic, not violations. A short generic type name
qualified by its module (stream.Message vs processor.Message) is the standard-library pattern in every module system (see the cookbook's language notes for per-language exemplars). Flag same-name types only when they share a semantic domain and confuse a cross-module reader (class #8), or collide in one file.
- Forward contracts are alive even with zero producers. A registry entry / enum value with no
backend producer may be consumed by a frontend switch as a forward contract. Grep every consumer surface (web, contract, seeds) before calling anything dead. Disposition for these: annotate and group, never delete on producer-absence alone.
- Persisted wire literals are frozen. The fix unifies definition sites; the string values
on disk/in events never change. Even naming inconsistencies baked into the wire (mixed prefixes) get documented, not repaired.
- A shape change is legitimate when it carries information — adds a sequence number, hides
internal fields for an audience, renames into a consumer's vocabulary. The disease is reshaping that carries nothing (class #7), typed→map regressions, and chains where every hop re-converts without adding anything (class #9). Judge each hop separately: a chain can contain one real boundary and two gratuitous ones.
- Re-checks at trust boundaries are defense, not duplication. A server re-validating client
input, or a DB constraint backing an app-level check, is deliberate redundancy across trust levels. Class #10 flags re-implemented rules at the same trust level — two layers behind the same boundary each owning their own copy of the regex, threshold, or parse.
- Port/impl module pairs and per-plugin modules are conventions, not fragmentation. Don't
recommend flattening them in an SSOT report.
- Deliberate, adjudicated dual vocabularies can exist (e.g. an ADR chose a flat result type
with a closed discriminator). Check decision records before flagging; contrast honestly — a 4-field type with a closed-set kind is not the same disease as a 9-field union with no discriminator semantics.
The credibility of the whole report rests on this section. One overreaching finding ("unify all the Messages!") teaches the reader to ignore the real ones.
Treatment Recipes
Every confirmed finding routes to exactly one recipe; the recipe determines the disposition:
| Recipe | When | Notes |
|---|---|---|
| Codegen | One side can mechanically generate the other (type → schema, registry → enum file) | Strongest fix; pairs with a currency test (guards "forgot to regenerate" — that is a constructive gap a compiler can't close, so the test is legitimate) |
| Parity guard | Both sides are real artifacts that cannot share a source (language enum vs DB CHECK constraint) | Include a red drill: deliberately desync once and confirm the guard fires |
| Constant promotion | Bare string keys in map envelopes crossing layers | Promote to a named constant next to its siblings; both writer and reader reference it |
| Typed payload | Shape proliferation / map regressions at seams | Often a phase of a larger contract plan; don't band-aid per-field |
| Convert at the edge | Redundant conversion chains | Convert once where the value enters the system; pass one canonical type through the interior; merge hops that add no information |
| Single rule owner | Re-implemented derivations | Hoist the rule into one named function/type and make every layer call it; where possible encode the proof in the type (parse, don't validate) so downstream layers cannot re-do the work |
| Vocabulary close-out | Word overload, legacy mapping layers | Glossary entry + rename of the cheap side; map function single-sourced or fixtures moved to current vocabulary |
| Adjudication | Behavior forks | These need a decision, not a patch: unify the behavior, or promote the fork into an explicitly documented contract. Present both options with a default recommendation |
| Delete the pin | After any recipe eliminates a copy, delete the parity test that was holding the copies together — its survival is evidence of remaining multi-source |
Workflow
1. Scope. Agree on the sweep surface (a module, a contract plane, the whole repo). Note any prior sweeps/plans to avoid re-finding adjudicated items. 2. Map the seams first. List the type-system-unreachable boundaries in scope (which wires, which DB constraints, which untyped envelopes, which generated artifacts). The Core Law says the findings live there. 3. Hunt per symptom class using the cookbook greps. For each candidate, capture file:line for every definition/use site — counts matter ("this literal is defined in exactly 2 places", "adding one event touches 6 files" is the change-amplification number that lands the point). 4. Verify producer AND consumer for anything you might call dead or removable. The three-surface discipline: backend producers, frontend/contract consumers, seeds/fixtures. 5. Adjudicate honestly (section above). Sort exonerated candidates into the "not counted" note. 6. Write the report using references/output-template.md: numbered findings, evidence, severity, recipe, disposition table, positive list ("already-healthy patterns to copy" — naming what the repo already does right makes the report constructive and gives fixes a local precedent to imitate). 7. Execute quick wins if asked — constant promotions and literal de-duplications are usually safe same-day (zero wire change, full test gate). Bigger recipes get routed to plans; behavior forks get routed to the user as decisions.
Hand off when
- The finding's root cause is a module-boundary or layering problem → hai-architecture.
- A finding needs a new name, or the report turns into a rename list → hai-naming.
- The dispositions need to become a phased, verifiable plan → hai-goal.
- The user wants to reframe the whole contract surface rather than patch findings → geju.
What this skill is NOT
- Not a linter: it reports adjudicated findings with treatment routes, not raw matches.
- Not "unify everything": its credibility comes from the not-counted list as much as the findings.
- Not a wire-migration tool: persisted values are out of bounds; only definition sites move.
Detection Cookbook — per-symptom search recipes
Adapt the patterns to the repo's languages. The examples assume Go backend + TS frontend + SQL migrations, but every class has an analog in other stacks. Language-specific idioms and exemptions live in the Language notes section at the bottom.
1. Multi-source literals
- Pick the wire vocabularies in scope (event types, status enums, error codes). For each value,
count definition sites: grep -rn '"the.literal"' --include='*.go' --include='*.ts' | grep -v _test A healthy literal has exactly one non-test definition; references go through the constant.
- Hunt private constants shadowing public ones:
grep -rn 'EventType = "' | sort by value. - Registries/manifests that mix constant references with bare strings: scan the registry file for
quoted literals — each one either lacks a constant (create it) or ignores an existing one.
- Find parity/pin/currency tests (
grep -rni 'pin\|currency\|stays in sync' *_test*) — each marks
a multi-source site, treated or untreated.
2. Shape proliferation
- For each wire payload: locate the producing struct and the consuming schema. Hand-written
schema + struct = dual source. Evidence to capture: a field that already diverged (the strongest possible exhibit is a past incident).
- Typed→map regressions: grep seams for
map[string]any{literals built from a typed value's
fields (Foo: x.Foo inside a map build), and AsPayload()/AsMap() methods whose keys nobody reads back.
- Count shapes per concept: list every struct whose doc/name claims the same noun ("event",
"result", "snapshot") and diff their field sets.
3. Word overload
- Take the domain's load-bearing nouns (from the glossary / core beliefs). For each, inventory
every file/dir/type that uses the word: find . -iname '*word*' + grep -rn 'type.*Word' --include='*.go'. Classify by meaning; ≥2 meanings = finding.
- Special case: infrastructure adjectives squatting on domain nouns ("memory" = in-process vs
the product's memory system).
4. Legacy mapping layers
- Grep retired vocabulary (old enum words, pre-rename terms) — survivors usually sit inside
display-mapping switch statements and test fixtures.
- Then check whether the mapping itself exists more than once: same old→new pairs in two files.
5. Dual-pathway behavior forks
- List every entry point into a shared engine/service (server worker, CLI, scheduled job, test
harness). Diff the options each entry passes: nil-vs-explicit-empty collections, skipped middleware/pipelines, different defaults. A fork only documented by a code comment is a finding.
6. Scattered defaults
- For each default-looking literal (paths, durations, limits, model IDs): count birth sites.
Constants + inline fallbacks (if x == "" { x = ... }) for the same value in different layers, not referencing each other, = finding.
firstNonEmpty/cmp.Orcascades are where these hide.
7. Pure-subset shape pairs
- Grep converter functions that only copy fields (
return B{X: a.X, Y: a.Y, ...}with no
transformation). Diff the two field sets; if B ⊆ A and the converter adds nothing, it's a finding.
8. Same-name-different-shape
grep -rn '^type Name struct' --include='*.go'for each exported type name appearing more
than once inside one module tree; flag only same-semantic-domain pairs (see adjudication).
9. Redundant conversion chains
- Pick a concept that crosses layers; trace one value end-to-end and count shape changes
between the system edge and the point of use. More than one conversion = candidate chain.
- Grep converter compositions and round-trips: nested
toX(fromY(...))calls,
FromDTO(ToDTO(x)), mapper functions whose output feeds straight into another mapper.
- Typed→string→typed relays: a value serialized (
json.Marshal,JSON.stringify,
.toString()) and re-parsed downstream within the same process — grep marshal/unmarshal pairs on the same type inside one call path.
- Evidence to capture: every hop with file:line, and the per-hop answer to "what information
does this shape add?" Hops with no answer are the finding; hops at real boundaries (audience change, info added) are exonerated individually.
10. Re-implemented derivations
- Pick rule-shaped logic: validation regexes, normalizations (trim / lowercase / URL / date
parsing), derived flags and labels (isActive, displayName, totals, status computation). For each, count implementation sites across layers — duplicated-but-slightly-different is the strongest exhibit (the small diff IS the drift).
- Grep the same regex / threshold / format string appearing in more than one layer; grep the
same field parsed (time.Parse, new Date(, parseInt) at more than one point on one path.
- Adjudicate trust boundaries first: client→server re-validation and DB constraints backing
app checks are defense, not duplication. Flag only same-trust-level re-implementation.
Cross-stack seam checklist (where to aim all of the above)
- Raw SQL strings and CHECK constraints vs language enums
- Hand-written wire schemas (Zod/JSON Schema/OpenAPI) vs producing types
- Untyped envelopes crossing package/layer boundaries (suspension payloads,
evidence/metadata bags, magic request keys)
- Generated artifacts and their generators (is everything claimed-generated actually generated?)
- Docs that claim authority (glossaries, registries) vs the code they describe
- Test fixtures vs current wire vocabulary
Language notes — idioms and exemptions per stack
Go
- Untyped envelopes look like
map[string]any/map[string]interface{}built with bare
string keys; grep map literals whose fields copy a typed value (Foo: x.Foo inside a map build).
- Package-qualified generic names (
stream.Messagevsprocessor.Message) are the stdlib's own
bytes.Buffer / http.Client pattern — exempt unless they share a semantic domain (class #8).
- Port/impl package pairs and per-plugin packages are layout conventions, not shape proliferation.
- Round-trip tells for class #9:
json.Marshal+json.Unmarshalon the same type within one
call path; struct↔map↔struct shuffles at layer boundaries.
TypeScript
- Untyped envelopes look like
Record<string, unknown>/anybags / index signatures; the
class-2 regression tell is an as cast or a spread that flattens a typed object at a seam.
- Hand-written Zod / JSON Schema / OpenAPI fragments mirroring a server type are the canonical
shape-proliferation site — prefer generating one from the other (z.infer, openapi-typescript).
- The same union-of-literals re-declared per layer (
'active' | 'archived'in three files) is a
class-1 multi-source literal even though no enum keyword appears.
- Interfaces re-declared per layer with one optional-field diff are class-7 pure-subset pairs;
prefer Pick / Omit / Partial over a hand-copied interface.
- Round-trip tells for class #9:
JSON.parse(JSON.stringify(x))clones,toJSON/fromJSON
pairs inside one process, DTO↔domain mappers stacked per layer.
SSOT Findings Report Template
Fill every section. The Not-Counted note and the Positive List are mandatory — they carry the report's credibility and give fixes local precedents.
# SSOT 诊断 — <scope>(<date>)
## 总规律(先于清单)
<Where the violations clustered in THIS sweep, stated against the Core Law: which
type-system-unreachable seams produced them. One paragraph.>
## 违规清单
### S1 — <symptom-class>: <one-line title>(severity / disposition)
<Definition sites with file:line for EVERY copy. The change-amplification number when
relevant ("adding one X touches N places"). Past incidents this class already caused,
if any — they are the strongest evidence.>
**处置**: <recipe + where it lands: fix-now / plan-name phase / decision owner>
### S2 — …
## 判定要诚实(不计违规)
<Candidates examined and exonerated, each with the rule that exonerated it
(idiomatic module-qualified name / forward contract with live consumer /
information-carrying projection / adjudicated by ADR-x). This section prevents
re-litigation and "unify everything" overreach.>
## 已治理范本(正面清单)
| 范本 | 机制 |
|---|---|
| <healthy pattern in this repo> | <codegen + currency test / parity guard / single table + derivation> |
## 处置汇总
| 项 | 治法 | 归属 | 状态 |
|---|---|---|---|
| S1 … | constant promotion | fix-now | ✅ / 待排 |
| S2 … | parity guard | <plan> P2 | 已立计划 |
| S3 … | adjudication | 用户裁决 | 待裁决 |Rules of the format:
- Number findings S1, S2, … and keep numbers stable across follow-up sweeps of the same scope
(append, don't renumber).
- Every finding cites file:line for all sites, not just one example.
- Severity ordering: caused-incident > persisted/user-visible wire > cross-stack seam > intra-module.
- Dispositions must name a real destination: a same-day fix, a specific plan phase, or a named
decision for the user. "待优化" with no owner is not a disposition.
- If quick wins are executed in the same session, mark them ✅ with the date inside the report —
the report doubles as the execution record.
Hai SSOT(中文说明)
执行以英文 SKILL.md 为准;本文是给中文读者的速览。一句话
诊断代码库中违反"单一数据源"(SSOT)的地方,产出带编号(S1、S2…)、带 file:line 证据、带诚实判定与处置归属的 findings 报告——是有治疗主张的诊断,不是 linter。
核心规律
违规几乎全部聚集在类型系统够不到的边界:语言↔数据库(SQL 字面量、CHECK 约束)、语言↔wire(手写 schema 镜像生产端类型)、层↔层(无类型信封裸键——Record<string, unknown>、map[string]any、裸 dict)、代码↔文档、生产↔fixture。编译器可达处单源是常态;不可达处多源是均衡态。推论:从跨栈接缝开打;每个"对拍/pin 测试"都标记着一处违规或一处已施治。
十类症状
1. 多源字面量 — 同一 wire 字符串在 N 处独立定义 2. 形状增生 — 同一概念 N 个形状;最恶劣形态是 typed→无类型 map 的逆向退化 3. 同词异义 — 一个词 N 个意思(多源的镜像病) 4. 旧词映射层续命 — wire 改名后旧词退守显示/fixture 映射层,映射自己又被复制 5. 双通道行为分叉 — 同一操作不同入口行为不同(如 CLI 跳过 server 的安全管道) 6. 默认值多处出生 — 同一兜底值在多层各自声明、互不引用 7. 纯子集形状对 — B = A 减 k 个字段 + 纯拷贝转换函数 8. 同名异形 — 同一语义域内两个同名导出类型形状不同 9. 冗余转换链 — 同一概念沿一条调用链被反复转换(A→B→C、A→B→A 来回转、typed→string→typed 中转),中间形状不携带任何信息;逐跳追问"这次转换增加了什么信息",答不上来的跳合并 10. 同一规则多处实现 — 同一条规则(校验、归一化、解析、派生字段)在 N 层各写一份,每份都是漂移点(同一个正则三处略有不同、同一个日期字段三层各解析一次)
诚实判定(不计违规)
- 模块限定的通用重名(
stream.Messagevsprocessor.Message)是任何模块体系的标准库惯例(各语言范例见 cookbook 的 Language notes) - 零 producer 的注册项可能是前端 forward contract——grep 全部消费面后才许判死
- 已持久化的 wire 字面量冻结:修定义点,永不改值
- 携带信息的形状变化(加 seq、藏字段、换受众词汇)是正当投影;转换链逐跳单独判,一条链里可以一跳正当两跳多余
- 信任边界处的重复校验是防御不是病(server 复核 client 输入、DB 约束兜底应用层校验);第 10 类只抓同一信任级内的重复实现
- 端口/实现分模块、per-plugin 模块是惯例不是碎片
- 有 ADR 背书的刻意双轨先查决策记录再下结论
报告的公信力一半来自"不计违规"清单——一条过度扩张的 finding 会让读者忽略真问题。
治理配方
codegen(+currency test)/ 对拍守卫(含红色演练)/ 常量升格 / typed payload / 边界一次转换(入口处转一次,内部走一个规范类型,无信息的跳合并)/ 单一规则所有者(规则收口到一个具名函数/类型,能编码进类型就编码——parse, don't validate)/ 词汇收口 / 裁决(行为分叉要的是决定不是补丁)/ 副本消灭后删掉 pin 测试。
参考文件
references/detection-cookbook.md— 每类症状的具体 grep 食谱 + 跨栈接缝清单 + 各语言惯例与豁免(Go / TypeScript)references/output-template.md— 报告模板(总规律 / S# 清单 / 不计违规 / 正面范本 / 处置汇总)
交接
根因是模块边界 → hai-architecture;要起新名 → hai-naming;处置要成计划 → hai-goal;要重构整个契约面 → geju。