Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
91ca9e4
chore: SDD ワークスペースを gitignore する
wharfe Sep 22, 2026
3993b66
feat(articles): 条番号の slug と日本語表記の変換を足す
wharfe Sep 22, 2026
a53c106
feat(articles): 出荷済み diff から対象条文と改正履歴を集める
wharfe Sep 22, 2026
91663d9
feat(articles): 今日の本文を索引し、削除条文の欠番まで解決する
wharfe Sep 22, 2026
3a5a2b7
feat(articles): 本文と解説の版整合ゲートを足す
wharfe Sep 22, 2026
e89c247
fix(articles): 版整合ゲートを空リスト同士の一致から閉じる
wharfe Sep 22, 2026
1783d07
feat(articles): 関連条文を条名で解決し、他法令・附則・自己参照を弾く
wharfe Sep 22, 2026
ca81384
feat(articles): 法令単位 JSON の構築と保存前検証
wharfe Sep 22, 2026
5c8785b
fix(articles): 曖昧な別名・空の旧条文・slug不整合を締める
wharfe Sep 22, 2026
c7938e1
fix(articles): slug検証の except を握りつぶさず報告する
wharfe Sep 22, 2026
45da607
feat(articles): main を足す(取得失敗で止まる・日付が変わったら保存しない)
wharfe Sep 22, 2026
e20f715
fix(articles): main の CLI 誤入力を新設 exit 5 で締める
wharfe Sep 22, 2026
8d09238
feat(articles): 条文ページ用データを生成し、出荷データ検査を足す
wharfe Sep 22, 2026
055fccb
fix(articles): 本文が「削除」だけの条を改正typeによらず墓標として扱う
wharfe Sep 22, 2026
966925b
docs: 条文ページ作業の引き継ぎと、記述のズレを直す
wharfe Sep 22, 2026
932bf7f
feat(articles): 条文ページのルートとコンポーネントを足す
wharfe Sep 22, 2026
8541f95
feat(articles): 法令ページ・差分ページ・sitemap から条文ページへの導線を足す
wharfe Sep 22, 2026
e38fcb7
fix(articles): 条文ページの Gate 3 指摘6件を根で直す
wharfe Sep 22, 2026
81f3ed3
fix(articles): 複数条の接続語を数え直し、時点ラベルを fail-closed にする
wharfe Sep 22, 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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ data/raw/
data/diffs/
data/timelines/
data/proposers/
data/articles/

# Node
frontend/node_modules/
Expand All @@ -32,3 +33,6 @@ frontend/out/

# Agent tooling (local settings, per-machine memory)
.claude/

# SDD workspace (plan ledger, briefs, review packages)
.superpowers/
8 changes: 7 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ uv run python scripts/diff.py <law_id> <date_before> <date_after>
uv run python scripts/timeline.py <law_id> # Amendment history for /law/<law_id>
uv run python scripts/explainer.py <law_id> # Plain-language "recent amendments" section
uv run python scripts/law_summary.py <law_id> # /law overview — fetches today's text itself
uv run python scripts/articles.py --all # Per-article pages — fetches today's text itself
uv run pytest # Python tests

# Frontend (in frontend/ directory)
Expand All @@ -48,6 +49,7 @@ npm run lint # Lint
│ ├── explainer.py AI-generated "recent amendments" section (see below)
│ ├── llm.py Shared Claude API helpers for the three scripts above
│ ├── lawtext.py Reading the e-Gov law tree (shared by diff/law_summary)
│ ├── articles.py Per-article pages (current text + this article's history)
│ └── requirements.txt Python dependencies (legacy, use pyproject.toml)
├── tests/ pytest suite — the pure functions in scripts/, plus
│ test_shipped_data.py over frontend/public/data/
Expand All @@ -56,7 +58,8 @@ npm run lint # Lint
│ ├── raw/ Raw API responses
│ ├── diffs/ Computed diff JSON files
│ ├── timelines/ Amendment history per law
│ └── proposers/ Bill sponsor data from the NDL API
│ ├── proposers/ Bill sponsor data from the NDL API
│ └── articles/ Per-article pages (current text + history)
└── frontend/ Next.js application
├── app/ Pages and layouts
├── components/ React components
Expand Down Expand Up @@ -169,6 +172,9 @@ blank field, because a typo would silently revert a page that took months to ran

```
e-Gov API v2 → fetch.py (raw JSON) → diff.py (structured diff) → frontend/public/data/

articles.py reads the shipped diffs back and pairs each amended article with
today's text, fetched in the same run → frontend/public/data/articles/
```

## API Notes
Expand Down
84 changes: 43 additions & 41 deletions HANDOFF.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,61 +2,63 @@

<!-- /wrap-up が更新する引き継ぎファイル。単一ファイルを上書き更新(履歴は vault にある) -->

- 更新: 2026-09-22 (JST)
- ブランチ: `main`(push 済み `f94fafe`。PR なし・実装未着手)
- 更新: 2026-09-22 22:30 (JST)
- ブランチ: `feat/article-pages`(main から 15 commits。**追跡リモート未設定・PR なし**)

## ゴール

issue #4 — `/diff` がサイト最大の表示回数を集めながらクリック 0 の状態を解く。
条文 1 本 = 1 ページ(現在 163 枚)を新設し、「現在の条文・いつどう変わったか・平易な解説・
関連条文」を 1 画面にまとめて、条文閲覧の検索意図に応える。
条文 1 本 = 1 ページを新設し、「現在の条文・いつどう変わったか・平易な解説・関連条文」を
1 画面にまとめて、条文閲覧の検索意図に応える。

## 現在地

- **Gate 1 完了・Gate 2 完了(上限 3 周で打ち切り)。実装は 1 行も書いていない。**
- 確定仕様: `docs/design-debate/article-pages/spec.md`
- 実装計画: `docs/superpowers/plans/2026-09-22-article-pages.md`(10 タスク・TDD・
全ステップに実コード・赤→緑の確認コマンド付き。zero context の実装者向けに書いてある)
- Gate 1 の記録は同じディレクトリの `brief.md` / `design-claude.md` / `design-codex.md` /
`critique-of-claude.md` / `critique-of-codex.md` / `verdict.md`
- Gate 2 の記録は `gate2-grok.md`(周回1)/ `gate2-grok-r2.md`(周回2)/ `gate2-grok-r3.md`(周回3)と、
計画ファイル末尾の 3 つの「Gate 2 の記録」節
**計画 10 タスク中 8 つ完了。Python パイプラインと実データは出来ている。フロントエンドが未着手。**

**仕様の中心は 2 つだけ憶えておけばよい:**

1. **版整合ゲート**(spec §4)— 出荷済み diff は法令から 5〜24 か月遅れている。
解説が書かれた時点の条文と今日の条文を照合し、**不一致なら解説ブロックを丸ごと落とす**
(実測: 162 条中 30 条が該当)。関連条文リンクも同じゲートに掛かる
2. **フィールドの出所表**(spec §5)— 「現在」と名乗るブロックに出るものは、
今日の取得由来か、ゲートを通ったものだけ。新しいフィールドは表に行を足してから実装する
- `scripts/articles.py` 完成(純粋関数 + `main()`)。`tests/test_articles.py` 78 件
- 実データ生成済み: `frontend/public/data/articles/*.json` = **11 法令・163 条文**
(労働基準法は本則の改正が 0 件のためファイル無し。これは正常)
- うち **36 件**は版不一致で `current_summary` が null(解説を出さない)
- リンク 497 本、うちページを持つものが 375 本
- `tests/test_shipped_data.py` に出荷データ検査を追加。`uv run pytest` = **267 件緑**
- 実装計画: `docs/superpowers/plans/2026-09-22-article-pages.md` の **Task 9 から**
- 実行記録・裁定の全リスト: `.superpowers/sdd/2026-09-22-article-pages/progress.md`
(git 管理外。`git clean -fdx` で消えるので、必要なら先に読む)

## 次の一手

1. `docs/superpowers/plans/2026-09-22-article-pages.md` の **Task 1** から実装する。
`superpowers:subagent-driven-development`(タスクごとに fresh subagent + 間でレビュー)を推奨
2. Task 8 で実データを生成する(e-Gov へ 11 リクエスト)。`LookupError` が出たら**止まる** —
「施行済みの改正があるのに今日の本文に無い」条文なので、条番号と法令を報告して人に上げる
3. 全タスク後に受け入れコマンドを通す:
1. `superpowers:subagent-driven-development` で **Task 9**(フロントエンドの型・読み出し・
ページ本体)から再開する。ledger が `.superpowers/sdd/2026-09-22-article-pages/progress.md`
にあるので、まずそれを読む(完了タスクを再実行しないため)
2. **Task 9 の計画コードはそのまま書くと壊れる。** 下の「注意」の 1 番を必ず反映する
3. Task 10(`/law` の条文一覧・`/diff` カードの anchor とリンク・sitemap)
4. 受け入れコマンドを通す:
`uv run pytest` → `uv run python scripts/articles.py --all` →
`uv run pytest tests/test_shipped_data.py` → `cd frontend && npm run lint && npm run build`
4. 生成 JSON の slug 総数と `frontend/out/law/*/article/*` の HTML 数が一致することを確認する
5. 生成 JSON の slug 総数と `frontend/out/law/*/article/*` の HTML 数が一致することを確認
(**163 という固定値をテストに書かない**)
5. `/code-gate`((B) 区分なので Gate3 必須)
6. `/code-gate`((B) 区分なので Gate3 必須)
7. push と PR は未実施。`git push -u origin feat/article-pages` するか、main へ直接入れるかは判断待ち

## 注意

- **Gate 2 は PASS ではなく上限 3 周による打ち切り。** 周回 3 の修正自体はレビューを受けていない。
Gate 3 で拾う
- **`diff.py` を変更しない。** `find_articles` の caption 取りこぼしは既知バグだが本計画の対象外。
条文ページ側は `extract_article_body()` で `ArticleTitle` と `ArticleCaption` を別々に読む
- **`format_paragraph` は `ParagraphNum` を読み飛ばす。** 差分ビューでは正しいが条文ページでは
項番号が消える。`Paragraph@Num` を `num`、`ParagraphNum` を `mark` として別に読むこと
(複数項の改正エントリは 107 件)
- **削除条文は `find_section_path(tree, "753")` が `None` を返す。**
`current["source_article_num"]`(`753:754`)で引く
- **`cross_references` の `article_num` を単独で信じない。** LLM の自由記述で、
他法令 34 件・空 10 件・表記ゆれ混在。条名(`ref`)で解決し `article_num` は veto にだけ使う
- 試して**不採用**にしたもの: e-Gov の `?elm=Article_306`(実在するが provenance が
163 個に散る)/ timeline との施行日比較(2 API の施行日軸が違い初日から 2 法令が赤)/
壁時計依存の期限切れテスト(CI 内で直せない)。**再提案しない**(理由は spec §10)
- 関連 issue: #23(出荷 diff が全法令で遅れている。直すとゲートに落ちる 30 条の大半が埋まる)
1. **`current.status` は 3 値になった** — `present` / `deleted` / `merged_deleted`。
計画の Task 9 のコードは 2 値前提で `merged_deleted` しか分岐していない。**そのまま書くと
墓標ページ(民法733・民法746・刑法178)が生きた条文のように表示される。**
- `merged_deleted` = 範囲ノードに畳まれた(民法753・754)。
文面は「{displayNum}は「{source_label}」として欠番になっています。」
- `deleted` = 単独で本文が「削除」の一語(民法733・746・刑法178)。**別の文面が要る** —
何かに畳まれたのではなく、単に廃止された条文
2. **`former`(改正直前の条文)が、改正 type が `deleted` でないページにも付く。**
墓標 5 ページ全部が `former` を持つ
3. **24 ページが `caption` 空。** 見出しで空の `()` を描画しないこと
4. **`cross_references` の `context`** は、リンク先の版が古いときは Python 側で落としてある
(空文字)。フロント側で「context があるときだけ出す」形にすること
5. **`diff.py` を変更しない。** `find_articles` の caption 取りこぼしは既知バグだが対象外
6. **`scripts/articles.py --all` は毎回 e-Gov へ 11 リクエストする。** 生成済みなので
フロント作業中に走らせ直す必要は無い。走らせるなら `LookupError` は**止まる**合図
(施行済み改正があるのに今日の本文に無い条文 → 飛ばさず人へ)
7. 試して**不採用**にしたもの(spec §10。再提案しない): e-Gov の `?elm=Article_306` /
timeline との施行日比較 / 壁時計依存の期限切れテスト
8. 関連 issue: #23(出荷 diff が全法令で遅れている)。マイナンバー法は 6 条すべて解説が
落ちているが、これはゲートが正しく働いた結果で、直すなら #23 側
24 changes: 22 additions & 2 deletions frontend/app/diff/[diffId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import type { Metadata } from "next";
import { getDiffIds, getDiffData, mainChangeCounts } from "@/lib/data";
import {
getDiffIds,
getDiffData,
mainChangeCounts,
getArticleLawIds,
getArticleData,
} from "@/lib/data";
import { RevisionHeader } from "@/components/revision-header";
import { PrSummaryCard } from "@/components/pr-summary";
import { DiffViewer } from "@/components/diff-viewer";
Expand Down Expand Up @@ -45,6 +51,16 @@ export default async function DiffPage({
}) {
const { diffId } = await params;
const data = getDiffData(decodeURIComponent(diffId));
// 労働基準法 has 0 本則 changes and ships no articles/ file; calling
// getArticleData unconditionally would throw and break its two
// 附則-only diff builds.
const articleSlugs: Record<string, string> = getArticleLawIds().includes(
data.law_id
)
? Object.fromEntries(
getArticleData(data.law_id).articles.map((a) => [a.article_num, a.slug])
)
: {};

return (
<div className="flex flex-col gap-6">
Expand All @@ -63,7 +79,11 @@ export default async function DiffPage({
/>
<RevisionHeader data={data} />
{data.pr_summary && <PrSummaryCard summary={data.pr_summary} />}
<DiffViewer diffs={data.diffs} />
<DiffViewer
diffs={data.diffs}
lawId={data.law_id}
articleSlugs={articleSlugs}
/>
</div>
);
}
110 changes: 110 additions & 0 deletions frontend/app/law/[lawId]/article/[articleSlug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import type { Metadata } from "next";
import Link from "next/link";
import { getArticleParams, getArticleData, findArticle } from "@/lib/data";
import { isTombstone } from "@/lib/types";
import { ArticleText, FormerArticleText } from "@/components/article-text";
import { ArticleHistory } from "@/components/article-history";
import { ArticleLinks } from "@/components/article-links";
import { BreadcrumbJsonLd } from "@/components/breadcrumb-jsonld";
import { jpDate } from "@/lib/format";

export function generateStaticParams() {
const params = getArticleParams();
// The build is the only feedback that reaches anyone here, so make it fail.
const seen = new Set(params.map((p) => `${p.lawId}/${p.articleSlug}`));
if (params.length === 0 || seen.size !== params.length) {
throw new Error(`article params are empty or contain duplicates (${params.length})`);
}
return params;
}

/** "民法第306条(一般の先取特権)", or without the parenthesis when the current
* text carries no caption -- 民法754条 has none, and an empty () is worse
* than no caption at all. */
function head(lawTitle: string, displayNum: string, caption: string): string {
return caption ? `${lawTitle}${displayNum}(${caption})` : `${lawTitle}${displayNum}`;
}

export async function generateMetadata({
params,
}: {
params: Promise<{ lawId: string; articleSlug: string }>;
}): Promise<Metadata> {
const { lawId, articleSlug } = await params;
const data = getArticleData(lawId);
const article = findArticle(lawId, articleSlug)!;
const title = head(data.law_title, article.display_num, article.caption);
const years = [...new Set(article.changes.map((c) => c.year))].join("・");
// Only what every page actually carries: the dated text and the amendment
// years we hold. "改正前後の条文" is true of deleted articles alone, and the
// related-article links are absent on a page whose text has moved on.
return {
title: `${title}の条文と改正(${years}年改正)`,
description: `${title}の${jpDate(data.source.asof)}時点の条文と、${years}年の改正内容。`,
alternates: { canonical: `/law/${lawId}/article/${articleSlug}` },
};
}

export default async function ArticlePageRoute({
params,
}: {
params: Promise<{ lawId: string; articleSlug: string }>;
}) {
const { lawId, articleSlug } = await params;
const data = getArticleData(lawId);
const article = findArticle(lawId, articleSlug)!;
const title = head(data.law_title, article.display_num, article.caption);

return (
<div className="flex flex-col gap-6">
<BreadcrumbJsonLd
items={[
{ name: "lexdiff", url: "https://lexdiff.com" },
{ name: data.law_title, url: `https://lexdiff.com/law/${lawId}` },
{
name: article.display_num,
url: `https://lexdiff.com/law/${lawId}/article/${articleSlug}`,
},
]}
/>
<header>
<h1 className="text-2xl font-bold">{title}</h1>
{article.section_path.length > 0 && (
<p className="mt-1 text-[13px] opacity-70">{article.section_path.join(" › ")}</p>
)}
</header>

<ArticleText
current={article.current}
asof={data.source.asof}
displayNum={article.display_num}
/>

{/* spec §6: a tombstone page lists 現在の姿 → 改正直前の条文 → 改正履歴 →
/diff リンク, with no summary slot. "現行本文に対応する解説は未収録
です" asserts a current text whose explanation is merely missing --
false for a repealed article, so the whole section is skipped here. */}
{!isTombstone(article.current.status) && (
<section>
<h2 className="text-[14px] font-bold mb-2">この条文は何をする条文か</h2>
{article.current_summary ? (
<p className="leading-[26px]">{article.current_summary.text}</p>
) : (
<p className="text-[13px] opacity-70">
現行本文に対応する解説は未収録です。収録済み改正時点の説明は改正履歴で確認できます。
</p>
)}
</section>
)}

{article.former && <FormerArticleText former={article.former} />}

<ArticleHistory changes={article.changes} anchor={article.article_num} />
<ArticleLinks lawId={lawId} related={article.related_articles} />

<Link href={`/law/${lawId}`} className="text-[var(--diff-hunk-text)]">
{data.law_title}の改正履歴をすべて見る →
</Link>
</div>
);
}
Loading
Loading