diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json index e89b685..7d0c4c3 100644 --- a/.agents/plugins/marketplace.json +++ b/.agents/plugins/marketplace.json @@ -8,7 +8,7 @@ "plugins": [ { "name": "ia", - "version": "1.2.0", + "version": "1.2.1", "description": "IBM i Impact Analysis — dependency tracing, field impact, call hierarchy, program documentation.", "source": "./" } diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index e89b685..7d0c4c3 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -8,7 +8,7 @@ "plugins": [ { "name": "ia", - "version": "1.2.0", + "version": "1.2.1", "description": "IBM i Impact Analysis — dependency tracing, field impact, call hierarchy, program documentation.", "source": "./" } diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 6510a4d..ec80dc2 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "ia", - "version": "1.2.0", + "version": "1.2.1", "description": "IBM i Impact Analysis — dependency tracing, field impact, call hierarchy, program documentation.", "homepage": "https://github.com/programmersio-ibmi/skills", "license": "MIT", diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 9da910c..4d4fcc0 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "ia", - "version": "1.2.0", + "version": "1.2.1", "description": "IBM i Impact Analysis for coding agents.", "homepage": "https://github.com/programmersio-ibmi/skills", "license": "MIT", diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index 48e5451..e02366b 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "ia", "displayName": "iA — IBM i Impact Analysis", - "version": "1.2.0", + "version": "1.2.1", "description": "IBM i Impact Analysis — dependency tracing, field impact, call hierarchy, program documentation.", "publisher": "programmersio", "homepage": "https://github.com/programmersio-ibmi/skills", diff --git a/.github/plugin/marketplace.json b/.github/plugin/marketplace.json index e89b685..7d0c4c3 100644 --- a/.github/plugin/marketplace.json +++ b/.github/plugin/marketplace.json @@ -8,7 +8,7 @@ "plugins": [ { "name": "ia", - "version": "1.2.0", + "version": "1.2.1", "description": "IBM i Impact Analysis — dependency tracing, field impact, call hierarchy, program documentation.", "source": "./" } diff --git a/CHANGELOG.md b/CHANGELOG.md index c232199..d74524e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,21 @@ All notable changes to this project are documented here. The format is loosely based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.2.1] — 2026-07-19 + +### Added + +- **Source-freshness gates** in the program-documentation workflow (`references/program-documentation.md`): + - Quality Report footer now carries a source fingerprint (member changed date/time, total lines, repo schema). + - Export staleness gate: before converting a spec not generated in the current session, compare its fingerprint against a live `ia_member_lookup` and offer regeneration if the source has changed. + - Verification rule for absolute access-method claims ("SQL-only", "no native I/O") — such claims now require in-session proof from F-specs plus a native-opcode source search. +- `--no-cover` flag on `scripts/convert_md_to_docx.py` to skip the cover page and TOC for compact 1–2 page documents. + +### Changed + +- Both converters (`convert_md_to_docx.py`, `convert_md_to_pdf.py`) no longer force a page break before every major section; headings are kept with following content so they never strand at the bottom of a page. +- DOCX converter resolves nested inline formatting (e.g. code spans) inside link text instead of emitting raw placeholders. + ## [1.2.0] — 2026-06-27 ### Added diff --git a/gemini-extension.json b/gemini-extension.json index 5032408..df0b9d5 100644 --- a/gemini-extension.json +++ b/gemini-extension.json @@ -1,6 +1,6 @@ { "name": "ia", - "version": "1.2.0", + "version": "1.2.1", "description": "IBM i Impact Analysis — dependency tracing, field impact, call hierarchy, program documentation.", "contextFileName": "GEMINI.md" } diff --git a/package.json b/package.json index d932fdd..cb4cf9c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ia-tools-skill", - "version": "1.2.0", + "version": "1.2.1", "description": "IBM i Impact Analysis — dependency tracing, field impact, call hierarchy, program documentation.", "license": "MIT", "type": "module", diff --git a/plugin.json b/plugin.json index 3c15476..f3e77d7 100644 --- a/plugin.json +++ b/plugin.json @@ -1,6 +1,6 @@ { "name": "ia", - "version": "1.2.0", + "version": "1.2.1", "description": "IBM i Impact Analysis — dependency tracing, field impact, call hierarchy, program documentation.", "publisher": "programmersio", "homepage": "https://github.com/programmersio-ibmi/skills", diff --git a/skills/ia/references/program-documentation.md b/skills/ia/references/program-documentation.md index f80457b..6675631 100644 --- a/skills/ia/references/program-documentation.md +++ b/skills/ia/references/program-documentation.md @@ -462,6 +462,8 @@ PROGRAMNAME (LIBRARY) | ✓ Subroutine line ranges | Every subroutine block header shows `lines START–END`; every internal anchor has `*(line NNN)*` | **ERROR** — add line numbers | | ✓ Source isolation | No file under `docs/program-specs/` was read during generation (only directory listed for versioning) | **ERROR** — regenerate from iA tools only | | ✓ DocType = Test_Case_Document | Run both gates in [`test-case-generation.md`](test-case-generation.md) §5: `python scripts/validate_testcases.py ` exits 0, then the TC content-check table | **ERROR** — fix every finding before delivery | +| ✓ Absolute access-method claims | Any "all / only / no / never" claim about data access (e.g. "SQL-only — no CHAIN/READ") is backed by in-session proof: `ia_rpg_source(source_spec='F')` shows only WORKSTN files **and** `ia_rpg_source_search` for native opcodes (`CHAIN`, `READ`, `WRITE `, `UPDATE `, `SETLL`) over the member returns no database-file hits | **ERROR** — drop the absolute; describe access as mixed, per file | +| ✓ Source fingerprint in footer | Quality Report Freshness row carries the source fingerprint: member changed date/time + total lines + repo schema (all already in hand from Step 2 LOOKUP/COMPLEXITY) | **ERROR** — add fingerprint | Fix all **ERRORs** before delivery. Present **WARNINGs** with notes in the quality metrics footer. @@ -529,7 +531,7 @@ Files like `BIO60R_Specification.md`, `IAMENUR_Doc.md`, `{PGM}_Technical_Specifi | Verification Rules | % | ✅/❌ /7 rules passed | | Business Rules Coverage | % | ✅/⚠️ BRs for subroutine clusters | | Source Traceability | ✅/⚠️ | Line numbers included: YES/NO | -| Freshness | Current | ✅ Generated | +| Freshness | Current | ✅ Generated · source changed · lines · repo | **Validation Warnings (if any):** - @@ -579,6 +581,7 @@ Each script writes its output next to the source `.md` with the `.docx` / `.pdf` **Operational rules:** +- **Staleness gate** — if the `.md` being exported was **not generated in this session**, run `ia_member_lookup(member_name)` and compare against the doc's Freshness fingerprint line (grep that **one line only** from the file — the single sanctioned read under `docs/program-specs/`; never read other content). If the changed date/time or line count differ, stop: tell the user the source has changed since the doc was generated and offer to regenerate before exporting. - **One format at a time** — only generate the format the user asked for. Don't produce both unless explicitly requested. - **Missing dependency** — if a script exits with `Error: python-docx is not installed` (or `reportlab`), run the printed `pip install …` command, then re-run the conversion. Do not silently swap to a different tool. - **No network calls expected** — both scripts are self-contained. The DOCX converter only reaches `mermaid.ink` if it encounters a ```` ```mermaid ```` fenced block; specs produced by this skill use text-based ASCII trees instead, so this path is not exercised. @@ -615,6 +618,7 @@ Each script writes its output next to the source `.md` with the `.docx` / `.pdf` | Output filename must match canonical pattern `{PGM}_{CanonicalDocType}.md` from Step 7.5 | CanonicalDocType ∈ {Technical_Specification, Functional_Document, Operations_Guide, Architecture_Review, Test_Case_Document}, no version suffix; any other shape = **ERROR**, do not write | | Run the Step 1.5 existence check before any `ia_*` call; if the canonical doc exists, surface it (date + link) and get confirmation before regenerating | Avoids silently regenerating a doc the user already has and avoids wasting iA calls when they decline | | `TodoWrite` must run after Step 1.5 and before any `ia_*` MCP call | Without an explicit plan, agents skip verification, invent filenames, or drop steps; the todo list is the agent's self-checklist | +| Never make an absolute access-method claim ("all data access via SQL", "no native I/O") without in-session proof from F-specs + a native-opcode source search | A later mod that adds native I/O turns a confident absolute into a QA-visible falsehood; absolutes require machine proof, not impressions | --- @@ -639,6 +643,7 @@ Each script writes its output next to the source `.md` with the `.docx` / `.pdf` | Regenerated a doc that already exists without asking | Replaced the user's current spec, or re-ran all the iA queries when they'd have declined | Step 1.5 is mandatory — list the folder, surface the existing canonical doc (date + link), get confirmation before any `ia_*` call | | Skipped `TodoWrite` kickoff | Agent jumped straight from Step 1.5 to `ia_program_spec_bundle` | Step 1.6 is mandatory; create the todo list before any `ia_*` call | | Missing ASCII process flow tree | Generated doc has no visual flow in the section required by its template | One ASCII tree (fenced `text` block, box-drawing chars) per template-required section | +| Stale spec vs live source | QA flags operations (e.g. native READ/WRITE) the doc says don't exist; doc line refs don't match current source | The doc describes an older source version. Compare its Freshness fingerprint to `ia_member_lookup` (changed date + line count) — if the member changed after generation, regenerate; don't patch the old doc | --- diff --git a/skills/ia/scripts/convert_md_to_docx.py b/skills/ia/scripts/convert_md_to_docx.py index 73f43a2..23fa72a 100644 --- a/skills/ia/scripts/convert_md_to_docx.py +++ b/skills/ia/scripts/convert_md_to_docx.py @@ -275,7 +275,16 @@ def render(segment: str, bold: bool = False, italic: bool = False): run.italic = True elif kind == 'L': ltext, lurl = link_spans[idx_s] - add_hyperlink(para, lurl, ltext) + # Link text may contain nested placeholders (e.g. a code + # span saved before links) — resolve them to plain text. + def _resolve(pm): + k, i2 = pm.group(1), int(pm.group(2)) + return {'C': code_spans, 'X': bi_spans, 'B': bold_spans, + 'I': italic_spans, 'E': esc_spans, + 'L': [t for t, _ in link_spans]}[k][i2] + while placeholder_re.search(ltext): + ltext = re.sub(r'\x00([CLXBIE])(\d+)\x00', _resolve, ltext) + add_hyperlink(para, lurl, _xml_clean(ltext)) elif kind == 'X': render(bi_spans[idx_s], bold=True, italic=True) elif kind == 'B': @@ -934,7 +943,8 @@ def setup_header_footer(doc: Document, title: str, meta: Dict[str, str]): # ── Main converter ──────────────────────────────────────────────────── -def convert_md_to_docx(md_file: str, docx_file: Optional[str] = None) -> str: +def convert_md_to_docx(md_file: str, docx_file: Optional[str] = None, + cover: bool = True) -> str: try: with open(md_file, 'r', encoding='utf-8') as f: raw = f.read() @@ -961,11 +971,16 @@ def convert_md_to_docx(md_file: str, docx_file: Optional[str] = None) -> str: configure_document_defaults(doc) setup_header_footer(doc, title, meta) - add_cover_page(doc, meta, title) - add_toc_page(doc) + if cover: + add_cover_page(doc, meta, title) + add_toc_page(doc) + else: + p = doc.add_heading(level=1) + parse_inline_formatting(p, title) + p.alignment = WD_ALIGN_PARAGRAPH.LEFT + p.paragraph_format.keep_with_next = True enable_update_fields(doc) - section_count = 0 i = meta_skip while i < len(lines): line = lines[i] @@ -989,11 +1004,10 @@ def convert_md_to_docx(md_file: str, docx_file: Optional[str] = None) -> str: parse_inline_formatting(p, heading_text) if level == 1: p.alignment = WD_ALIGN_PARAGRAPH.LEFT - if level == 2: - # Each major section starts on a fresh page (after the first) - section_count += 1 - if section_count >= 2: - p.paragraph_format.page_break_before = True + # Never force a page break per section — let content flow. + # Keep the heading glued to what follows so it can't strand + # alone at the bottom of a page. + p.paragraph_format.keep_with_next = True i += 1 continue @@ -1107,13 +1121,15 @@ def main(): ) parser.add_argument('input', help='Input Markdown file') parser.add_argument('output', nargs='?', help='Output DOCX file (default: same name with .docx)') + parser.add_argument('--no-cover', action='store_true', + help='Skip the cover page and table of contents (for compact 1-2 page documents)') args = parser.parse_args() if not os.path.exists(args.input): print(f"Error: Input file '{args.input}' not found.") sys.exit(1) - output_file = convert_md_to_docx(args.input, args.output) + output_file = convert_md_to_docx(args.input, args.output, cover=not args.no_cover) try: print(f"[OK] Created: {output_file}") except UnicodeEncodeError: diff --git a/skills/ia/scripts/convert_md_to_pdf.py b/skills/ia/scripts/convert_md_to_pdf.py index 7e0cb6d..6efd6cd 100644 --- a/skills/ia/scripts/convert_md_to_pdf.py +++ b/skills/ia/scripts/convert_md_to_pdf.py @@ -592,7 +592,6 @@ def md_to_pdf_elements(md_content: str, doc_title: str = '') -> List[Flowable]: styles = make_styles() elements = [] lines = md_content.split('\n') - section_count = 0 _, meta_skip = extract_metadata(md_content) i = meta_skip while i < len(lines): @@ -614,9 +613,8 @@ def md_to_pdf_elements(md_content: str, doc_title: str = '') -> List[Flowable]: elements.append(Paragraph(inline_md_to_xml(line[4:].strip()), styles['h3'])) i += 1 elif line.startswith('## '): - section_count += 1 - if section_count >= 2: - elements.append(PageBreak()) + # No forced page break per section — headings use keepWithNext + # so they never strand alone at the bottom of a page. elements.append(Spacer(1, 8)) elements.append(Paragraph(inline_md_to_xml(line[3:].strip()), styles['h2'])) elements.append(SectionRule())