Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
72a0c9b
chore: add data-enrichment plan + gitignore workspace
pro-vi May 31, 2026
57410ca
feat(types): add Wings/Legge optional fields (U1)
pro-vi May 31, 2026
aac62c4
feat(data): add 序卦 + 雜卦 modules (U2)
pro-vi May 31, 2026
a81e20a
feat(data): add 說卦傳 module (U3)
pro-vi May 31, 2026
c9633f3
feat(data): TrigramInfo .assoc + en glosses (U4)
pro-vi May 31, 2026
d3111fc
feat(core): connections overlay + HexagramDetail aggregation (U5)
pro-vi May 31, 2026
d073a5f
feat(terminal): detail-renderer Wings + trigram catalogue (U6)
pro-vi May 31, 2026
647c240
feat(core,cli): Style union + CLI lockstep — gc key (U7)
pro-vi May 31, 2026
20d3b08
feat(data): backfill 卦辭 + 小象傳 into GUA (U8)
pro-vi May 31, 2026
c44afcf
feat(core,terminal): Legge module + voice activation (U10)
pro-vi May 31, 2026
e724365
docs: data-layers reference + orbit-integrity callout (U11)
pro-vi May 31, 2026
c055611
refactor: apply /naming findings — SHUO_GUA, _GLOSS_EN, buildConnections
pro-vi May 31, 2026
55b36c1
fix: close consumer-completeness gaps from /code-review
pro-vi May 31, 2026
f46362f
refactor: extract renderer helpers + commit data-import scripts
pro-vi May 31, 2026
5fa9b26
fix: Legge zagua anomaly reroute + idempotent cleanup metadata
pro-vi May 31, 2026
9d27002
test: tighten Legge zagua anomaly coverage
pro-vi May 31, 2026
3233b7e
chore: ignore local data acquisition scripts
pro-vi May 31, 2026
a1fa83d
feat(terminal): open Shuogua citation chapters
pro-vi May 31, 2026
f751b90
feat(terminal): enrich Shuogua chapter reader
pro-vi Jun 1, 2026
b25f882
refactor(terminal): lead derived relations with gua text
pro-vi Jun 1, 2026
f541373
Revert "refactor(terminal): lead derived relations with gua text"
pro-vi Jun 1, 2026
b2cdadb
feat(config): add display language setting
pro-vi Jun 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,6 @@ publish/iching.js
.prov/
logs/
.research/
/data-acquisition/
# Local JSON -> TS acquisition tooling depends on ignored /data-acquisition inputs.
/scripts/data-acquisition/
23 changes: 22 additions & 1 deletion apps/cli/src/__tests__/config-command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,14 @@ describe("config command", () => {
await rm(dataDir, { recursive: true, force: true });
});

test("list shows every persisted key, including castMethod, castMode, and taijituStyle", async () => {
test("list shows every persisted key, including language and cast settings", async () => {
const { exitCode, stdout } = await runCli(dataDir, ["config", "list"]);
expect(exitCode).toBe(0);
// Every field that JsonConfigStore.DEFAULT_CONFIG persists must be visible.
for (const key of [
"theme",
"motion",
"language",
"color",
"timezone",
"glyphAnim",
Expand All @@ -78,6 +79,12 @@ describe("config command", () => {
expect(stdout.trim()).toBe("auto");
}, 20_000);

test("get language returns the default value", async () => {
const { exitCode, stdout } = await runCli(dataDir, ["config", "get", "language"]);
expect(exitCode).toBe(0);
expect(stdout.trim()).toBe("zh-Hant");
}, 20_000);

test("get taijituStyle returns the default value", async () => {
const { exitCode, stdout } = await runCli(dataDir, ["config", "get", "taijituStyle"]);
expect(exitCode).toBe(0);
Expand Down Expand Up @@ -105,6 +112,14 @@ describe("config command", () => {
expect(getResult.stdout.trim()).toBe("yarrow");
}, 20_000);

test("set language en persists, reload reads en", async () => {
const setResult = await runCli(dataDir, ["config", "set", "language", "en"]);
expect(setResult.exitCode).toBe(0);
const getResult = await runCli(dataDir, ["config", "get", "language"]);
expect(getResult.exitCode).toBe(0);
expect(getResult.stdout.trim()).toBe("en");
}, 20_000);

test("set castMode rejects yarrow (now out of castMode's domain)", async () => {
const { exitCode, stderr } = await runCli(dataDir, ["config", "set", "castMode", "yarrow"]);
expect(exitCode).not.toBe(0);
Expand Down Expand Up @@ -136,6 +151,12 @@ describe("config command", () => {
expect(stderr.toLowerCase()).toContain("invalid value");
}, 20_000);

test("set language rejects invalid value", async () => {
const { exitCode, stderr } = await runCli(dataDir, ["config", "set", "language", "latin"]);
expect(exitCode).not.toBe(0);
expect(stderr.toLowerCase()).toContain("invalid value");
}, 20_000);

test("get unknown key reports error", async () => {
const { exitCode, stderr } = await runCli(dataDir, ["config", "get", "notARealKey"]);
expect(exitCode).not.toBe(0);
Expand Down
25 changes: 24 additions & 1 deletion apps/cli/src/__tests__/scene-factories.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ import { mkdtemp } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { JsonlJournalStore } from "@iching/storage";
import { BrowseScene, DetailScene, JournalScene } from "@iching/terminal";
import {
BrowseScene,
DetailScene,
JournalScene,
ShuoGuaChapterScene,
} from "@iching/terminal";
import {
makeBrowseFactory,
makeJournalFactory,
Expand Down Expand Up @@ -42,6 +47,13 @@ describe("makeBrowseFactory", () => {
expect(factory({ type: "openJournal" })).toBeNull();
expect(factory({ type: "openSettings" })).toBeNull();
});

test("typed openShuoguaChapter signal returns ShuoGuaChapterScene", () => {
const factory = makeBrowseFactory({ journal });
const scene = factory({ type: "openShuoguaChapter", chapter: 3, op: "nuclear" });
expect(scene).toBeInstanceOf(ShuoGuaChapterScene);
expect((scene as ShuoGuaChapterScene).getOp()).toBe("nuclear");
});
});

describe("makeJournalFactory", () => {
Expand Down Expand Up @@ -73,6 +85,17 @@ describe("makeJournalFactory", () => {
expect(scene).toBeInstanceOf(BrowseScene);
});

test("typed openShuoguaChapter returns ShuoGuaChapterScene", () => {
const factory = makeJournalFactory({
journal,
entries: [],
session: { cols: 80, rows: 24 },
});
const scene = factory({ type: "openShuoguaChapter", chapter: 6, op: "mirror" });
expect(scene).toBeInstanceOf(ShuoGuaChapterScene);
expect((scene as ShuoGuaChapterScene).getOp()).toBe("mirror");
});

test("typed openJournal resets to JournalScene (j-from-replay path)", () => {
const factory = makeJournalFactory({
journal,
Expand Down
80 changes: 80 additions & 0 deletions apps/cli/src/__tests__/style-union-parity.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { describe, test, expect } from "bun:test";
import { STYLES, QUOTE_STYLES, type Hexagram } from "@iching/core";
import { VALID_STYLES } from "../commands/hexagram.js";
import { hexagramToJson } from "../output/json.js";

/**
* U7 parity test — Style union, hardcoded enumeration lockstep.
*
* The four lists below MUST agree on the shape of the Style universe so
* future additions can't drift between them.
*
* Style (type) — types.ts
* STYLES (array) — data/trigrams.ts (must match Style at runtime)
* QUOTE_STYLES — data/trigrams.ts (Style minus "st" minus "gc")
* VALID_STYLES — apps/cli/src/commands/hexagram.ts (Style minus "st")
*
* "gc" is in Style and VALID_STYLES but intentionally NOT in QUOTE_STYLES:
* 卦辭 is the root oracle, not a random-quotable commentary lineage.
*/

describe("Style union — parity across hardcoded lists", () => {
test("STYLES carries the full Style union (7 keys after U7)", () => {
expect(STYLES.sort()).toEqual(["dx", "en", "gc", "st", "te", "tu", "w"]);
});

test("QUOTE_STYLES is STYLES minus \"st\" minus \"gc\" (5 quote-able lineages)", () => {
const expected = STYLES.filter((s) => s !== "st" && s !== "gc").sort();
expect(QUOTE_STYLES.sort()).toEqual(expected);
expect(QUOTE_STYLES).not.toContain("gc");
expect(QUOTE_STYLES).not.toContain("st");
});

test("VALID_STYLES (CLI) is STYLES minus \"st\" — includes gc for explicit lookup", () => {
const expected = STYLES.filter((s) => s !== "st").sort();
expect([...VALID_STYLES].sort()).toEqual(expected);
expect(VALID_STYLES).toContain("gc");
expect(VALID_STYLES).not.toContain("st");
});
});

describe("hexagramToJson — conditional gc emission", () => {
function makeHex(overrides: Partial<Hexagram> = {}): Hexagram {
return {
u: "X",
n: "X",
p: "X",
ename: "X",
l: [1, 1, 1, 1, 1, 1],
dx: "dx",
tu: "tu",
en: "en",
te: "te",
w: "w",
yao: ["", "", "", "", "", ""],
yaoEn: ["", "", "", "", "", ""],
...overrides,
};
}

test("legacy hexagram (no gc populated) — gc key absent from commentary", () => {
const json = hexagramToJson(1, makeHex()) as {
commentary: Record<string, unknown>;
};
expect("gc" in json.commentary).toBe(false);
expect(json.commentary).toMatchObject({
dx: "dx",
tu: "tu",
en: "en",
te: "te",
w: "w",
});
});

test("hexagram with gc populated — gc key present in commentary", () => {
const json = hexagramToJson(1, makeHex({ gc: "元亨利貞" })) as {
commentary: Record<string, unknown>;
};
expect(json.commentary.gc).toBe("元亨利貞");
});
});
7 changes: 7 additions & 0 deletions apps/cli/src/app/scene-factories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
JournalScene,
type Scene,
type SceneFactory,
ShuoGuaChapterScene,
} from "@iching/terminal";

export interface SessionDims {
Expand Down Expand Up @@ -44,6 +45,9 @@ export function makeDetailScene(kw: number, deps: DetailDeps): DetailScene {
export function makeBrowseFactory(deps: DetailDeps): SceneFactory {
return (signal): Scene | null => {
if (signal.type === "openDetail") return makeDetailScene(signal.kw, deps);
if (signal.type === "openShuoguaChapter") {
return new ShuoGuaChapterScene(signal.chapter, signal.op);
}
return null;
};
}
Expand All @@ -68,6 +72,9 @@ export function makeJournalFactory(deps: JournalDeps): SceneFactory {
return cs;
}
if (signal.type === "openDetail") return makeDetailScene(signal.kw, deps);
if (signal.type === "openShuoguaChapter") {
return new ShuoGuaChapterScene(signal.chapter, signal.op);
}
if (signal.type === "openDictionary") return new BrowseScene();
// `j` from a replayed CastScene inside the journal router → reset to the journal list.
if (signal.type === "openJournal") return new JournalScene(deps.entries);
Expand Down
Loading
Loading