Skip to content

Commit 65e4051

Browse files
authored
chore(FR-2172): add epic linking, issue linking, and docs-toolkit architecture doc (#5641)
1 parent 60f4837 commit 65e4051

3 files changed

Lines changed: 228 additions & 9 deletions

File tree

AGENTS.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,11 +117,15 @@ Production build (`pnpm run build`) runs these steps sequentially:
117117

118118
- **Tool Requirements**:
119119
- **Jira**: Use `scripts/jira.sh` (REST API with token auth, no MCP dependency)
120-
- `bash scripts/jira.sh create --type Task --title "Title" [--desc "..."] [--labels "l1,l2"]`
120+
- `bash scripts/jira.sh create --type Task --title "Title" [--desc "..."] [--labels "l1,l2"] [--parent FR-XXXX]`
121+
- `bash scripts/jira.sh create --type Epic --title "Epic Title" [--desc "..."] [--labels "l1,l2"]`
121122
- `bash scripts/jira.sh get FR-XXXX`
122-
- `bash scripts/jira.sh update FR-XXXX [--assignee me] [--sprint current]`
123+
- `bash scripts/jira.sh update FR-XXXX [--assignee me] [--sprint current] [--parent FR-XXXX]`
123124
- `bash scripts/jira.sh search "JQL query" [--limit 20]`
124125
- `bash scripts/jira.sh comment FR-XXXX "Comment text"`
126+
- `bash scripts/jira.sh link --from FR-XXXX --to FR-YYYY --type blocks` (link types: blocks, relates, clones, duplicate)
127+
- **Epic linking**: When creating issues under an Epic, ALWAYS use `--parent FR-XXXX` to link them. Issues created without `--parent` will be orphaned and not visible under the Epic.
128+
- **Issue linking**: When Epics or issues have dependencies or relationships, use `jira.sh link` to connect them (e.g. `--type blocks` for dependencies, `--type relates` for related work).
125129
- Setup: `ATLASSIAN_EMAIL` + `ATLASSIAN_API_TOKEN` env vars or `~/.config/atlassian/credentials`
126130
- **GitHub**: Use `gh` CLI (preferred) or GitHub MCP (`mcp__github__*`)
127131
- **Git/PR**: Use Graphite MCP (`mcp__graphite__run_gt_cmd`) for branch/commit/push
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
# docs-toolkit Architecture Reference
2+
3+
Technical reference for the `backend.ai-docs-toolkit` package internals.
4+
This document helps developers and AI agents understand the codebase before making changes.
5+
6+
## Overview
7+
8+
A TypeScript-based documentation engine that transforms Markdown into PDF and HTML output.
9+
Two rendering pipelines share a common markdown processing core.
10+
11+
```
12+
book.config.yaml ──┐
13+
├── markdown-processor.ts ──→ PDF pipeline
14+
docs-toolkit.config.yaml (Playwright → pdf-lib)
15+
├── markdown-processor-web.ts ──→ HTML preview
16+
│ (single-page, live-reload)
17+
src/{lang}/*.md ────┘
18+
```
19+
20+
## File Map
21+
22+
| File | Purpose |
23+
|------|---------|
24+
| `cli.ts` | CLI entry point. Routes commands: `pdf`, `preview`, `preview:html`, `init`, `agents` |
25+
| `config.ts` | Config loading (`docs-toolkit.config.yaml`), defaults, type definitions |
26+
| `markdown-processor.ts` | PDF markdown pipeline. Shared utilities: `slugify`, `deduplicateH1`, `substituteTemplateVars`, `normalizeRstTables`, `convertIndentedNotes`, `resolveMarkdownPath` |
27+
| `markdown-processor-web.ts` | Web HTML pipeline. Two-pass rendering with anchor registry. **Has `multiPage` flag already** |
28+
| `markdown-extensions.ts` | Admonition processing, code block title/highlight parsing, figure labels, image size hints |
29+
| `html-builder.ts` | PDF HTML template (cover page, TOC, chapters with page breaks) |
30+
| `html-builder-web.ts` | Web HTML template (sidebar + content, single-page layout, live-reload script) |
31+
| `styles.ts` | PDF CSS (A4 print layout, CJK typography) |
32+
| `styles-web.ts` | Web CSS (Infima variables, responsive layout, admonition styles) |
33+
| `generate-pdf.ts` | PDF orchestrator. Reads config, processes markdown, renders via Playwright |
34+
| `pdf-renderer.ts` | Playwright PDF rendering, multi-pass page number injection |
35+
| `preview-server.ts` | PDF preview dev server (live-reload) |
36+
| `preview-server-web.ts` | HTML preview dev server (live-reload, image serving) |
37+
| `version.ts` | Version resolution from `package.json` |
38+
| `theme.ts` | PDF theme definitions |
39+
| `sample-content.ts` / `sample-content-markdown.ts` | Style catalog sample content |
40+
| `index.ts` | Public API exports |
41+
42+
## Core Data Types
43+
44+
```typescript
45+
// A processed markdown chapter ready for rendering
46+
interface Chapter {
47+
title: string; // From book.config.yaml nav entry
48+
slug: string; // slugify(title), e.g. "session-page"
49+
htmlContent: string; // Rendered HTML
50+
headings: Heading[]; // Collected during rendering
51+
}
52+
53+
interface Heading {
54+
level: number; // 1-6
55+
text: string; // Plain text (tags stripped)
56+
id: string; // e.g. "session-page-resource-summary-panels"
57+
}
58+
```
59+
60+
## Anchor ID System
61+
62+
All heading IDs follow the pattern: `{chapterSlug}-{headingSlug}`
63+
64+
- Chapter slug: `slugify(nav.title)` from `book.config.yaml`
65+
- Example: `"Session Page"``"session-page"`
66+
- Heading slug: `slugify(headingText)`
67+
- Example: `"Resource Summary Panels"``"resource-summary-panels"`
68+
- Final ID: `"session-page-resource-summary-panels"`
69+
70+
Explicit anchors (`<a id="custom-id">`) keep their raw ID without chapter prefix.
71+
72+
### Anchor Registry (Web pipeline only)
73+
74+
Built in `markdown-processor-web.ts`:
75+
76+
```typescript
77+
interface AnchorRegistry {
78+
anchors: Map<string, AnchorEntry[]>; // rawId → entries across chapters
79+
resolvedIds: Set<string>; // all final IDs for quick lookup
80+
}
81+
```
82+
83+
**Two-pass rendering**:
84+
1. Pass 1: Render all chapters, collect headings and explicit anchors into registry
85+
2. Pass 2: Rewrite `href="#anchor"` links using the registry
86+
87+
**Cross-page link resolution** (`rewriteCrossPageLinks`):
88+
- Same-chapter links: rewrite to chapter-prefixed resolved ID
89+
- Cross-chapter links (single-page mode): rewrite to `#resolvedId`
90+
- Cross-chapter links (**multi-page mode**): rewrite to `./{targetSlug}.html#resolvedId`
91+
- The `multiPage` parameter already exists but is currently always `false`
92+
93+
## Markdown Processing Pipeline
94+
95+
Both PDF and Web pipelines share these preprocessing steps (in order):
96+
97+
1. `deduplicateH1` — Remove duplicate H1 headings (RST migration artifact)
98+
2. `substituteTemplateVars` — Replace `|year|`, `|version|`, `|date|` etc.
99+
3. Image path rewriting — Resolve relative paths for the target environment
100+
4. `normalizeRstTables` — Convert RST grid tables to Markdown tables
101+
5. `convertIndentedNotes` — Convert 3-space indented blocks to blockquotes
102+
6. `processAdmonitions` — Convert `:::note` blocks to HTML divs with icons
103+
7. `processCodeBlockMeta` — Extract `title="..."` and `{1,3-5}` from code fences
104+
105+
Then `marked` renders with a custom renderer that handles headings, images, and code blocks.
106+
107+
## Configuration
108+
109+
### `docs-toolkit.config.yaml` (toolkit-level)
110+
111+
Controls engine behavior: title, company, paths, PDF settings, language labels, agent templates.
112+
113+
Key fields for website feature:
114+
- `languageLabels` — Display names per language
115+
- `localizedStrings` — "User Guide", "Table of Contents" per language
116+
- `admonitionTitles` — Localized admonition type labels
117+
- `figureLabels` — "Figure" label per language
118+
119+
### `src/book.config.yaml` (content-level)
120+
121+
Defines navigation structure per language. Each entry has `title` and `path`:
122+
123+
```yaml
124+
navigation:
125+
en:
126+
- title: Session Page
127+
path: session_page/session_page.md
128+
```
129+
130+
The `path` is relative to `src/{lang}/`.
131+
132+
## Preview Server Architecture
133+
134+
`preview-server-web.ts`:
135+
- Node.js `http.createServer` (no Express or framework)
136+
- Routes: `/` (HTML page), `/__reload` (live-reload polling), `/images/*` (static files)
137+
- File watching with debounced rebuild (300ms)
138+
- Serves images from `src/{lang}/` directory
139+
140+
## Extension Points for Website Feature
141+
142+
### Already prepared
143+
144+
1. **`multiPage` flag** in `rewriteCrossPageLinks()` — enables `./slug.html#id` link format
145+
2. **`AnchorRegistry`** — global anchor index usable for search index building
146+
3. **`Chapter` type** — contains all data needed for individual page generation
147+
4. **`styles-web.ts`** — Infima-based CSS, ready to extend
148+
149+
### Needs to be built
150+
151+
1. **`website-builder.ts`** — Multi-page HTML generator (page template with sidebar, prev/next nav, footer)
152+
2. **`website-generator.ts`** — Build orchestrator (read → process → write individual files + assets)
153+
3. **`search-index-builder.ts`** — Extract text content, build inverted index JSON
154+
4. **`styles-website.ts`** or extend `styles-web.ts` — Additional CSS for pagination, search UI, footer
155+
5. **`cli.ts`** — New `build:web` command
156+
6. **`config.ts`** — New `website` config section (editBaseUrl, GitHub repo info)
157+
158+
### Key design considerations
159+
160+
- **Image path resolution**: Currently resolves to absolute file URLs (PDF) or server-relative paths (preview). Static website needs paths relative to each page's location.
161+
- **CSS delivery**: Currently inlined in `<style>` tag. Static website should use a shared `.css` file to avoid duplication across pages.
162+
- **Search index**: Must support CJK languages (Korean, Japanese, Thai). Bigram tokenization is effective for CJK. The index must work entirely client-side (no server, no CDN) for air-gapped deployments.
163+
- **Last updated date**: `git log -1 --format=%aI -- {filepath}` is the most accurate source. Fallback to `fs.statSync().mtime` for non-git environments. This data should be collected during build and embedded in each page.

scripts/jira.sh

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@
1414
# Or create ~/.config/atlassian/credentials with those two lines.
1515
#
1616
# Usage:
17-
# jira.sh create --type Task --title "Title" [--desc "..."] [--labels "l1,l2"]
17+
# jira.sh create --type Task --title "Title" [--desc "..."] [--labels "l1,l2"] [--parent FR-XXXX]
1818
# jira.sh get FR-XXXX
19-
# jira.sh update FR-XXXX [--assignee me] [--sprint current] [--desc "..."] [--comment "text"]
19+
# jira.sh update FR-XXXX [--assignee me] [--sprint current] [--parent FR-XXXX] [--desc "..."] [--comment "text"]
2020
# jira.sh search "JQL query" [--limit 20]
2121
# jira.sh comment FR-XXXX "Comment text"
22+
# jira.sh link --from FR-XXXX --to FR-YYYY [--type blocks|relates|clones|duplicate]
2223
# jira.sh myself
2324

2425
set -euo pipefail
@@ -340,13 +341,14 @@ get_current_sprint_id() {
340341
# ── Commands ─────────────────────────────────────────────────
341342

342343
cmd_create() {
343-
local type="Task" title="" desc="" labels=""
344+
local type="Task" title="" desc="" labels="" parent=""
344345
while (( $# )); do
345346
case $1 in
346347
--type) type=$2; shift 2 ;;
347348
--title) title=$2; shift 2 ;;
348349
--desc) desc=$2; shift 2 ;;
349350
--labels) labels=$2; shift 2 ;;
351+
--parent) parent=$2; shift 2 ;;
350352
*) die "create: unknown flag $1" ;;
351353
esac
352354
done
@@ -361,6 +363,12 @@ cmd_create() {
361363
desc_json=$(md_to_adf "$desc")
362364
fi
363365

366+
# Build parent field (for linking to Epic)
367+
local parent_json="null"
368+
if [[ -n "$parent" ]]; then
369+
parent_json=$(jq -n --arg key "$parent" '{key: $key}')
370+
fi
371+
364372
local payload
365373
payload=$(jq -n \
366374
--arg summary "$title" \
@@ -369,6 +377,7 @@ cmd_create() {
369377
--argjson desc "$desc_json" \
370378
--argjson labels "$labels_json" \
371379
--argjson repo "$GITHUB_REPO_FIELD_VALUE" \
380+
--argjson parent "$parent_json" \
372381
'{
373382
fields: {
374383
project: { key: $project },
@@ -378,7 +387,8 @@ cmd_create() {
378387
labels: $labels,
379388
customfield_10173: $repo
380389
}
381-
}')
390+
}
391+
| if $parent != null then .fields.parent = $parent else . end')
382392

383393
local result
384394
result=$(api POST "/issue" -d "$payload")
@@ -390,7 +400,7 @@ cmd_create() {
390400
cmd_get() {
391401
local key=${1:?get: issue key required}
392402
local raw
393-
raw=$(api GET "/issue/${key}?fields=summary,status,assignee,description,labels,customfield_10020,customfield_10170")
403+
raw=$(api GET "/issue/${key}?fields=summary,status,assignee,description,labels,parent,issuetype,customfield_10020,customfield_10170")
394404

395405
# Extract description as raw JSON (may be ADF object or string)
396406
local desc_raw
@@ -400,9 +410,11 @@ cmd_get() {
400410

401411
echo "$raw" | jq --arg desc "$desc_text" '{
402412
key: .key,
413+
type: .fields.issuetype.name,
403414
summary: .fields.summary,
404415
status: .fields.status.name,
405416
assignee: (.fields.assignee.displayName // "Unassigned"),
417+
parent: (.fields.parent.key // null),
406418
labels: .fields.labels,
407419
github_issue_url: (.fields.customfield_10170 // null),
408420
description: $desc
@@ -420,6 +432,9 @@ cmd_update() {
420432
aid=$(resolve_assignee "$2")
421433
fields=$(echo "$fields" | jq --arg id "$aid" '. + {assignee:{accountId:$id}}')
422434
shift 2 ;;
435+
--parent)
436+
fields=$(echo "$fields" | jq --arg key "$2" '. + {parent:{key:$key}}')
437+
shift 2 ;;
423438
--sprint)
424439
local sid
425440
if [[ $2 == "current" ]]; then
@@ -548,6 +563,39 @@ cmd_check_dup() {
548563
fi
549564
}
550565

566+
cmd_link() {
567+
local from="" to="" type="Relates"
568+
while (( $# )); do
569+
case $1 in
570+
--from) from=$2; shift 2 ;;
571+
--to) to=$2; shift 2 ;;
572+
--type) type=$2; shift 2 ;;
573+
*) die "link: unknown flag $1" ;;
574+
esac
575+
done
576+
[[ -n "$from" ]] || die "link: --from required"
577+
[[ -n "$to" ]] || die "link: --to required"
578+
579+
# Map shorthand names to Jira link type names
580+
local link_type="$type"
581+
case "$type" in
582+
blocks|Blocks) link_type="Blocks" ;;
583+
relates|Relates) link_type="Relates" ;;
584+
clones|Cloners) link_type="Cloners" ;;
585+
duplicate|Duplicate) link_type="Duplicate" ;;
586+
*) link_type="$type" ;;
587+
esac
588+
589+
# outwardIssue = --from (the one that "blocks"/"relates to"/etc.)
590+
# inwardIssue = --to (the one that "is blocked by"/"relates to"/etc.)
591+
api POST "/issueLink" -d "$(jq -n \
592+
--arg type "$link_type" \
593+
--arg from "$from" \
594+
--arg to "$to" \
595+
'{type:{name:$type}, outwardIssue:{key:$from}, inwardIssue:{key:$to}}')" > /dev/null
596+
echo "Linked: ${from} --[${link_type}]--> ${to}"
597+
}
598+
551599
# ── Main ─────────────────────────────────────────────────────
552600
require_jq
553601
cmd=${1:-help}; shift 2>/dev/null || true
@@ -558,14 +606,17 @@ case $cmd in
558606
Usage: jira.sh <command> [options]
559607
560608
Commands:
561-
create --type Task --title "Title" [--desc "..."] [--labels "l1,l2"]
609+
create --type Task --title "Title" [--desc "..."] [--labels "l1,l2"] [--parent FR-XXXX]
562610
get FR-XXXX
563-
update FR-XXXX [--assignee me] [--sprint current] [--desc "..."] [--comment "text"]
611+
update FR-XXXX [--assignee me] [--sprint current] [--parent FR-XXXX] [--desc "..."] [--comment "text"]
564612
search "JQL query" [--limit 20]
565613
comment FR-XXXX "Comment text"
614+
link --from FR-XXXX --to FR-YYYY [--type blocks|relates|clones|duplicate]
566615
check-dup --labels "l1,l2" [--include-done] Check for duplicate issues by labels
567616
myself Show current user info
568617
618+
The --parent flag links an issue to a parent Epic (e.g. --parent FR-1234).
619+
The link command creates issue links (e.g. "FR-1 blocks FR-2", "FR-1 relates to FR-2").
569620
Description and comment fields accept Markdown, which is automatically
570621
converted to Atlassian Document Format (ADF) for proper Jira rendering.
571622
@@ -581,6 +632,7 @@ USAGE
581632
update) cmd_update "$@" ;;
582633
search) cmd_search "$@" ;;
583634
comment) cmd_comment "$@" ;;
635+
link) cmd_link "$@" ;;
584636
check-dup) cmd_check_dup "$@" ;;
585637
myself) cmd_myself ;;
586638
*) die "Unknown command: $cmd" ;;

0 commit comments

Comments
 (0)