36 Commits
Author SHA1 Message Date
imacatandClaude Fable 5 d46c50a2db Changed the natural-coding definition files to single-pass convergence
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 22:38:26 +08:00
imacatandClaude Fable 5 964006ec6d Add the natural-coding definition files and the convergence algorithms
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 22:38:26 +08:00
imacatandClaude Fable 5 0c82da7a1d Settle women-power as the a-priori screen label term
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 22:38:26 +08:00
imacatandClaude Fable 5 93a4f808bf Reduce run-llm to a pure batch executor
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 22:38:26 +08:00
imacatandClaude Fable 5 7a58f37d9f Record the natural-coding pipeline design in the project documents
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 22:38:26 +08:00
imacatandClaude Fable 5 b727284ef3 Gather the CLI command modules into a commands sub-package
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 22:38:26 +08:00
imacatandClaude Fable 5 396848e898 Add the export-llm-input subcommand for the lyrics-only LLM input
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 22:38:26 +08:00
imacatandClaude Fable 5 55926b8dc4 Regenerate the artists report with the Wikidata snapshot applied
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 22:38:26 +08:00
imacatandClaude Fable 5 ee6a1b88c5 Rename the Markdown documents to dash-connected names
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 22:38:20 +08:00
imacatandClaude Fable 5 1a7519becd Drop the engine preparation from build-db
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 15:34:36 +08:00
imacatandClaude Fable 5 a534b1b70b Move the invariant checks into the importers and drop find_violations
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 15:34:36 +08:00
imacatandClaude Fable 5 8fdd21f8a2 Package the capture-layer application into a CaptureImporter class
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 15:34:36 +08:00
imacatandClaude Fable 5 c561330bd0 Repackage the build-db import steps into job classes
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 15:34:36 +08:00
imacatandClaude Fable 5 64c62eb394 Capture the song lyrics from the pilot study and the public APIs
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 15:34:36 +08:00
imacatandClaude Fable 5 0d3cd69a0f Align the fetch-lyrics summary with fetch-artists via shared utils
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 15:34:26 +08:00
imacatandClaude Fable 5 81975b72e4 Remove the missing-lyrics report from fetch-lyrics
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 15:15:49 +08:00
imacatandClaude Fable 5 1b21471845 Fall back to the original artist credit when fetching lyrics
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 15:15:49 +08:00
imacatandClaude Fable 5 15f207ffc6 Remove the retired artist overrides layer and the mixed artist type
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 15:15:49 +08:00
imacat 93aa06703b Keep the artist snapshot sorted by name 2026-08-04 15:15:49 +08:00
imacat 8fdd25f569 Show resolved count and elapsed time in the fetch-artists summary 2026-08-04 15:15:49 +08:00
imacat 00b5c208ec Resolve artists on Wikidata via exact-match SPARQL with song corroboration 2026-08-04 15:15:49 +08:00
imacat c81bfcf51d Restore official artist name spellings in the canonical table 2026-08-04 15:15:49 +08:00
imacat 942a74c86c Record the complete-Wikidata-then-capture workflow decision 2026-08-04 15:15:49 +08:00
imacat 1f95f0231d Export human-readable songs and artists reports to the derived data layer 2026-08-04 15:15:49 +08:00
imacat ae0e9d0a08 Split artist credits with documented exceptions and normalize artist names 2026-08-04 15:15:49 +08:00
imacat 6a91bc73eb Merge same-song chart rows via a canonical artist credit table 2026-08-04 15:15:49 +08:00
imacat e15c988bf4 Store the source chart CSV with its original CRLF line endings 2026-08-04 15:15:49 +08:00
imacat f36a30411d Align the source chart CSV with the archived Billboard year-end charts 2026-08-04 15:15:49 +08:00
imacatandClaude Fable 5 5d5e4fac50 Reorganize data/ into source, captures, and manual layers
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 15:15:49 +08:00
imacatandClaude Fable 5 e9027472f4 Make every subcommand input an explicit CLI argument or option
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 15:13:31 +08:00
imacatandClaude Fable 5 e0eb343ce0 Refactor internal data passing to dataclasses and enums, and remove unneeded defensive code
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 15:13:31 +08:00
imacatandClaude Fable 5 8d223177cd Add the data layer with the build-db, fetch-lyrics, and fetch-artists subcommands
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 15:12:22 +08:00
imacatandClaude Fable 5 f9ab79f0c2 Use the unified pydantic-settings configuration in run_llm
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 14:03:37 +08:00
imacatandClaude Fable 5 b68393ab01 Record the data storage architecture and phase-1 plan in the project documents
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 14:03:37 +08:00
imacatandClaude Fable 5 12ace6f45a Add the tools sub-project with the run_llm batch runner
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 14:03:28 +08:00
imacatandClaude Fable 5 9a42b463a3 Initialize project structure, workflow rules, and research plan
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 18:51:10 +08:00
13 changed files with 10 additions and 169 deletions
+5 -27
View File
@@ -99,8 +99,8 @@
## 2026-08-02 ## 2026-08-02
- **`import-lyrics` 不設為子命令,pilot 歌詞改以 `excludes/` - **`import-lyrics` 不設為子命令,pilot 歌詞改以私人腳本
私人腳本匯入**。理由:pilot 捕捉檔不隨論文發布,子命令 匯入**。理由:pilot 捕捉檔不隨論文發布,子命令
形式會在發布的 CLI 裡留下讀者無法執行的死命令——要交待的 形式會在發布的 CLI 裡留下讀者無法執行的死命令——要交待的
是「沿用 pilot 捕捉」的事實(記於 lyrics-provenance.csv 與 是「沿用 pilot 捕捉」的事實(記於 lyrics-provenance.csv 與
論文方法節),不是工具本身;工具移出專案,發布管線即 論文方法節),不是工具本身;工具移出專案,發布管線即
@@ -178,8 +178,8 @@
來源公開可稽核,快照乾淨、overrides 縮小;論文方法節揭露 來源公開可稽核,快照乾淨、overrides 縮小;論文方法節揭露
「捕捉前經研究者查證補完」。性別以公開自我認同為準, 「捕捉前經研究者查證補完」。性別以公開自我認同為準,
推測不確定且無佐證者列疑慮清單;查證屬資料策展而非 推測不確定且無佐證者列疑慮清單;查證屬資料策展而非
分析,以 Claude Code 輔助、不走分析 APIQS 批次 分析,以 Claude Code 輔助、不走分析 APIQS 批次
`excludes/`私人工作檔,不隨論文發布 私人工作檔留存,不隨論文發布。
## 2026-08-04 ## 2026-08-04
@@ -227,7 +227,7 @@
- **Markdown 檔名一律以 dash 連接**decision-log.md、 - **Markdown 檔名一律以 dash 連接**decision-log.md、
research-plan.md、project-structure.md 等;與 data/ 層 research-plan.md、project-structure.md 等;與 data/ 層
CSV 檔名慣例一致),定義檔命名慣例同步改為 CSV 檔名慣例一致),定義檔命名慣例同步改為
`prompts/<task>-v<N>.md`(如 screen-v1.md),`excludes/` `prompts/<task>-v<N>.md`(如 screen-v1.md),版本庫外的
私人工作檔一併改名;`conference_abstract.md` 改名並搬入 私人工作檔一併改名;`conference_abstract.md` 改名並搬入
`paper/`——它是本次年會實際送出的摘要,與全文同屬投稿 `paper/`——它是本次年會實際送出的摘要,與全文同屬投稿
血脈,不是 `docs/` 的內部工作文件。全 repo 指涉同步更新。 血脈,不是 `docs/` 的內部工作文件。全 repo 指涉同步更新。
@@ -374,25 +374,3 @@
層;(3) 複雜仲裁機械無以自證其益(見前條)。驗證改為 層;(3) 複雜仲裁機械無以自證其益(見前條)。驗證改為
確定性格式檢查(完整分割、組名唯一、cap ≤ 50),違規 確定性格式檢查(完整分割、組名唯一、cap ≤ 50),違規
依協定修訂定義檔重跑。 依協定修訂定義檔重跑。
## 2026-08-05
- **歌詞捕捉加浮水印/誤碼正規化**:全語料普查(程式窮舉
非 ASCII)發現歌詞站浮水印與 cp1252 誤碼——同形字(西里爾
е 160 處/78 檔、希臘 ό 6 處)、異體空格(U+2005 211 處、
U+205F 80 處、U+200A 6 處)、零寬字元(U+200B 3 處)、C1
誤碼(U+0091/92/93/94/97 共 58 處;先前手修之 U+0085 同
類);受影響 81/883 檔全部來自 pilot 匯入,新抓取檔乾淨。
正規化規則四條(C1 依 cp1252 本義還原、同形字還原、異體
空格→ASCII 空格、零寬刪除)實作於 fetch-lyrics 的
normalize_lyrics(),寫入時套用;既有語料以同一函式一次性
套用。倒放行等歌曲本體內容不動——策展界線:清傳輸雜質,
不改內容。
- **Song 775 拒答機制查明與裁定為無法分析**:tag 首跑僅
此一首遭模型拒答。以隔離子代理做消融測試(四變體)
證實觸發源是歌詞中的倒放拼寫行(觸發混淆文字防護),
與歌詞中的種族蔑稱無關、與零寬字元亦無關(移除
U+200B 而保留倒放行仍拒答)。裁定:倒放行是歌曲的
忠實內容,保留不動——語料忠實優先於可分析性;該首
視為無法分析,缺漏歸因於 Anthropic 平台防護,非本
研究語料的問題。
+1 -1
View File
@@ -73,7 +73,7 @@ pop-fem-audit/
│ └── methodology.md # 方法細節(全文方法節底稿; │ └── methodology.md # 方法細節(全文方法節底稿;
│ # 映射分析方法須在看結果前寫定) │ # 映射分析方法須在看結果前寫定)
└── paper/ └── paper/
├── conference-abstract.md # pilot 摘要(投稿版) ├── abstract.md # 摘要
└── full-paper.md # 全文 └── full-paper.md # 全文
``` ```
+3 -7
View File
@@ -7,7 +7,7 @@
- **研究敘事**:全文為獨立完整的正式研究(2016–2025 十年語料 - **研究敘事**:全文為獨立完整的正式研究(2016–2025 十年語料
共 1000 筆榜單紀錄,凍結程序後全量執行),**不與先導研究 共 1000 筆榜單紀錄,凍結程序後全量執行),**不與先導研究
paper/conference-abstract.md做比較**;全文數字一律以正式研究 做比較**;全文數字一律以正式研究
結果為準。先導研究僅作為 codebook v0 與假說的內部來源, 結果為準。先導研究僅作為 codebook v0 與假說的內部來源,
記於決策日誌,不進入論文敘事。 記於決策日誌,不進入論文敘事。
- **執行原則**:主會話只做討論;所有分析由 deterministic script - **執行原則**:主會話只做討論;所有分析由 deterministic script
@@ -72,8 +72,8 @@
候選歌曲的歌手另做深度背景(族裔以公開自我認同為準、音樂 候選歌曲的歌手另做深度背景(族裔以公開自我認同為準、音樂
場景),script 輔助、人工核定。 場景),script 輔助、人工核定。
- **Pilot 歌詞沿用(私人匯入,不進發布管線)**:先導研究 - **Pilot 歌詞沿用(私人匯入,不進發布管線)**:先導研究
捕捉檔(lyrics.json684 首,20182025)以 `excludes/` 捕捉檔(lyrics.json684 首,20182025)以私人腳本
私人腳本匯入歌詞快取——只取識別欄位與歌詞本文,以 匯入歌詞快取——只取識別欄位與歌詞本文,以
(year, rank) 精確匹配 song_id。匯入工具不屬於專案交付物 (year, rank) 精確匹配 song_id。匯入工具不屬於專案交付物
(讀者拿不到其輸入),讀者的重現路徑純粹是 `fetch-lyrics` (讀者拿不到其輸入),讀者的重現路徑純粹是 `fetch-lyrics`
沿用之**事實**記於 `data/captures/lyrics-provenance.csv`(進 git): 沿用之**事實**記於 `data/captures/lyrics-provenance.csv`(進 git):
@@ -165,14 +165,10 @@
與「貶低其他女性/以男性指標定義權力」。 與「貶低其他女性/以男性指標定義權力」。
- 流程:v0 → 校準樣本試編 → 修訂 → 凍結 v1 → 全量編碼; - 流程:v0 → 校準樣本試編 → 修訂 → 凍結 v1 → 全量編碼;
中途修訂須記決策日誌並回檢已編歌曲。 中途修訂須記決策日誌並回檢已編歌曲。
- Positionality statement 寫入論文(53 歲、長年婦運者、資深
工程師、離開學術圈多年、因整理榜單而起)。
## 其他已定案事項 ## 其他已定案事項
- 歌詞受版權保護:完整歌詞不進 git(`data/captures/lyrics/` gitignored), - 歌詞受版權保護:完整歌詞不進 git(`data/captures/lyrics/` gitignored),
論文與 repo 只留分類所引摘錄。 論文與 repo 只留分類所引摘錄。
- 工程支援(寫 script、style check、審稿)用公司訂閱的
Claude Code;進論文的分析 token 由個人 API 帳號支付。
- Fable 5 不用於 pipeline:成本高、thinking 無法關閉且不可稽核、 - Fable 5 不用於 pipeline:成本高、thinking 無法關閉且不可稽核、
無 temperature 控制,且會混淆「盲點是提示問題」的核心主張。 無 temperature 控制,且會混淆「盲點是提示問題」的核心主張。
-2
View File
@@ -14,8 +14,6 @@ Rules:
- For each keyword, quote 1 to 3 verbatim lines of the lyrics - For each keyword, quote 1 to 3 verbatim lines of the lyrics
that ground it; do not give a keyword you cannot ground. that ground it; do not give a keyword you cannot ground.
Do not wrap the output in a Markdown code fence.
Output a single JSON object mapping each keyword to its list Output a single JSON object mapping each keyword to its list
of quotes, and nothing else: of quotes, and nothing else:
-2
View File
@@ -17,8 +17,6 @@ Rules:
by hyphens, that best names the theme it gathers. Group by hyphens, that best names the theme it gathers. Group
names must be unique. names must be unique.
Do not wrap the output in a Markdown code fence.
Output a single JSON object mapping each group name to the Output a single JSON object mapping each group name to the
list of its member keywords, and nothing else: list of its member keywords, and nothing else:
-2
View File
@@ -20,8 +20,6 @@ Rules:
by hyphens, that best names the theme it gathers. Group by hyphens, that best names the theme it gathers. Group
names must be unique. names must be unique.
Do not wrap the output in a Markdown code fence.
Output a single JSON object mapping each group name to the Output a single JSON object mapping each group name to the
list of its member keywords, and nothing else: list of its member keywords, and nothing else:
-2
View File
@@ -21,8 +21,6 @@ Rules:
the lyrics that ground it; do not assign a keyword you the lyrics that ground it; do not assign a keyword you
cannot ground. cannot ground.
Do not wrap the output in a Markdown code fence.
Output a single JSON object mapping each assigned keyword to Output a single JSON object mapping each assigned keyword to
its list of quotes (an empty object when no keyword applies), its list of quotes (an empty object when no keyword applies),
and nothing else: and nothing else:
-2
View File
@@ -30,8 +30,6 @@ Rules:
lines of the lyrics; quote them. lines of the lyrics; quote them.
- Do not add any keyword that is not listed. - Do not add any keyword that is not listed.
Do not wrap the output in a Markdown code fence.
Output a single JSON object mapping each kept keyword to your Output a single JSON object mapping each kept keyword to your
list of quotes, and nothing else. A dropped keyword is simply list of quotes, and nothing else. A dropped keyword is simply
left out; output an empty object when nothing is kept: left out; output an empty object when nothing is kept:
-2
View File
@@ -12,8 +12,6 @@ Rules:
lines of the lyrics that ground the judgment; if you cannot lines of the lyrics that ground the judgment; if you cannot
ground it in a quote, the answer is no. ground it in a quote, the answer is no.
Do not wrap the output in a Markdown code fence.
Output a single JSON array and nothing else: the quoted Output a single JSON array and nothing else: the quoted
lines when the song expresses the theme, or an empty array lines when the song expresses the theme, or an empty array
when it does not: when it does not:
-2
View File
@@ -21,8 +21,6 @@ Rules:
lines of the lyrics that ground the judgment; if you cannot lines of the lyrics that ground the judgment; if you cannot
ground it in a quote, the answer is no. ground it in a quote, the answer is no.
Do not wrap the output in a Markdown code fence.
Output a single JSON array and nothing else: the quoted Output a single JSON array and nothing else: the quoted
lines when the song expresses the theme, or an empty array lines when the song expresses the theme, or an empty array
when it does not: when it does not:
@@ -57,49 +57,6 @@ SLEEP_SECONDS: float = 1.0
"""The delay between consecutive HTTP requests, in seconds.""" """The delay between consecutive HTTP requests, in seconds."""
def _build_normalization() -> dict[int, str | None]:
"""Build the lyrics normalization translation table.
:return: The codepoint-to-replacement mapping, a replacement
of None meaning removal.
"""
table: dict[int, str | None] = {}
codepoint: int
for codepoint in range(0x80, 0xa0):
try:
table[codepoint] = bytes([codepoint]).decode("cp1252")
except UnicodeDecodeError:
table[codepoint] = None
table[0x0435] = "e"
table[0x03cc] = "ó"
for codepoint in (0x2005, 0x205f, 0x200a):
table[codepoint] = " "
for codepoint in (0x200b, 0x200c, 0x200d, 0xfeff):
table[codepoint] = None
return table
NORMALIZATION: dict[int, str | None] = _build_normalization()
"""The codepoint-to-replacement mapping applied to fetched
lyrics: cp1252-mojibake restoration for U+0080-U+009F (with the
five byte values undefined in cp1252 removed), homoglyph
restoration for the Cyrillic "e" and the Greek "o" with tonos,
ASCII-space restoration for exotic space variants, and removal
of zero-width characters. A replacement of None removes the
codepoint."""
def normalize_lyrics(text: str) -> str:
"""Restore or remove watermark and mojibake characters.
:param text: The lyrics text as fetched from an API.
:return: The text with the codepoints in
:data:`NORMALIZATION` replaced or removed; every other
character is unchanged.
"""
return text.translate(NORMALIZATION)
def parse_args(argv: list[str] | None) -> argparse.Namespace: def parse_args(argv: list[str] | None) -> argparse.Namespace:
"""Parse the command-line arguments. """Parse the command-line arguments.
@@ -230,9 +187,6 @@ def save_lyrics(lyrics_dir: Path, song_id: int,
The cache directory is created when missing. The cache directory is created when missing.
The lyrics text is normalized with :func:`normalize_lyrics`
before being written.
:param lyrics_dir: The lyrics cache directory. :param lyrics_dir: The lyrics cache directory.
:param song_id: The song ID. :param song_id: The song ID.
:param lyrics: The lyrics text. :param lyrics: The lyrics text.
@@ -241,7 +195,7 @@ def save_lyrics(lyrics_dir: Path, song_id: int,
""" """
lyrics_dir.mkdir(parents=True, exist_ok=True) lyrics_dir.mkdir(parents=True, exist_ok=True)
(lyrics_dir / f"{song_id}.txt").write_text( (lyrics_dir / f"{song_id}.txt").write_text(
normalize_lyrics(lyrics), encoding="utf-8") lyrics, encoding="utf-8")
def append_provenance(path: Path, song_id: int, def append_provenance(path: Path, song_id: int,
-73
View File
@@ -317,79 +317,6 @@ class TestFetchLyrics(unittest.TestCase):
self.assertEqual([x[0] for x in rows[1:]], self.assertEqual([x[0] for x in rows[1:]],
["1", "2", "2"]) ["1", "2", "2"])
def test_normalize_cp1252_mojibake(self) -> None:
"""Test that cp1252 mojibake codepoints are restored."""
self.assertEqual(
fetch_lyrics.normalize_lyrics("wait…"),
"wait…")
self.assertEqual(
fetch_lyrics.normalize_lyrics(
"‘quote’"),
"quote")
self.assertEqual(
fetch_lyrics.normalize_lyrics(
"“quote”"),
"“quote”")
self.assertEqual(
fetch_lyrics.normalize_lyrics("dash—line"),
"dash—line")
def test_normalize_undefined_cp1252_removed(self) -> None:
"""Test that undefined cp1252 byte values are removed."""
text: str = ("abcdef")
self.assertEqual(
fetch_lyrics.normalize_lyrics(text), "abcdef")
def test_normalize_homoglyphs(self) -> None:
"""Test that watermark homoglyphs are restored."""
self.assertEqual(
fetch_lyrics.normalize_lyrics("likе that"),
"like that")
self.assertEqual(
fetch_lyrics.normalize_lyrics("lό que soy"),
"ló que soy")
def test_normalize_space_variants(self) -> None:
"""Test that exotic space variants become ASCII space."""
self.assertEqual(
fetch_lyrics.normalize_lyrics(
"abcd"),
"a b c d")
def test_normalize_zero_width_removed(self) -> None:
"""Test that zero-width characters are removed."""
text: str = (
"abcde")
self.assertEqual(
fetch_lyrics.normalize_lyrics(text), "abcde")
def test_normalize_ascii_unchanged(self) -> None:
"""Test that plain ASCII text passes through unchanged."""
text: str = "Hello, it's me\n"
self.assertEqual(
fetch_lyrics.normalize_lyrics(text), text)
def test_normalize_legitimate_non_ascii_unchanged(self) -> None:
"""Test that legitimate non-ASCII content is unchanged."""
text: str = "¿cómo estás? 안녕하세요\n"
self.assertEqual(
fetch_lyrics.normalize_lyrics(text), text)
def test_fetched_lyrics_saved_normalized(self) -> None:
"""Test that a fetched lyric is normalized before saving."""
self.__seed([("Hello", "Adele")])
with mock.patch(
"urllib.request.urlopen",
side_effect=[self.__response(
{"lyrics": "wait… likе"
" thatnow\n"})]):
status: int = self.__run_fetch()[0]
self.assertEqual(status, 0)
self.assertEqual(
(self.__lyrics / "1.txt")
.read_text(encoding="utf-8"),
"wait… like that now\n")
def test_no_store_fails(self) -> None: def test_no_store_fails(self) -> None:
"""Test that a missing working store fails the run.""" """Test that a missing working store fails the run."""
urlopen: mock.Mock urlopen: mock.Mock