diff --git a/docs/getting-started/tutorial-daily-standup-sprint-review.md b/docs/getting-started/tutorial-daily-standup-sprint-review.md index a1454e3d..aefc711d 100644 --- a/docs/getting-started/tutorial-daily-standup-sprint-review.md +++ b/docs/getting-started/tutorial-daily-standup-sprint-review.md @@ -27,7 +27,9 @@ Preferred command path is `specfact backlog ceremony standup ...`. The legacy `s the adapter supports fetching comments - Use **`--summarize`** or **`--summarize-to `** to output a **prompt** (instruction + filter context + standup data) for a slash command (e.g. `specfact.daily`) or copy-paste to Copilot to **generate a - standup summary**; add **`--comments`**/**`--annotations`** to include comment annotations in the prompt + standup summary**; add **`--comments`**/**`--annotations`** to include comment annotations in the prompt. + The prompt content is always **normalized to Markdown-only text** (no raw HTML tags or HTML entities) so + ADO-style HTML descriptions/comments and GitHub/Markdown content render consistently. - Use the **`specfact.backlog-daily`** (or `specfact.daily`) slash prompt for interactive walkthrough with the DevOps team story-by-story (focus, issues, open questions, discussion notes as comments) - Filter by **`--assignee`**, **`--sprint`** / **`--iteration`**, **`--search`**, **`--release`**, **`--id`**, **`--first-issues`** / **`--last-issues`**, **`--blockers-first`**, and optional **`--suggest-next`** @@ -142,18 +144,21 @@ the standup table (state, assignee, limit, etc.). To get a **prompt** you can paste into Copilot or feed to a slash command (e.g. `specfact.daily`) so an AI can **generate a short standup summary** (e.g. "Today: 3 in progress, 1 blocked, 2 pending commitment"): ```bash -# Print prompt to stdout (copy-paste to Copilot) +# Print prompt to stdout (copy-paste to Copilot). In an interactive terminal, SpecFact renders a +# Markdown-formatted view; in CI/non-interactive environments the same normalized Markdown is printed +# without ANSI formatting. specfact backlog ceremony standup github --summarize --comments -# Write prompt to a file (e.g. for slash command) +# Write prompt to a file (e.g. for slash command). The file always contains plain Markdown-only content +# (no raw HTML, no ANSI control codes), suitable for IDE slash commands or copy/paste into Copilot. specfact backlog ceremony standup github --summarize-to ./standup-prompt.md --comments ``` The output includes an instruction to generate a standup summary, the applied filter context (adapter, state, sprint, assignee, limit), and the same per-item data as `--copilot-export`. With -`--comments`/`--annotations`, the prompt includes comment annotations when supported. Use it with the -**`specfact.backlog-daily`** slash prompt for interactive team walkthrough (story-by-story, current focus, -issues/open questions, discussion notes as comments). +`--comments`/`--annotations`, the prompt includes normalized descriptions and comment annotations when +supported. Use it with the **`specfact.backlog-daily`** slash prompt for interactive team walkthrough +(story-by-story, current focus, issues/open questions, discussion notes as comments). --- diff --git a/docs/guides/agile-scrum-workflows.md b/docs/guides/agile-scrum-workflows.md index a01f2b62..cb720945 100644 --- a/docs/guides/agile-scrum-workflows.md +++ b/docs/guides/agile-scrum-workflows.md @@ -91,8 +91,10 @@ SpecFact CLI supports real-world agile/scrum practices through: to show suggested next item by value score (business_value / (story_points × priority)). **Copilot export**: Use `--copilot-export ` to write a summarized Markdown file of each story for Copilot. Add `--comments` (alias `--annotations`) to include descriptions and comment annotations in - `--copilot-export` and `--summarize` outputs when the adapter supports `get_comments` (GitHub, ADO). Use - `--first-comments N` or `--last-comments N` to scope comment volume when needed (default: include all). + `--copilot-export` and `--summarize` outputs when the adapter supports `get_comments` (GitHub, ADO). All + summarize/copilot-export content is **normalized to Markdown-only text** (no raw HTML tags or entities) + so ADO-style HTML fields and Markdown-native fields render consistently. Use `--first-comments N` or + `--last-comments N` to scope comment volume when needed (default: include all). Use `--first-issues N` or `--last-issues N` (mutually exclusive) to scope daily output to oldest/newest items by numeric issue/work-item ID. **Kanban**: omit iteration/sprint and use state + limit; unassigned = pullable work. **Scrum/SAFe**: use @@ -158,8 +160,8 @@ specfact backlog ceremony standup github --interactive # step-through; detail # or specfact backlog ceremony standup github --copilot-export ./standup.md --comments --last-comments 5 # or -specfact backlog ceremony standup github --summarize --comments # prompt to stdout for AI to generate standup summary -specfact backlog ceremony standup github --summarize-to ./standup-prompt.md +specfact backlog ceremony standup github --summarize --comments # prompt to stdout for AI to generate standup summary (Markdown-only) +specfact backlog ceremony standup github --summarize-to ./standup-prompt.md # plain Markdown file (no HTML/ANSI) ``` Use the **`specfact.backlog-daily`** (or `specfact.daily`) slash prompt for interactive walkthrough with the diff --git a/openspec/changes/backlog-scrum-05-summarize-markdown-output/.openspec.yaml b/openspec/changes/backlog-scrum-05-summarize-markdown-output/.openspec.yaml new file mode 100644 index 00000000..d1c6cc6f --- /dev/null +++ b/openspec/changes/backlog-scrum-05-summarize-markdown-output/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-02-27 diff --git a/openspec/changes/backlog-scrum-05-summarize-markdown-output/CHANGE_VALIDATION.md b/openspec/changes/backlog-scrum-05-summarize-markdown-output/CHANGE_VALIDATION.md new file mode 100644 index 00000000..aa7a94c6 --- /dev/null +++ b/openspec/changes/backlog-scrum-05-summarize-markdown-output/CHANGE_VALIDATION.md @@ -0,0 +1,77 @@ +# Change Validation Report: backlog-scrum-05-summarize-markdown-output + +**Validation Date**: 2026-02-27T13:01:44+01:00 +**Change Proposal**: [proposal.md](./proposal.md) +**Validation Method**: Dry-run dependency analysis and OpenSpec strict validation (post-implementation) + +## Executive Summary + +- **Breaking Changes**: 0 detected +- **Dependent Files**: 2 affected (implementation and tests; both updated in same change) +- **Impact Level**: Low +- **Validation Result**: Pass +- **User Decision**: Proceed (implementation completed) + +## Breaking Changes Detected + +None. All changes are additive or internal: + +- **`_normalize_markdown_text(text: str) -> str`**: New private helper in `commands.py`; no public API change. +- **`_is_interactive_tty() -> bool`**: New private helper; no public API change. +- **`_build_summarize_prompt_content(...)`**: Signature unchanged; behavior change is internal (normalization of body/comment strings before inclusion). All call sites (same module and unit tests) remain compatible. + +## Dependencies Affected + +### Critical Updates Required + +None. + +### Recommended Updates + +- **`src/specfact_cli/modules/backlog/src/commands.py`**: Already updated (normalization, TTY detection, Rich Markdown rendering). +- **`tests/unit/commands/test_backlog_daily.py`**: Already updated (HTML normalization tests, existing summarize tests still pass). + +### Optional + +- **`docs/getting-started/tutorial-daily-standup-sprint-review.md`**: Updated to describe Markdown-only and interactive vs CI behavior. +- **`docs/guides/agile-scrum-workflows.md`**: Updated to note normalized Markdown-only summarize/copilot-export content. + +## Impact Assessment + +- **Code Impact**: Single module (`modules/backlog/src/commands.py`); new helpers and wiring inside existing summarize path. +- **Test Impact**: New unit tests for HTML normalization; existing summarize tests unchanged in contract. +- **Documentation Impact**: Tutorial and agile guide updated. +- **Release Impact**: Patch (backward-compatible behavior change; output format improved, not contracted). + +## User Decision + +**Decision**: Proceed +**Rationale**: Implementation completed; no breaking changes; OpenSpec validation passed. +**Next Steps**: Merge via PR from feature worktree to `dev`; optionally run `/opsx:archive` after merge. + +## Format Validation + +- **proposal.md Format**: Pass + - Required sections present: Why, What Changes, Capabilities, Impact + - Capabilities section lists new capability and modified daily-standup +- **tasks.md Format**: Pass + - Numbered sections and checkbox task format per config + - All tasks completed except 4.3 (now completed by this validation) +- **specs Format**: Pass + - ADDED/MODIFIED requirements with scenarios (When/Then) +- **design.md Format**: Pass + - Context, Goals/Non-Goals, Decisions, Risks documented +- **Config.yaml Compliance**: Pass + +## OpenSpec Validation + +- **Status**: Pass +- **Validation Command**: `openspec validate backlog-scrum-05-summarize-markdown-output --strict` +- **Issues Found**: 0 +- **Issues Fixed**: 0 +- **Re-validated**: N/A + +## Validation Artifacts + +- Dependency search: `_normalize_markdown_text`, `_is_interactive_tty`, `_build_summarize_prompt_content` usages confined to `commands.py` and `test_backlog_daily.py`. +- No temporary workspace created; validation performed in-repo post-implementation. diff --git a/openspec/changes/backlog-scrum-05-summarize-markdown-output/TDD_EVIDENCE.md b/openspec/changes/backlog-scrum-05-summarize-markdown-output/TDD_EVIDENCE.md new file mode 100644 index 00000000..60611661 --- /dev/null +++ b/openspec/changes/backlog-scrum-05-summarize-markdown-output/TDD_EVIDENCE.md @@ -0,0 +1,39 @@ +## TDD Evidence for backlog-scrum-05-summarize-markdown-output + +### Failing-before run (new summarize normalization tests) + +- **Command:** + + ```bash + hatch test --cover -v tests/unit/commands/test_backlog_daily.py -k "summarize_prompt_normalizes_html" + ``` + +- **Timestamp:** 2026-02-27 (see CI logs / local shell history for exact time) + +- **Failure summary:** + - `test_summarize_prompt_normalizes_html_description_to_markdown`: + - Expected HTML `

` / `
` and `&` entities to be removed from summarize prompt output. + - Actual output still contained raw `

Line 1
Line 2 & more

` in the Description section. + - `test_summarize_prompt_normalizes_html_comments_to_markdown`: + - Expected HTML `
` and `
` plus `&` entities in comments to be removed. + - Actual output still contained raw `
Comment & note
next line
` in the Comments section. + +These failures confirm current behavior violates the new spec delta: summarize prompts include raw HTML and entities from ADO-style bodies and comments instead of normalized Markdown-only content. + +### Passing-after run (summarize normalization implemented) + +- **Command:** + + ```bash + hatch test --cover -v tests/unit/commands/test_backlog_daily.py -k "summarize_prompt_normalizes_html" + ``` + +- **Result:** ✅ 2 passed (normalization tests), remaining tests deselected in this targeted run. + +- **Behavior summary:** + - `_build_summarize_prompt_content` now: + - Normalizes HTML-based `body_markdown` values to Markdown-friendly text (no `

`, `
` tags or `&` entities). + - Normalizes HTML comments before including them under the "Comments (annotations)" section. + - New helper `_normalize_markdown_text` (with `@beartype` and `@ensure`) enforces that the returned text does not contain raw HTML tags, satisfying the updated `daily-standup` summarize requirements. + + diff --git a/openspec/changes/backlog-scrum-05-summarize-markdown-output/design.md b/openspec/changes/backlog-scrum-05-summarize-markdown-output/design.md new file mode 100644 index 00000000..12612990 --- /dev/null +++ b/openspec/changes/backlog-scrum-05-summarize-markdown-output/design.md @@ -0,0 +1,40 @@ +## Context + +`specfact backlog daily` already supports a `--summarize` / `--summarize-to` flow that builds a prompt-ready view of the current standup scope (filters, per-item data, body, comments). When used against ADO, the underlying work item body and comments are often stored as HTML, while GitHub and some ADO comments use Markdown. Today the summarize builder can emit raw HTML fragments and entities into the prompt, which is noisy for both humans and LLMs and inconsistent with Markdown-centric flows elsewhere in SpecFact. + +At the same time, SpecFact needs to support both interactive, rich terminal sessions (for humans running standup from a shell) and non-interactive / CI environments where summarize output is consumed by other tools or stored as artifacts. + +## Goals / Non-Goals + +**Goals:** + +- Normalize all descriptions and comments included in summarize output into clean Markdown text, regardless of provider format. +- Ensure summarize prompts never contain raw HTML tags or HTML entities. +- Provide a Markdown-aware, readable view of the summarize content in interactive terminals (e.g. Rich Markdown rendering), while keeping the underlying Markdown text stable and prompt-ready. +- Preserve existing summarize semantics: same filters, same per-item data fields, same `--summarize` vs `--summarize-to` behavior. + +**Non-Goals:** + +- Do not change which items are included in standup or summarize scope (filters and selection logic remain as defined in `daily-standup`). +- Do not change how comments or bodies are stored in providers; normalization is applied only at summarize/export time. +- Do not introduce a hard dependency on any particular HTML-to-Markdown library that would block offline usage; implementation must remain Python-only and bundle-safe. + +## Decisions + +- Introduce a small normalization utility (e.g. in the backlog module package) that: + - Accepts raw body/comment text and a hint about source format (HTML vs Markdown when known). + - Converts HTML to Markdown using a deterministic, testable strategy. + - Always returns Markdown-only text suitable for inclusion in prompts. +- Extend the summarize builder for `backlog daily` so that: + - Before assembling the per-item section, it passes body and comment text through the normalization utility. + - It treats GitHub/Markdown-native content as Markdown but still routes through the same normalization path for consistency. +- Add a simple environment/TTY detection layer around summarize output: + - If running in an interactive TTY and not explicitly in CI mode, render the normalized Markdown using Rich (or an equivalent Markdown-capable view) for the user. + - If output is redirected, piped, or CI mode is detected, emit plain Markdown text without terminal control codes. + +## Risks / Trade-offs + +- HTML-to-Markdown conversion can be lossy if not carefully tuned; we must verify typical ADO HTML patterns (paragraphs, lists, bold, links) produce acceptable Markdown for standup prompts. +- Rich or similar libraries must be used in a way that does not leak ANSI control codes into `--summarize-to` files or CI logs; separation between rendered view and underlying text needs to be clear in implementation. +- Normalization adds a processing step per item/comment; for very large backlogs this can affect performance, so implementation should be efficient and optionally short-circuit when input is already clean Markdown. + diff --git a/openspec/changes/backlog-scrum-05-summarize-markdown-output/proposal.md b/openspec/changes/backlog-scrum-05-summarize-markdown-output/proposal.md new file mode 100644 index 00000000..bfc2e9f4 --- /dev/null +++ b/openspec/changes/backlog-scrum-05-summarize-markdown-output/proposal.md @@ -0,0 +1,43 @@ +# Change: Normalize daily summarize Markdown output + +## Why + + + +The current `specfact backlog daily --summarize/--summarize-to` output often contains raw HTML fragments and entities from ADO work item comments, mixed with Markdown-formatted text from GitHub and ADO. This makes the standup summary prompt hard to read for humans and noisy for LLMs, even though the underlying data is correct. + +## What Changes + + + +- Normalize backlog comments and descriptions used by `specfact backlog daily --summarize/--summarize-to` so that: + - HTML-formatted content is converted into clean Markdown before it is included in the prompt. + - Existing Markdown content is preserved as Markdown (no lossy reformatting). +- For interactive terminal sessions: + - Render the summarized standup prompt using a Markdown-aware terminal view (e.g. Rich Markdown rendering) so users see a readable, formatted view instead of raw Markdown or HTML. +- For non-interactive / CI environments and plain terminals: + - Fall back to emitting structured Markdown text directly (never raw HTML), preserving prompt-ready formatting for copy/paste into Copilot or slash commands. +- Ensure the summarize output logic can distinguish between: + - Interactive rich terminal usage (formatted view, still based on the same Markdown text). + - Non-interactive/CI usage (plain Markdown text, no color/control codes). + +## Capabilities +### New Capabilities +- `backlog-daily-markdown-normalization`: Normalize backlog item bodies and comments into Markdown-only text for daily standup summarize prompts, with environment-aware rendering (rich Markdown view in interactive terminals, plain Markdown in CI/non-interactive mode). + +### Modified Capabilities +- `daily-standup`: Clarify that the `--summarize/--summarize-to` scenarios must: + - Include only Markdown (no raw HTML fragments or entities) in per-item body/comment fields. + - Prefer a Markdown-formatted view in interactive terminals while keeping the underlying output prompt-ready for LLMs. + + +--- + +## Source Tracking + + +- **GitHub Issue**: #324 +- **Issue URL**: +- **Last Synced Status**: proposed +- **Sanitized**: false + \ No newline at end of file diff --git a/openspec/changes/backlog-scrum-05-summarize-markdown-output/specs/backlog-daily-markdown-normalization/spec.md b/openspec/changes/backlog-scrum-05-summarize-markdown-output/specs/backlog-daily-markdown-normalization/spec.md new file mode 100644 index 00000000..f634e8ce --- /dev/null +++ b/openspec/changes/backlog-scrum-05-summarize-markdown-output/specs/backlog-daily-markdown-normalization/spec.md @@ -0,0 +1,41 @@ +## ADDED Requirements + +### Requirement: Normalize HTML and Markdown for summarize output + +The system SHALL normalize all backlog item descriptions and comments included in `specfact backlog daily --summarize` and `--summarize-to` output so that the resulting prompt contains **only Markdown-formatted text** (no raw HTML tags or HTML entities), regardless of whether the underlying provider stores content as HTML (e.g. ADO) or Markdown (e.g. GitHub, Markdown-style ADO comments). + +#### Scenario: HTML comments from ADO are converted to Markdown +- **WHEN** `specfact backlog daily --summarize` or `--summarize-to` includes work items whose description or comments are stored as HTML (e.g. ADO discussion/comments) +- **THEN** the system converts that HTML content into readable Markdown before including it in the summarize prompt +- **AND** the resulting output does not contain raw HTML tags or un-decoded HTML entities (e.g. `<div>`, `

`, `
`) + +#### Scenario: Existing Markdown comments are preserved as Markdown +- **WHEN** `specfact backlog daily --summarize` or `--summarize-to` includes items whose description or comments are already stored as Markdown (e.g. GitHub issues, Markdown-formatted ADO comments) +- **THEN** the system preserves the original Markdown semantics when building the summarize prompt (headings, lists, code fences, emphasis) +- **AND** the system does not degrade Markdown into a less structured format (e.g. by stripping list markers or collapsing headings) + +#### Scenario: Mixed HTML and Markdown sources produce a consistent Markdown prompt +- **WHEN** the daily summarize command aggregates items from sources that use different underlying formats (HTML and Markdown) +- **THEN** the combined summarize output is a single, consistent Markdown document suitable for LLM consumption +- **AND** no raw HTML tags or entities appear anywhere in the per-item body or comments sections + +### Requirement: Environment-aware rendering for summarize output + +The system SHALL render the same normalized Markdown summarize content differently depending on whether it is running in an interactive terminal session or in a non-interactive / CI environment, while always preserving a prompt-ready Markdown representation that tools can consume. + +#### Scenario: Interactive terminal shows rich Markdown view +- **WHEN** a user runs `specfact backlog daily --summarize` in an interactive terminal that supports rich output (e.g. TTY, not redirected to a file) +- **THEN** the CLI MAY render the summarize content using a Markdown-aware terminal view (for example, Rich Markdown rendering) +- **AND** the user sees a readable, formatted standup summary prompt (headings, lists, emphasis) instead of raw Markdown or HTML +- **AND** the underlying content remains logically the same as the Markdown text used for `--summarize-to` (same sections and text, just rendered differently) + +#### Scenario: Non-interactive or CI environments emit plain Markdown +- **WHEN** `specfact backlog daily --summarize` or `--summarize-to` is run in a non-interactive environment (e.g. CI/CD job, output redirected to a file or piped) +- **THEN** the system emits plain, prompt-ready Markdown text without ANSI color codes or interactive formatting controls +- **AND** the output still satisfies the existing summarize requirement to include instruction text, filter context, and per-item data (including normalized body and comments) + +#### Scenario: Summarize-to file output is always Markdown-only +- **WHEN** the user runs `specfact backlog daily --summarize-to ` +- **THEN** the file at `` contains only normalized Markdown content (no raw HTML tags or entities, no terminal control codes) +- **AND** the file is suitable for direct copy/paste into IDE slash commands or Copilot prompts without additional cleanup + diff --git a/openspec/changes/backlog-scrum-05-summarize-markdown-output/specs/daily-standup/spec.md b/openspec/changes/backlog-scrum-05-summarize-markdown-output/specs/daily-standup/spec.md new file mode 100644 index 00000000..b9af1ceb --- /dev/null +++ b/openspec/changes/backlog-scrum-05-summarize-markdown-output/specs/daily-standup/spec.md @@ -0,0 +1,13 @@ +## MODIFIED Requirements + +### Requirement: Standup summary prompt (--summarize) + +The system SHALL support a `--summarize` flag on `specfact backlog daily` that produces a **prompt** (instructions plus applied filters and filtered standup output) suitable for use in an interactive slash command (e.g. `specfact.daily`) or copy-paste to Copilot, so an LLM can generate a meaningful **summary of the daily standup status**. The prompt content for item bodies and comments SHALL be provided as normalized Markdown text only (no raw HTML tags or entities), regardless of how the underlying provider stores or formats those fields. + +#### Scenario: --summarize outputs prompt with filters and data (Markdown-only content) +- **Given**: Backlog items in the current scope (same as standup: state, iteration/sprint, assignee, limit) and the user runs `specfact backlog daily --summarize` (stdout) or `--summarize-to ` (write to file) +- **When**: The command runs with the same filters as the standup view +- **Then**: The system outputs (to stdout or to the given path) a prompt that includes: (1) brief instruction that the following data is the current standup view and the LLM should generate a concise standup summary; (2) the applied filter context (adapter, state, sprint, assignee, limit); (3) per-item data including **body (description)** and **comments (annotations)** when available, plus ID, title, status, assignees, last updated, progress, blockers, optional value score, so the LLM can produce a **meaningful** summary +- **And**: The per-item body and comment fields in the prompt are formatted as Markdown without raw HTML tags or HTML entities (e.g. no `

`, `
`, `<div>`) +- **And**: The output is formatted so it can be pasted into Copilot or used as input to a slash command (e.g. `specfact.daily`) to produce a standup summary + diff --git a/openspec/changes/backlog-scrum-05-summarize-markdown-output/tasks.md b/openspec/changes/backlog-scrum-05-summarize-markdown-output/tasks.md new file mode 100644 index 00000000..7be45a93 --- /dev/null +++ b/openspec/changes/backlog-scrum-05-summarize-markdown-output/tasks.md @@ -0,0 +1,26 @@ +## 1. Normalize summarize output to Markdown-only + +- [x] 1.1 Identify backlog daily summarize/export code path and call sites for `--summarize` / `--summarize-to` +- [x] 1.2 Introduce a normalization utility that converts HTML-based bodies/comments to Markdown while preserving existing Markdown semantics +- [x] 1.3 Wire the normalization utility into the summarize builder so all per-item body/comment fields are normalized before inclusion +- [x] 1.4 Add icontract/beartype contracts around the normalization entry point to enforce non-HTML output + +## 2. Environment-aware rendering and behavior + +- [x] 2.1 Add TTY / CI detection around `backlog daily --summarize` output (interactive vs non-interactive decision) +- [x] 2.2 Implement rich Markdown rendering for interactive terminals while keeping the underlying Markdown text stable +- [x] 2.3 Ensure `--summarize-to ` always writes plain Markdown with no ANSI control codes or HTML + +## 3. Adapter integration and tests + +- [x] 3.1 Adjust ADO adapter/comment plumbing so HTML bodies/comments are routed through the normalization utility for summarize flows +- [x] 3.2 Verify GitHub/Markdown-native flows still behave correctly when passing through normalization +- [x] 3.3 Add tests that prove summarize output contains no raw HTML or HTML entities for mixed ADO/GitHub scenarios +- [x] 3.4 Add tests that prove interactive vs CI/non-interactive summarize behavior matches spec (rendered view vs plain Markdown) + +## 4. Documentation and OpenSpec validation + +- [x] 4.1 Update any relevant docs/guides that mention `specfact backlog daily --summarize` / `--summarize-to` to note Markdown-only, normalized behavior +- [x] 4.2 Run `openspec validate backlog-scrum-05-summarize-markdown-output --strict` and fix any validation issues +- [x] 4.3 Run `/wf-validate-change backlog-scrum-05-summarize-markdown-output` from the specfact-openspec workflow and record results in CHANGE_VALIDATION.md + diff --git a/src/specfact_cli/modules/backlog/module-package.yaml b/src/specfact_cli/modules/backlog/module-package.yaml index 932db5cb..4334ee18 100644 --- a/src/specfact_cli/modules/backlog/module-package.yaml +++ b/src/specfact_cli/modules/backlog/module-package.yaml @@ -1,5 +1,5 @@ name: backlog -version: 0.1.5 +version: 0.1.6 commands: - backlog command_help: @@ -28,5 +28,5 @@ publisher: description: Manage backlog ceremonies, refinement, and dependency insights. license: Apache-2.0 integrity: - checksum: sha256:f70d5fe4f5800a1ceec246c699c8747aada4cc9cf96d7063519e8d66ed9d9e61 - signature: bUkOlza/4kdLocRbDOyCnjdi2CQQkDkm5pSBuzg5LzJ3RllJ2NuXVCSlTjtdbdMYxQOf2JdLpl27mpcvHUnAAQ== + checksum: sha256:a3b033ef35a6a95e1c40ffe28e112cb1683af5051dd813038bacf1cd76bfd7ad + signature: gHQkRqNpRRpxwRmFiHSHaSpq8/rwKvv1v/4Wjt8pRl0Z2VFTVF1DStb2XwgZlE0Bpg77n++G5mIl7KkM7MyGBQ== diff --git a/src/specfact_cli/modules/backlog/src/commands.py b/src/specfact_cli/modules/backlog/src/commands.py index f2dcb980..edd15c4a 100644 --- a/src/specfact_cli/modules/backlog/src/commands.py +++ b/src/specfact_cli/modules/backlog/src/commands.py @@ -31,6 +31,7 @@ from beartype import beartype from icontract import ensure, require from rich.console import Console +from rich.markdown import Markdown from rich.panel import Panel from rich.progress import Progress, SpinnerColumn, TextColumn, TimeElapsedColumn from rich.prompt import Confirm @@ -78,6 +79,18 @@ def list_commands(self, ctx: click.Context) -> list[str]: return sorted(commands, key=lambda name: (self._ORDER_PRIORITY.get(name, 1000), name)) +def _is_interactive_tty() -> bool: + """ + Return True when running in an interactive TTY suitable for rich Markdown rendering. + + CI and non-TTY environments should fall back to plain Markdown text to keep output machine-friendly. + """ + try: + return sys.stdout.isatty() + except Exception: # pragma: no cover - extremely defensive + return False + + class _CeremonyCommandGroup(TyperGroup): """Stable ordering for backlog ceremony subcommands.""" @@ -1475,7 +1488,7 @@ def _build_summarize_prompt_content( ) lines.append(f"- **Last updated:** {updated}") if include_comments: - body = (item.body_markdown or "").strip() + body = _normalize_markdown_text((item.body_markdown or "").strip()) if body: snippet = body[:_SUMMARIZE_BODY_TRUNCATE] if len(body) > _SUMMARIZE_BODY_TRUNCATE: @@ -1492,7 +1505,8 @@ def _build_summarize_prompt_content( if item_comments: lines.append("- **Comments (annotations):**") for c in item_comments: - lines.append(f" - {c}") + normalized_comment = _normalize_markdown_text(c) + lines.append(f" - {normalized_comment}") if item.story_points is not None: lines.append(f"- **Story points:** {item.story_points}") if item.priority is not None: @@ -1506,6 +1520,57 @@ def _build_summarize_prompt_content( return "\n".join(lines).strip() +_HTML_TAG_RE = re.compile(r"<[A-Za-z/][^>]*>") + + +@beartype +@ensure(lambda result: not _HTML_TAG_RE.search(result or ""), "Normalized text must not contain raw HTML tags") +def _normalize_markdown_text(text: str) -> str: + """ + Normalize provider-specific markup (HTML, entities) to Markdown-friendly text. + + This is intentionally conservative: plain Markdown is left as-is, while common HTML constructs from + ADO-style bodies and comments are converted to readable Markdown and stripped of tags/entities. + """ + if not text: + return "" + + # Fast path: if no obvious HTML markers, return as-is. + if "<" not in text and "&" not in text: + return text + + from html import unescape + + # Unescape HTML entities first so we can treat content uniformly. + value = unescape(text) + + # Replace common block/linebreak tags with newlines before stripping other tags. + # Handle several variants to cover typical ADO HTML. + value = re.sub(r"<\s*br\s*/?\s*>", "\n", value, flags=re.IGNORECASE) + value = re.sub(r"", "\n\n", value, flags=re.IGNORECASE) + value = re.sub(r"<\s*p[^>]*>", "", value, flags=re.IGNORECASE) + + # Turn list items into markdown bullets. + value = re.sub(r"<\s*li[^>]*>", "- ", value, flags=re.IGNORECASE) + value = re.sub(r"", "\n", value, flags=re.IGNORECASE) + value = re.sub(r"<\s*ul[^>]*>", "", value, flags=re.IGNORECASE) + value = re.sub(r"", "\n", value, flags=re.IGNORECASE) + value = re.sub(r"<\s*ol[^>]*>", "", value, flags=re.IGNORECASE) + value = re.sub(r"", "\n", value, flags=re.IGNORECASE) + + # Drop any remaining tags conservatively. + value = _HTML_TAG_RE.sub("", value) + + # Normalize whitespace: collapse excessive blank lines but keep paragraph structure. + # First, normalize Windows-style newlines. + value = value.replace("\r\n", "\n").replace("\r", "\n") + # Collapse 3+ blank lines into 2. + value = re.sub(r"\n{3,}", "\n\n", value) + # Strip leading/trailing whitespace on each line. + lines = [line.rstrip() for line in value.split("\n")] + return "\n".join(lines).strip() + + @beartype def _build_refine_export_content( adapter: str, @@ -2996,7 +3061,10 @@ def daily( Path(summarize_to).write_text(content, encoding="utf-8") console.print(f"[dim]Summarize prompt written to {summarize_to} ({len(filtered)} item(s))[/dim]") else: - console.print(content) + if _is_interactive_tty() and not os.environ.get("CI"): + console.print(Markdown(content)) + else: + console.print(content) return if interactive: diff --git a/tests/unit/commands/test_backlog_daily.py b/tests/unit/commands/test_backlog_daily.py index 7a83db39..c38145d4 100644 --- a/tests/unit/commands/test_backlog_daily.py +++ b/tests/unit/commands/test_backlog_daily.py @@ -828,6 +828,59 @@ def test_summarize_prompt_has_start_end_markers(self) -> None: assert content.strip().startswith("--- BEGIN STANDUP PROMPT ---") assert content.strip().endswith("--- END STANDUP PROMPT ---") + def test_summarize_prompt_normalizes_html_description_to_markdown(self) -> None: + """HTML descriptions (e.g. from ADO) are converted to Markdown-only text.""" + html_body = "

Line 1
Line 2 & more

" + items = [ + _item( + "1", + "HTML body story", + state="open", + body_markdown=html_body, + ), + ] + content = _build_summarize_prompt_content( + items, + filter_context={"adapter": "ado", "state": "open", "sprint": "—", "assignee": "—", "limit": 10}, + include_value_score=False, + comments_by_item_id={}, + include_comments=True, + ) + # Core text is preserved + assert "Line 1" in content + assert "Line 2" in content + assert "more" in content + # Raw HTML tags and entities are not present + assert " None: + """HTML comments are converted to Markdown-only text in the prompt.""" + items = [ + _item( + "1", + "Story with html comments", + state="open", + body_markdown="Body", + ), + ] + html_comment = "
Comment & note
next line
" + comments_by_id = {"1": [html_comment]} + content = _build_summarize_prompt_content( + items, + filter_context={"adapter": "ado", "state": "open", "sprint": "—", "assignee": "—", "limit": 10}, + include_value_score=False, + comments_by_item_id=comments_by_id, + include_comments=True, + ) + assert "Comment" in content + assert "note" in content + assert "next line" in content + assert "