Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
20 changes: 20 additions & 0 deletions cmd/scenario/media_matrix/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -398,10 +398,18 @@ func answerForQuestion(item matrixCase, questionID string) string {
return item.OpeningEpisode
case briefdomain.QuestionIDReader:
return item.Reader
case briefdomain.QuestionIDReaderProblem:
return "媒体ごとの作法が違い、どの粒度で書けば読者に届くか迷っている。"
case briefdomain.QuestionIDExpectedReaderAction:
return item.ExpectedReaderAction
case briefdomain.QuestionIDKeyTakeaway:
return "媒体に合わせて、同じ知見でも入口と根拠の出し方を変える。"
case briefdomain.QuestionIDMustInclude:
return item.MustInclude
case briefdomain.QuestionIDConcreteExample:
return "実際の取得元、Markdown形式、検証コマンド、生成後の評価結果を例として出す。"
case briefdomain.QuestionIDEvidence:
return "シナリオCLIの出力、style score、verification PASS/NEEDS_REVIEW、生成文字数を根拠にする。"
case briefdomain.QuestionIDPersonalContext:
return item.PersonalContext
case briefdomain.QuestionIDExclusions:
Expand All @@ -410,12 +418,24 @@ func answerForQuestion(item matrixCase, questionID string) string {
return item.TargetLengthStructure
case briefdomain.QuestionIDToneStance:
return item.ToneStance
case briefdomain.QuestionIDTitleKeywords:
return "AI駆動開発、媒体別、下書き、検証、Evo X2。"
case briefdomain.QuestionIDStoryArc:
return "導入の違和感から、実践で見えた発見へ進み、読者が次に試す一歩で締める。"
case briefdomain.QuestionIDTargetStack:
return "Go 1.26、OpenAI互換API、Ollama/Evo X2、Markdown validator、ローカルシナリオCLI。"
case briefdomain.QuestionIDPrerequisiteKnowledge:
return "GoとMarkdownの基礎は知っているが、媒体別の記法差分やローカルLLM運用はこれから試す読者。"
case briefdomain.QuestionIDTechnicalProof:
return "実行コマンド、JSON出力、本文長、style score、verification結果を比較表に残す。"
case briefdomain.QuestionIDCodeExamples:
return "必要ならMakefileターゲット、curl、JSONの抜粋を短く載せる。"
case briefdomain.QuestionIDReferences:
return "Zenn/Qiita公式Markdownガイド、corsweb2024のMarkdown記事、過去の検証ログ。"
case briefdomain.QuestionIDCorBlogPurpose:
return "技術知見の報告を主にし、社員や採用候補へ開発方針も伝える。"
case briefdomain.QuestionIDCorBlogNextAction:
return "Cor.incの開発文化と検証姿勢を理解し、相談や協業につなげてもらう。"
case briefdomain.QuestionIDHomepageCTA:
return "問い合わせまたは技術相談への導線を置き、検証可能な発信基盤を短く伝える。"
case briefdomain.QuestionIDHomepageTrust:
Expand Down
3 changes: 2 additions & 1 deletion docs/adrs/0002-multi-persona-multi-format-extension.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,10 +215,11 @@ Current implementation status as of 2026-05-03:
- Phase D1 is implemented and merged: handler tests now cover template selection, edit/fork errors, SSE follow-up and draft paths, completed-session draft fallback, regenerate-section context recovery, Analyze/Generate compatibility handlers, and SQLite driver selection. `go test ./internal/handlers -cover` reports 80%+ statement coverage ([#29](https://github.com/terisuke/note_maker/issues/29)).
- Runtime runner support is implemented and merged: `cmd/scenario/live_media_matrix` reads the offline matrix, emits planned aggregate JSON/Markdown by default, and executes live Evo X2 draft runs only when `RUN_LIVE_MEDIA_MATRIX=1` or `make scenario-media-matrix-live` is used ([#57](https://github.com/terisuke/note_maker/issues/57)).
- The 2026-05-03 browser 500 analysis showed an implementation drift: plain web-app startup still defaulted to workstation-local `127.0.0.1:8081`, while this ADR requires Evo X2 Tailnet as primary. Issue [#63](https://github.com/terisuke/note_maker/issues/63) restores the default order to Evo X2 Ollama over Tailnet → Evo X2 llama.cpp → workstation-local llama.cpp and makes the UI show the actual endpoint/model reported by SSE.
- The interview question set was simplified before the next Evo X2 run ([#66](https://github.com/terisuke/note_maker/issues/66)): broad editorial questions are now split into smaller plain-Japanese prompts, medium-specific prompts cover note/Zenn/Qiita/Cor blog needs, and optional questions can be advanced as `未定`. Validation is recorded in [Issue 66 plain brief questions validation](../validation/issue-66-plain-brief-questions-2026-05-03.md).

Near-term execution order:

1. Runtime default verification ([#63](https://github.com/terisuke/note_maker/issues/63)) — confirm the browser app no longer reaches for workstation-local inference first and that favicon noise is removed.
1. Browser sanity check for the #66 interview flow — confirm the new smaller questions are easier to answer before spending Evo X2 runtime.
2. Phase C2/C3 ([#27](https://github.com/terisuke/note_maker/issues/27), [#28](https://github.com/terisuke/note_maker/issues/28)) — expose persisted sessions, guides, briefs, drafts, and verification artifacts in the web app.
3. Runtime stabilization ([#40](https://github.com/terisuke/note_maker/issues/40)) — first run one bounded media-matrix case through `cmd/scenario/live_media_matrix`, then run the full Note/Qiita/Zenn/Cor blog Evo X2 comparison once the UI can reuse the stored outputs.
4. Browser E2E ([#13](https://github.com/terisuke/note_maker/issues/13)) — cover persona/format switching, edit/fork, streaming, section regeneration, and persisted-history recovery after C2/C3 has visible browser surface.
Expand Down
4 changes: 3 additions & 1 deletion docs/implementation-plans/issue-adr-guardrails.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ The phases in [ADR 0002](../adrs/0002-multi-persona-multi-format-extension.md) (
The interview is a structured取材 session, not a generic chat.

- Fixed questions run first in deterministic order.
- Fixed questions should be small and plain enough to answer in one or two short sentences. If one question asks for multiple kinds of thinking, split it into smaller template questions.
- Optional questions must be clearly optional in the UI and may advance as `未定`; do not force the user to invent detail just to continue.
- Deep-dive questions run after fixed questions.
- A deep-dive question must store:
- `target_question_id`
Expand All @@ -101,7 +103,7 @@ The interview is a structured取材 session, not a generic chat.
- Follow-ups must ask exactly one question.
- Follow-ups must not be yes/no questions.
- Follow-ups must not be binary choice questions.
- Follow-ups must ask for concrete scene, reason, turning point, emotion, or reader lesson.
- Follow-ups must ask for one concrete scene, step, number, reason, turning point, emotion, or reader lesson.
- If LLM-generated follow-up text fails validation, use a rule-based fallback.

## Draft Guardrails
Expand Down
3 changes: 2 additions & 1 deletion docs/implementation-plans/next-implementation-cut.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Open and active:
- Fallback and packaging follow-up: [#36](https://github.com/terisuke/note_maker/issues/36), [#45](https://github.com/terisuke/note_maker/issues/45), [#15](https://github.com/terisuke/note_maker/issues/15).
- Runtime defect fixed by this cut: [#63](https://github.com/terisuke/note_maker/issues/63) makes the plain web-app default match the intended Evo X2 Tailnet primary path and records the 2026-05-03 draft-generation 500 root cause.
- Documentation and DDD audit: [#64](https://github.com/terisuke/note_maker/issues/64), with details in [Runtime and DDD alignment audit](../validation/runtime-ui-ddd-audit-2026-05-03.md).
- Interview usability fixed before measurement: [#66](https://github.com/terisuke/note_maker/issues/66), with details in [Issue 66 plain brief questions validation](../validation/issue-66-plain-brief-questions-2026-05-03.md).

## Final evaluation target

Expand Down Expand Up @@ -80,7 +81,7 @@ Lane A and Lane B can run immediately in parallel. Lane C can start by implement

## Recommended order

1. Verify the browser app with the #63 runtime defaults: `/api/models` should hit Evo X2 Tailnet first, and `/api/drafts` SSE should report the actual endpoint/model before generation starts.
1. Browser-check the #66 smaller question flow with at least note and one technical format. Do this before spending Evo X2 runtime.
2. Run one bounded Evo X2 live case through #57 and attach it to #40 to verify the runner with real latency/score data.
3. Start #27 and #28 in parallel so persisted sessions, guides, and draft artifacts become visible in the web app.
4. Start #13 once the history/artifact UI has enough stable browser surface.
Expand Down
42 changes: 42 additions & 0 deletions docs/validation/issue-66-plain-brief-questions-2026-05-03.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Issue 66 plain brief questions validation - 2026-05-03

## Goal

Before live Evo X2 measurement, reduce the user's answering burden in the brief interview. The previous fixed questions were too broad and editorial, which made the one-question-at-a-time flow feel mentally heavy.

## Implementation

- Rewrote the base fixed questions in plain Japanese.
- Split broad prompts into smaller prompts:
- reader problem,
- key takeaway,
- concrete example,
- evidence,
- title or heading keywords.
- Expanded medium-specific prompts:
- note: emotional/story arc,
- Zenn/Qiita: stack, prerequisite knowledge, reproduction proof, code examples, references,
- Cor company blog: technical report vs vision-sharing purpose and desired reader reaction.
- Softened fallback follow-up wording so it asks for one concrete scene, step, number, feeling, or reason.
- Updated the LLM follow-up prompt to request shorter, easier questions and avoid difficult editorial terms.
- The UI now wraps template question text, labels required/optional questions, and lets optional questions advance as `未定` when the answer box is empty.
- Additional question answers now keep their question label in the final draft prompt instead of being appended as unlabeled fragments.

## Verification

Commands:

```bash
go test ./internal/domain/brief ./internal/application/brief ./internal/application/draft ./internal/handlers ./cmd/scenario/media_matrix
node --check static/js/script.js
go run ./cmd/scenario/media_matrix
```

Result:

- All checks passed.
- Offline media matrix completed with `question_template_ids=14`, `composed_templates=10`, and `cases=6`.

## Remaining measurement

This change intentionally does not run the live Evo X2 media matrix. The next step is to run one bounded #40 case after confirming the new interview flow is usable in the browser, then proceed to the full Note/Qiita/Zenn/Cor blog comparison.
6 changes: 6 additions & 0 deletions internal/application/brief/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,12 +207,18 @@ func fixedAnswers() []string {
"Local article generation with a small deterministic workflow.",
"Open with a failed local LLM run that timed out while drafting.",
"Solo developers who write note.com articles with local tools.",
"They are unsure how to turn rough technical notes into a readable article.",
"They should try a three-phase workflow before drafting.",
"The smallest useful workflow is style analysis, interview, draft, and verification.",
"Mention style analysis, brief interviews, and final draft checks.",
"Use a failed timeout and a fixed follow-up question as concrete examples.",
"Compare elapsed seconds, style score, verification result, and generated length.",
"Use the author's personal history as a musician and engineer.",
"Avoid cloud-only assumptions.",
"3000字前後 with six sections.",
"Practical and introspective.",
"local LLM, article draft, verification.",
"Start with friction, move to workflow design, and close with one small next step.",
}
}

Expand Down
43 changes: 42 additions & 1 deletion internal/application/draft/prompt.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"strconv"
"strings"

briefdomain "github.com/teradakousuke/note_maker/internal/domain/brief"
outputformat "github.com/teradakousuke/note_maker/internal/domain/format"
personadomain "github.com/teradakousuke/note_maker/internal/domain/persona"
)
Expand Down Expand Up @@ -145,6 +146,7 @@ func BuildSectionRegenerationPrompt(guide WritingStyleGuide, brief ArticleBrief,
appendLine(&prompt, "著者本人の属人的な文脈", brief.PersonalContext)
appendLine(&prompt, "含めないこと", brief.Exclusions)
appendLine(&prompt, "トーンと立場", brief.ToneStance)
appendCustomAnswers(&prompt, brief.CustomAnswers)
appendDeepDives(&prompt, brief.DeepDives)
if calibration := strictMetricCalibration(profile, brief, guide); calibration != "" {
prompt.WriteString("\n## strict style calibration\n")
Expand Down Expand Up @@ -415,12 +417,51 @@ func appendCustomAnswers(builder *strings.Builder, values []BriefAnswer) {
for _, value := range values {
content := strings.TrimSpace(value.Content)
if content != "" {
lines = append(lines, content)
lines = append(lines, briefQuestionLabel(value.QuestionID)+": "+content)
}
}
appendList(builder, "追加質問メモ", lines)
}

func briefQuestionLabel(questionID string) string {
switch questionID {
case briefdomain.QuestionIDReaderProblem:
return "読者の困りごと"
case briefdomain.QuestionIDKeyTakeaway:
return "持ち帰ってほしいこと"
case briefdomain.QuestionIDConcreteExample:
return "具体例・失敗例"
case briefdomain.QuestionIDEvidence:
return "根拠・数字・比較"
case briefdomain.QuestionIDTitleKeywords:
return "タイトル候補・見出し語"
case briefdomain.QuestionIDStoryArc:
return "note向けの感情の流れ"
case briefdomain.QuestionIDTargetStack:
return "技術・ツール・バージョン"
case briefdomain.QuestionIDPrerequisiteKnowledge:
return "読者の前提知識"
case briefdomain.QuestionIDTechnicalProof:
return "再現手順・検証結果"
case briefdomain.QuestionIDCodeExamples:
return "コード例・コマンド"
case briefdomain.QuestionIDReferences:
return "参考リンク"
case briefdomain.QuestionIDCorBlogPurpose:
return "自社ブログでの目的"
case briefdomain.QuestionIDCorBlogNextAction:
return "会社ブログとしての読後感"
case briefdomain.QuestionIDHomepageCTA:
return "CTA"
case briefdomain.QuestionIDHomepageTrust:
return "信頼の根拠"
case briefdomain.QuestionIDCloudiaViewpoint:
return "クラウディア視点"
default:
return questionID
}
}

func truncateRunes(value string, max int) string {
runes := []rune(value)
if len(runes) <= max {
Expand Down
7 changes: 7 additions & 0 deletions internal/application/draft/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

articledomain "github.com/teradakousuke/note_maker/internal/domain/article"
authordomain "github.com/teradakousuke/note_maker/internal/domain/author"
briefdomain "github.com/teradakousuke/note_maker/internal/domain/brief"
outputformat "github.com/teradakousuke/note_maker/internal/domain/format"
personadomain "github.com/teradakousuke/note_maker/internal/domain/persona"
)
Expand All @@ -27,6 +28,10 @@ func TestGenerateBuildsPromptFromGuideAndBriefOnly(t *testing.T) {
PersonalContext: "音楽家からエンジニアになった経験を入れる。",
Exclusions: "Note記事本文の再取得",
TargetLengthStructure: "1200字、導入・本論・結論",
CustomAnswers: []BriefAnswer{
{QuestionID: briefdomain.QuestionIDReaderProblem, Content: "媒体ごとの書き分けが難しい"},
{QuestionID: briefdomain.QuestionIDTitleKeywords, Content: "下書き、Evo X2、検証"},
},
},
AuthorProfile: profile,
}
Expand All @@ -51,6 +56,8 @@ func TestGenerateBuildsPromptFromGuideAndBriefOnly(t *testing.T) {
"参考記事本文は与えられていません",
"strict style calibration",
"一人称密度",
"読者の困りごと: 媒体ごとの書き分けが難しい",
"タイトル候補・見出し語: 下書き、Evo X2、検証",
} {
if !strings.Contains(generator.prompt, want) {
t.Fatalf("prompt does not contain %q:\n%s", want, generator.prompt)
Expand Down
25 changes: 14 additions & 11 deletions internal/domain/brief/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,9 @@ func (s ArticleBriefSession) CustomAnswers() []BriefAnswer {
}
answers := make([]BriefAnswer, 0)
for _, answer := range s.Answers {
if strings.TrimSpace(answer.Content) == "" {
continue
}
if answer.FlowType == QuestionFlowMain && !fixed[answer.QuestionID] {
answers = append(answers, answer)
}
Expand Down Expand Up @@ -241,36 +244,36 @@ func FallbackFollowUpText(target ArticleQuestion, answer BriefAnswer, followUpIn
switch target.ID {
case QuestionIDOpeningEpisode:
if followUpIndex == 1 {
question = "読者に最初に見せたい具体的な場面を、どの描写から始めますか?"
question = "その場面で、読者に最初に見せたいものを1つだけ挙げると何ですか?"
break
}
question = "その時点の感情を、どんな言葉で記事に残しますか?"
question = "その時の気持ちを短く書くなら、どんな言葉になりますか?"
case QuestionIDMustInclude:
if followUpIndex == 1 {
question = "必ず含めたい論点のうち、どの部分に具体的な根拠を足しますか?"
question = "必ず入れたいことの中で、特に詳しく説明したいものはどれですか?"
break
}
question = "その論点から読者に持ち帰ってほしい学びを、どう表現しますか?"
question = "その話を信じてもらうために、足せそうな根拠は何ですか?"
case QuestionIDPersonalContext:
if followUpIndex == 1 {
question = "記事の主張に最も直接つなげたい個人的な経験は何ですか?"
question = "あなた自身の経験として、記事に入れると伝わりやすい出来事は何ですか?"
break
}
question = "記事の中で見せたい個人的な価値観や迷いは何ですか?"
question = "その経験から、今の考え方が変わった点はありますか?"
case QuestionIDExpectedReaderAction:
if followUpIndex == 1 {
question = "読者がその行動を取りたくなる理由を、どの実感から説明しますか?"
question = "読者が最初に試せる小さな一歩は何ですか?"
break
}
question = "読後に読者が想像できる最初の一歩は何ですか?"
question = "その一歩を試すと、読者にどんな良いことがありますか?"
case QuestionIDToneStance:
if followUpIndex == 1 {
question = "記事で最も丁寧に説明したい立場は何ですか?"
question = "この文章で一番大事にしたい温度感は何ですか?"
break
}
question = "そのトーンや立場を支える経験は何ですか?"
question = "その温度感にしたい理由は何ですか?"
default:
question = "記事を実用的にするために、どんな具体的な情報を足しますか?"
question = "記事に足すと読みやすくなる具体的な情報を1つ挙げるなら何ですか?"
}
return contextualFollowUpQuestion(answer.Content, question)
}
Expand Down
Loading