9 Commits
Author SHA1 Message Date
imacat 502d8b6b5a Enforce strict validation of configuration settings 2026-08-21 08:12:17 +08:00
imacat 47bef84e7b Rename SQLALCHEMY_DATABASE_URL to SQLALCHEMY_DATABASE_URI to follow SQLAlchemy conventions. 2026-08-21 08:10:21 +08:00
imacatandClaude Fable 5 9b6450fcbb Capture the stderr summary in the request-preview tests
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-20 23:17:26 +08:00
imacat 309667e82b Add type hints and refactor SQLite foreign key enforcement setup 2026-08-20 23:17:22 +08:00
imacat 195605826f Advance tools to version 1.0.0. 2026-08-19 20:11:29 +08:00
imacat e569b3481c Add change log. 2026-08-19 20:10:38 +08:00
imacat f0547208ed Update README.md. 2026-08-19 20:05:15 +08:00
imacatandClaude Opus 5 eef434acf7 Gather the runs and the results under data/
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 20:01:59 +08:00
imacatandClaude Opus 5 7a625d50fc Trim the docs to their own jobs
The methodology keeps only the method that reaches the paper's
conclusions; the research plan becomes the proposal minus its
externalized method chapter; the standing rules move to a
Chinese conventions.md; the project-structure inventory, a
hand-kept mirror of the tree itself, is deleted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 20:01:54 +08:00
91 changed files with 91 additions and 73 deletions
+10 -2
View File
@@ -1,6 +1,6 @@
# 流行音樂中「女性力量」語彙的挪用與污染——以 Billboard Year-End Hot 1002016-2025)為例的內容分析
這是研究論文《流行音樂中「女性力量」語彙的挪用與污染——以 Billboard Year-End Hot 1002016-2025)為例的內容分析》的專案資料,包括論文本身、文件、紀錄、資料、工具程式、AI提示詞,等等。
這是研究論文《流行音樂中「女性力量」語彙的挪用與污染——以 Billboard Year-End Hot 1002016-2025)為例的內容分析》的專案資料,包括論文本身、文件、紀錄、資料、工具程式、LLM提示詞,等等。
## 論文和摘要
@@ -10,9 +10,17 @@
研究方法、記錄等文件,請參閱 docs/ 資料夾。
## LLM提示
LLM提示請參閱 prompts/ 資料夾。
## 輔助工具程式
輔助工具程式請參閱 tools/ 資料夾。
輔助工具程式請參閱 tools/ 資料夾。
## 研究資料
研究資料請參閱 data/ 資料夾。
## 授權
Can't render this file because it is too large.
+9 -6
View File
@@ -31,11 +31,11 @@ agent 不得見之)。凡於本專案工作的主會話,動手管線、
不符預期時,修訂定義檔並重複該循環;絕不手改結果。
- 一步的每次執行皆自我完備歸檔於 `run-llm` 命令列上
明示指定的目的目錄下(慣例為
`runs/<步驟>/run<N>/`):定義檔快照、原始輸出與
`data/runs/<步驟>/run<N>/`):定義檔快照、原始輸出與
`meta.json`model ID、參數、時間戳、batch ID)。
一步的 N 次執行即 N 次各自的 `run-llm` 呼叫。覆蓋
既有執行歸檔須明示旗標;被取代的執行留在 git 歷史。
確定性步驟歸檔於 `runs/<步驟>/`,不分 `run<N>` 層。
確定性步驟歸檔於 `data/runs/<步驟>/`,不分 `run<N>` 層。
- 每次 `run-llm` 執行的 token 用量與費用記入
`docs/run-costs.md`,與執行歸檔同一 commit。
- 腳本自環境變數 `ANTHROPIC_API_KEY` 讀取 API key
@@ -46,19 +46,22 @@ agent 不得見之)。凡於本專案工作的主會話,動手管線、
- `data/source/` 存手放後不動的原始檔;
`data/captures/` 只由 fetch 命令與私人匯入腳本
寫入;`data/manual/` 只由研究者親手寫入;
`data/derived/` 只由 `build-db` 子命令寫入
`data/derived/` 只由 `build-db` 子命令寫入
`data/runs/` 存 LLM 執行的原始歸檔,只由執行程序
寫入;`data/results/` 存論文引用的定案表,只由
計票程序寫入。
- 歌詞全文有版權:一律置於 `data/captures/lyrics/`
gitignored),絕不 commit,亦絕不於 repo 任何處
全文重現。
## 文件
- `results/` 存計票後的定案表(論文所引);`runs/`
原始稽核紀錄。論文只引 `results/`
- `data/results/` 存計票後的定案表(論文所引);
`data/runs/`原始稽核紀錄。論文只引 `data/results/`
- **Commit 判準**:凡能由「committed 的輸入+committed
的程式」決定性再生者不 commit;凡不能者一律以文字
格式 commit,格式跟著上文「資料規則」一節所定的
層次走。例外:`results/` 定案表與
層次走。例外:`data/results/` 定案表與
`data/derived/` 人讀報表雖可再生仍 commit——理由是
引用穩定性、審稿人零門檻、撰稿期數字變動可 diff;
兩者皆與工作儲存同一動作產出,稽核鏈無中間空缺。
+1 -1
View File
@@ -1,7 +1,7 @@
# LLM 輸出的契約查核
2026-08-06 量測。對象為步驟 3 的三份執行歸檔
`runs/3a-code/run1``run3`,共 883 首歌、44,149 筆標籤
`data/runs/3a-code/run1``run3`,共 883 首歌、44,149 筆標籤
指派、44,146 句引述。歌詞以模型實際看到的那一份為準,即
`tools/instance/llm-input-code.jsonl`。)
+14 -29
View File
@@ -42,52 +42,37 @@ frame-aware 提示修正實驗,均由當時協作的 Claude Code 設計並
研究問題。
- **粒度選擇**:正式研究鎖定 thematic keywords 這一粒度,
係繼承先導研究三種粒度的比較結果(keywords 過碎、themes
過早抽象),且於執行前鎖定以防事後擇優
過早抽象)。
- **`women-power` 一詞的來歷**:考據先導研究的 local agent
存檔可知,其第一步指令含數十個範例 thematic keywords,
其中即有 women-power——為當時協作的 Claude Code 依研究者
長期表達的關注主動加入(研究者端播種,非明示指定);先導
的標籤 `women-power-and-empowerment` 則是第三步強制合併
兩個關鍵字的管線人工產物。正式研究的探針因此指向被播種的
本詞 `women-power`,不用合併假影(見 `decision-log.md`
2026-08-04 條)。**附帶認清:先導第一步並非零語意提示,
此即正式研究「提示詞只定格式、不定語意」設計所矯正者。**
兩個關鍵字的管線人工產物(見 `decision-log.md` 2026-08-04
條)。
- **簿記容量的教訓**:先導研究九百餘詞可以在單一回應內完成
分組,正式研究的關鍵字規模已非此法所能及,詞彙表改由
確定性程序產生(見 `decision-log.md` 2026-08-06 條)。
分組;此法未沿用,詞彙表改由確定性程序產生(見
`decision-log.md` 2026-08-06 條)。
## 四、被棄用的成果
以下先導研究的產物未進入正式研究,論文亦未引用:
- **genuineperipheralfake 三分類與「44% 假女性力量」**:
構念與分母皆與正式研究不同(正式研究為 145 首女性力量群
歌曲中 111 首有性別問題),兩者不可對讀
- **五種「假女性力量」類型(A–E)**:正式研究改由儀器分三個
發話脈絡各自歸納,得 43 條問題樣態(`results/patterns.csv`)。
- **pussy 一詞的修辭分類**:正式研究範圍收斂至語彙的挪用與
污染,未納入。
構念與分母均未經稽核回溯,未沿用。
- **五種「假女性力量」類型(A–E)**:未沿用
- **pussy 一詞的修辭分類**:未沿用。
- **frame-aware(框架感知)提示修正法**:先導研究以框架判準
寫進提示以提高準確率;正式研究刻意不定義編碼,因為研究
對象正是 LLM 未受引導的自然編碼——把框架寫進提示,即無法
再以其輸出為批判對象。兩者的設計方向相反,故未沿用
- **三個獨立 LLM subagent+人工仲裁的流程**:正式研究改以
Anthropic API 逐首獨立呼叫,原因見 `decision-log.md`
2026-07-30 條。
寫進提示以提高準確率;未沿用。
- **三個獨立 LLM subagent+人工仲裁的流程**:未沿用,原因見
`decision-log.md` 2026-07-30 條
## 五、它如何促成正式研究的設計
## 五、它促成了可稽核的設計
先導研究的根本限制不在結論對錯,而在**不可稽核**:沒有定義檔
快照、沒有原始輸出歸檔、沒有參數紀錄,因此任何一個數字都無法
回溯到產生它的那一次執行。正式研究的幾項設計正是針對這一點:
- 所有 LLM 步驟改以 API script 執行,每次執行自我完備歸檔於
`runs/`;
- 可逐項機械比對的判斷採三次執行+多數決,並量測其穩定性
(見 `reliability.md`);
- 定義檔只規定任務形狀,不給主題定義、判準或範例;
- 每一筆編碼必附歌詞引述,供逐筆查核;
- 輸出的契約遵從另行查核(見 `output-validation.md`)。
回溯到產生它的那一次執行。正式研究的可稽核設計,正是針對這一點
而立。
換言之,正式研究對 LLM 輸出所採取的「不信任、須查核」立場,
其第一個案例就是先導研究本身。
+2 -2
View File
@@ -3,7 +3,7 @@
# Authors:
# imacat@mail.imacat.idv.tw (imacat), 2026/7/31
# The SQLAlchemy database URL.
SQLALCHEMY_DATABASE_URL="postgresql://user:password@host/db"
# The SQLAlchemy database URI.
SQLALCHEMY_DATABASE_URI="postgresql://user:password@host/db"
# The Anthropic API key
ANTHROPIC_API_KEY=sk-ant-...
+10
View File
@@ -0,0 +1,10 @@
Change Log
==========
version 1.0.0
-------------
Released 2026/8/19
Initial release.
+2
View File
@@ -13,6 +13,8 @@ This is a collection of supporting tools for the conference paper "流行音樂
:maxdepth: 2
:caption: Contents:
changelog
Indices and tables
==================
+1 -1
View File
@@ -6,5 +6,5 @@
"""Tools for A Feminist Audit of Pop Music."""
VERSION: str = "0.0.0"
VERSION: str = "1.0.0"
"""The package version."""
+2 -2
View File
@@ -14,12 +14,12 @@ class Settings(BaseSettings):
"""The application name."""
admin_email: str = "imacat@mail.imacat.idv.tw"
"""The administrator email address."""
SQLALCHEMY_DATABASE_URL: str
SQLALCHEMY_DATABASE_URI: str
"""The SQLAlchemy database URL."""
ANTHROPIC_API_KEY: str
"""The Anthropic API key."""
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
model_config = SettingsConfigDict(env_file=".env")
"""The model configuration."""
+30 -21
View File
@@ -7,10 +7,11 @@
"""
from functools import cached_property
from pathlib import Path
from typing import Any
import sqlalchemy as sa
from sqlalchemy.engine.interfaces import DBAPICursor, DBAPIConnection
from sqlalchemy.orm import DeclarativeBase, sessionmaker, Session
from sqlalchemy.pool import ConnectionPoolEntry
from .config import Settings, get_settings
@@ -29,7 +30,7 @@ class DataSource:
:return: The database engine.
"""
settings: Settings = get_settings()
return self.__create_engine(settings.SQLALCHEMY_DATABASE_URL)
return self.__create_engine(settings.SQLALCHEMY_DATABASE_URI)
@cached_property
def __session_local(self) -> sessionmaker:
@@ -51,9 +52,6 @@ class DataSource:
def __create_engine(cls, url: str) -> sa.Engine:
"""Constructs and returns the database engine.
The foreign key enforcement is enabled on every connection
of a SQLite engine.
:param url: The SQLAlchemy database URL.
:return: The database engine.
"""
@@ -65,24 +63,10 @@ class DataSource:
poolclass=sa.StaticPool)
else:
engine = sa.create_engine(url)
if engine.url.get_backend_name() == "sqlite":
sa.event.listen(engine, "connect",
cls.__enable_sqlite_foreign_keys)
if engine.dialect.name == "sqlite":
cls.__enable_sqlite_foreign_keys(engine)
return engine
@staticmethod
def __enable_sqlite_foreign_keys(dbapi_connection: Any,
_: Any) -> None:
"""Enables the foreign key enforcement on a new connection.
:param dbapi_connection: The DBAPI connection.
:param _: The connection record (unused).
:return: None.
"""
cursor: Any = dbapi_connection.cursor()
cursor.execute("PRAGMA foreign_keys=ON")
cursor.close()
@staticmethod
def __resolve_sqlite_relative_url(url: str) -> str:
"""Resolves the SQLite relative URL to the instance folder.
@@ -101,6 +85,31 @@ class DataSource:
path = base / "instance" / path
return f"sqlite:///{path}"
@staticmethod
def __enable_sqlite_foreign_keys(engine: sa.Engine) -> None:
"""Turns on the foreign key enforcement of SQLite.
The ``foreign_keys`` pragma is turned on for every
connection of the engine, so that the ``ON DELETE``
actions of the schema run.
:param engine: The SQLite database engine.
:return: None.
"""
def on_connect(dbapi_connection: DBAPIConnection,
_: ConnectionPoolEntry) -> None:
"""Turns on the pragma on a new connection.
:param dbapi_connection: The DB-API connection.
:param _: The connection record (unused).
:return: None.
"""
cursor: DBAPICursor = dbapi_connection.cursor()
cursor.execute("PRAGMA foreign_keys=ON")
cursor.close()
sa.event.listen(engine, "connect", on_connect)
ds: DataSource = DataSource()
"""The data source."""
+1 -1
View File
@@ -278,7 +278,7 @@ class TestBuildDB(unittest.TestCase):
self.__annotations: Path = self.__dir / "annotations.csv"
self.__write_chart(self.CHART_CSV)
config.set_settings(config.Settings(
SQLALCHEMY_DATABASE_URL="sqlite://",
SQLALCHEMY_DATABASE_URI="sqlite://",
ANTHROPIC_API_KEY="test-key"))
self.__ds: DataSource = DataSource()
self.addCleanup(self.__ds.engine.dispose)
+1 -1
View File
@@ -36,7 +36,7 @@ class TestExportLlmInput(unittest.TestCase):
self.__dir: Path = Path(tmp.name)
self.__output: Path = self.__dir / "llm-input.jsonl"
config.set_settings(config.Settings(
SQLALCHEMY_DATABASE_URL="sqlite://",
SQLALCHEMY_DATABASE_URI="sqlite://",
ANTHROPIC_API_KEY="test-key"))
self.__ds: DataSource = DataSource()
self.addCleanup(self.__ds.engine.dispose)
+1 -1
View File
@@ -45,7 +45,7 @@ class TestFetchArtists(unittest.TestCase):
self.__snapshot: Path = \
self.__dir / "artists_wikidata.csv"
config.set_settings(config.Settings(
SQLALCHEMY_DATABASE_URL="sqlite://",
SQLALCHEMY_DATABASE_URI="sqlite://",
ANTHROPIC_API_KEY="test-key"))
self.__ds: DataSource = DataSource()
self.addCleanup(self.__ds.engine.dispose)
+1 -1
View File
@@ -44,7 +44,7 @@ class TestFetchLyrics(unittest.TestCase):
self.__provenance: Path = \
self.__dir / "lyrics-provenance.csv"
config.set_settings(config.Settings(
SQLALCHEMY_DATABASE_URL="sqlite://",
SQLALCHEMY_DATABASE_URI="sqlite://",
ANTHROPIC_API_KEY="test-key"))
self.__ds: DataSource = DataSource()
self.addCleanup(self.__ds.engine.dispose)
+1 -1
View File
@@ -27,7 +27,7 @@ class TestModels(unittest.TestCase):
def setUp(self) -> None:
"""Create the schema on an in-memory SQLite database."""
config.set_settings(config.Settings(
SQLALCHEMY_DATABASE_URL="sqlite://",
SQLALCHEMY_DATABASE_URI="sqlite://",
ANTHROPIC_API_KEY="test-key"))
self.__ds: DataSource = DataSource()
self.addCleanup(self.__ds.engine.dispose)
+3 -2
View File
@@ -188,7 +188,8 @@ class TestRequestBuilding(RunLLMTestCase):
:return: The parsed request.
"""
stdout: io.StringIO = io.StringIO()
with redirect_stdout(stdout):
stderr: io.StringIO = io.StringIO()
with redirect_stdout(stdout), redirect_stderr(stderr):
run_llm.main([
str(self.__prompt), str(self.__input),
str(self.__archive_dir), "--dry-run"] + extra_argv)
@@ -239,7 +240,7 @@ class TestMainFlow(RunLLMTestCase):
str(self.__prompt), str(self.__input),
str(self.__archive_dir)]
self.__settings: config.Settings = config.Settings(
SQLALCHEMY_DATABASE_URL="sqlite://",
SQLALCHEMY_DATABASE_URI="sqlite://",
ANTHROPIC_API_KEY="test-key")
config.set_settings(self.__settings)
+1 -1
View File
@@ -50,7 +50,7 @@ class TestTallyAnnotations(unittest.TestCase):
run_dir.mkdir()
self.__runs.append(run_dir)
config.set_settings(config.Settings(
SQLALCHEMY_DATABASE_URL="sqlite://",
SQLALCHEMY_DATABASE_URI="sqlite://",
ANTHROPIC_API_KEY="test-key"))
self.__ds: DataSource = DataSource()
self.addCleanup(self.__ds.engine.dispose)
+1 -1
View File
@@ -39,7 +39,7 @@ class TestTallyCodings(unittest.TestCase):
self.__output_csv: Path \
= self.__dir / "results" / "codings.csv"
config.set_settings(config.Settings(
SQLALCHEMY_DATABASE_URL="sqlite://",
SQLALCHEMY_DATABASE_URI="sqlite://",
ANTHROPIC_API_KEY="test-key"))
self.__ds: DataSource = DataSource()
self.addCleanup(self.__ds.engine.dispose)