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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,10 @@ coverage.xml
**/.cache/decisions.jsonl.1
**/.cache/decisions.jsonl.lock

# doc-index cache (doc_index.py) — per-project structural index for JIT
# retrieval, read-once-per-file. Rebuilds automatically on content change.
**/.cache/doc-index/

# Superpowers brainstorming/planning specs (local-only)
docs/superpowers/

Expand Down
37 changes: 35 additions & 2 deletions architecture/features/traceability-validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
- [List ID Kinds](#list-id-kinds)
- [Validate TOC](#validate-toc)
- [TOC Utilities](#toc-utilities)
- [Document Index](#document-index)
- [Markdown Parsing Utilities](#markdown-parsing-utilities)
- [Fixing Prompt Enrichment](#fixing-prompt-enrichment)
- [Headings Contract Validation](#headings-contract-validation)
Expand Down Expand Up @@ -398,11 +399,11 @@ Catches structural and traceability issues that AI agents miss or hallucinate
2. [x] - `p1` - Generate expected TOC from headings - `inst-toc-generate-expected`
3. [x] - `p1` - Compare existing vs expected: check anchor validity, heading coverage, staleness - `inst-toc-compare`
4. [x] - `p1` - **IF** mismatch, record error with diff details - `inst-toc-if-mismatch`
4. [x] - `p1` - **RETURN** JSON: `{status, files_checked, errors}` - `inst-toc-return`
4. [x] - `p1` - **RETURN** JSON: `{status, files_validated, error_count, warning_count, results}`, each `results[]` entry `{file, status, error_count, warning_count}` plus `errors`/`warnings` arrays when `--verbose` or non-empty - `inst-toc-return`

**Supporting**:
- [x] - `p1` - Imports and module setup for validate-toc command - `inst-toc-imports`
- [x] - `p1` - Human-friendly formatter for validate-toc output - `inst-toc-format`
- [x] - `p1` - Human-friendly formatter for validate-toc output: a WARN-only file prints its warnings the same way a FAIL file prints its errors, not just the bare status - `inst-toc-format`

### TOC Utilities

Expand All @@ -416,6 +417,9 @@ Catches structural and traceability issues that AI agents miss or hallucinate
4. [x] - `p1` - Insert/update TOC using heading-based insertion (`## Table of Contents`) for kit file generator - `inst-toc-util-insert-heading`
5. [x] - `p1` - Process file: strip manual TOC, insert marker-based TOC, write if changed - `inst-toc-util-process-file`
6. [x] - `p1` - Validate TOC: check existence, anchor validity, completeness, staleness - `inst-toc-util-validate`
7. [x] - `p1` - Parse headings with line numbers, fence-aware and front-matter-aware (skips a leading YAML block so a `#`-prefixed front-matter line is never mistaken for a heading); shared by doc-index and the JIT-retrieval readiness checks, which need section boundaries the plain heading list doesn't carry -- `parse_headings` itself now delegates here, stripping the line number, so both share one fence/heading-match implementation - `inst-toc-util-parse-headings-lines`
8. [x] - `p1` - Collect JIT-retrieval readiness warnings for a document: gathers all four signals below over *every* heading level, independent of whatever level cap the caller configured for TOC-completeness checking - `inst-toc-jit-readiness-collect`
9. [x] - `p1` - Compute the four JIT-retrieval readiness signals -- duplicate heading titles (compared case-insensitively, with internal whitespace collapsed and Unicode-normalized, though the original text is still shown in the warning), heading depth jumps, oversized sections (`--max-section-lines`, default 300, validated against non-finite/non-positive input independent of the CLI's own argparse guard), and a missing top-of-file description/frontmatter block -- all warning-only, never errors (see constructorfabric/studio#104) - `inst-toc-jit-readiness`

**Supporting**:
- [x] - `p1` - Imports, constants, fence tracking, GitHub anchor slug generation - `inst-toc-util-datamodel`
Expand All @@ -437,6 +441,35 @@ Catches structural and traceability issues that AI agents miss or hallucinate
- [x] - `p1` - Heading-based TOC new-insert branch: inject `## Table of Contents` before first heading when absent - `inst-toc-util-insert-heading-new`
- [x] - `p1` - TOC validate init: build heading list and expected TOC string before comparison checks - `inst-toc-util-validate-init`

### Document Index

- [x] `p1` - **ID**: `cpt-studio-algo-traceability-validation-doc-index`

**Input**: Markdown file path

**Output**: `cfs doc-index`'s JSON is `{file, cache_hit, total_lines, section_count, sections}`, each `sections[]` entry `{level, heading, line_start, line_end, summary}`. The underlying index dict additionally carries `schema_version`, `path`, and `etag`.

A cached, read-once-per-file structural index for Markdown JIT retrieval (see
constructorfabric/studio#104): parsing a file's headings/section boundaries
happens once, not once per query, until its stat fingerprint (`mtime_ns` +
size) changes.
The cache-validity fingerprint is deliberately metadata-only (`mtime` + file
size via `Path.stat()`), never a content hash — the point of the cache is to
avoid reading the file at all on a hit, and a content hash would defeat that
by requiring the read it's meant to save.

1. [x] - `p1` - Build a fresh structural index: parse headings + line ranges from current content, compute the stat-based fingerprint, stamp the current schema version - `inst-doc-index-build`
2. [x] - `p1` - Load a cached index for a file, validated against current stat metadata (no content read on a hit) and against the required-field shape at the current schema version; returns `None` if missing, stale, corrupt, or an incomplete/outdated shape - `inst-doc-index-load`
3. [x] - `p1` - Persist an index to its cache location atomically (temp file + `os.replace`, so a concurrent reader never observes a torn write); no-ops silently outside a Studio-adapted project - `inst-doc-index-save`
4. [x] - `p1` - Return the cached index or build-and-cache a fresh one; reports cache hit/miss for benchmarking - `inst-doc-index-get-or-build`
5. [x] - `p1` - Attach a one-line, LLM-authored summary to a cached section by its `line_start`, for a future per-section-summary caller - `inst-doc-index-annotate`

**Supporting**:
- [x] - `p1` - Stat-based cache-validity fingerprint (`mtime_ns` + size); resolved from the file's own path, never a content hash - `inst-doc-index-etag`
- [x] - `p1` - Resolve the cache file location within the Studio directory owning the indexed file, resolved from the file's own path (not the process's working directory) - `inst-doc-index-cache-path`
- [x] - `p1` - `cfs doc-index` CLI wrapper: parse arguments, build the JSON output payload, reporting a clean error for a missing or unreadable file - `inst-doc-index-cmd`
- [x] - `p1` - Human-friendly formatter for `cfs doc-index` output - `inst-doc-index-cmd-format`

### Markdown Parsing Utilities

- [x] `p1` - **ID**: `cpt-studio-algo-traceability-validation-parsing-utils`
Expand Down
9 changes: 8 additions & 1 deletion skills/studio/scripts/studio/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,10 @@ def _cmd_eval(argv: List[str]) -> int:
from .commands.eval import cmd_eval
return cmd_eval(argv)

def _cmd_doc_index(argv: List[str]) -> int:
from .commands.doc_index import cmd_doc_index
return cmd_doc_index(argv)

# =============================================================================
# ADAPTER COMMAND
# =============================================================================
Expand Down Expand Up @@ -216,6 +220,7 @@ def _cmd_map(argv: List[str]) -> int:
"resolve-vars": "Resolve template variables to absolute paths",
"toc": "Generate/update Table of Contents",
"chunk-input": "Chunk oversized workflow input into line-bounded Markdown files",
"doc-index": "Build/reuse a cached heading index for a Markdown file (read once, not per query)",
"pdsl": "Validate PDSL prompt blocks",
"workspace-init": "Initialize multi-repo workspace",
"workspace-add": "Add a source to workspace config",
Expand All @@ -232,7 +237,7 @@ def _cmd_map(argv: List[str]) -> int:
("Validation", ["validate", "validate-kits", "validate-toc", "spec-coverage", "check-language"]),
("Search & Navigation", ["list-ids", "list-id-kinds", "get-content", "where-defined", "where-used"]),
("Kit Management", ["kit"]),
("Utility", ["toc", "chunk-input", "pdsl"]),
("Utility", ["toc", "chunk-input", "doc-index", "pdsl"]),
("Workspace", ["workspace-init", "workspace-add", "workspace-info", "workspace-sync"]),
("Delegation", ["delegate"]),
("Diagnostics", ["doctor"]),
Expand Down Expand Up @@ -263,6 +268,7 @@ def _cmd_map(argv: List[str]) -> int:
"validate-toc": "_cmd_validate_toc",
"spec-coverage": "_cmd_spec_coverage",
"chunk-input": "_cmd_chunk_input",
"doc-index": "_cmd_doc_index",
"workspace-init": "_cmd_workspace_init",
"workspace-add": "_cmd_workspace_add",
"workspace-info": "_cmd_workspace_info",
Expand Down Expand Up @@ -296,6 +302,7 @@ def _cmd_map(argv: List[str]) -> int:
_cmd_validate_toc,
_cmd_spec_coverage,
_cmd_chunk_input,
_cmd_doc_index,
_cmd_workspace_init,
_cmd_workspace_add,
_cmd_workspace_info,
Expand Down
78 changes: 78 additions & 0 deletions skills/studio/scripts/studio/commands/doc_index.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"""Studio doc-index command — build/reuse a cached structural index for a
Markdown file, so heading-based JIT retrieval reads a file's structure once,
not once per query.

Thin CLI wrapper around ``studio.utils.doc_index``.
"""

import argparse
import logging
from pathlib import Path
from typing import List

from ..utils.doc_index import get_or_build_doc_index
from ..utils.ui import ui

logger = logging.getLogger(__name__)


# @cpt-begin:cpt-studio-algo-traceability-validation-doc-index:p1:inst-doc-index-cmd
def cmd_doc_index(argv: List[str]) -> int:
"""Build (or reuse the cached) structural index for a Markdown file."""
p = argparse.ArgumentParser(
prog="cfs doc-index",
description=(
"Build or reuse a cached heading/section index for a Markdown file, "
"so navigation reads the file's structure once, not once per query."
),
)
p.add_argument("file", help="Markdown file path")
p.add_argument(
"--rebuild",
action="store_true",
help="Force a fresh build even if a valid cached index exists",
)
args = p.parse_args(argv)

filepath = Path(args.file).resolve()
if not filepath.is_file():
ui.result(
{"file": str(filepath), "status": "ERROR", "message": "File not found"},
human_fn=lambda d: ui.error(f"{d['file']}: {d['message']}"),
)
return 2

try:
index = get_or_build_doc_index(filepath, force_rebuild=args.rebuild)
except (OSError, UnicodeDecodeError) as exc:
logger.warning("doc-index: cannot read %s: %s", filepath, exc)
ui.result(
{"file": str(filepath), "status": "ERROR", "message": f"Cannot read file: {exc}"},
human_fn=lambda d: ui.error(f"{d['file']}: {d['message']}"),
)
return 2

output = {
"file": str(filepath),
"cache_hit": index["cache_hit"],
"total_lines": index["total_lines"],
"section_count": len(index["sections"]),
"sections": index["sections"],
}
ui.result(output, human_fn=_human_doc_index)
return 0
# @cpt-end:cpt-studio-algo-traceability-validation-doc-index:p1:inst-doc-index-cmd


# @cpt-begin:cpt-studio-algo-traceability-validation-doc-index:p1:inst-doc-index-cmd-format
def _human_doc_index(data: dict) -> None:
ui.header("Doc Index")
ui.substep(data["file"])
hit = "cache hit — reused existing index" if data["cache_hit"] else "cache miss — built fresh index"
ui.substep(hit)
ui.substep(f"{data['section_count']} section(s), {data['total_lines']} total lines")
for s in data["sections"]:
summary = f" — {s['summary']}" if s.get("summary") else ""
ui.substep(f" H{s['level']} [{s['line_start']}-{s['line_end']}] {s['heading']}{summary}")
ui.blank()
# @cpt-end:cpt-studio-algo-traceability-validation-doc-index:p1:inst-doc-index-cmd-format
13 changes: 12 additions & 1 deletion skills/studio/scripts/studio/commands/validate_toc.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from pathlib import Path
from typing import List

from ..utils.toc import add_toc_max_level_argument, validate_toc
from ..utils.toc import DEFAULT_MAX_SECTION_LINES, add_toc_max_level_argument, validate_toc
from ..utils.ui import ui
# @cpt-end:cpt-studio-algo-traceability-validation-validate-toc:p1:inst-toc-imports

Expand All @@ -31,6 +31,12 @@
help="Markdown file path(s) to validate",
)
add_toc_max_level_argument(p)
p.add_argument(
"--max-section-lines",
type=int,
default=DEFAULT_MAX_SECTION_LINES,
help=f"Warn when a section exceeds this many lines (default: {DEFAULT_MAX_SECTION_LINES})",
)
p.add_argument(
"--verbose",
action="store_true",
Expand Down Expand Up @@ -63,6 +69,7 @@
content,
artifact_path=filepath,
max_heading_level=args.max_level,
max_section_lines=args.max_section_lines,

@ainetx ainetx Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JIT-readiness warnings invisible in human CLI output

Severity: Major

Problem
The human-readable output branch for a WARN-only file (_human_validate_toc's final else:, line 134) prints only f"{path}: {status}" and never iterates r.get("warnings", []), unlike the FAIL branch just above it which does. Since this PR's four new JIT-retrieval readiness checks (duplicate headings, heading-depth jumps, oversized sections, missing description) are warning-only by design, any file that trips only these new checks lands in this silent branch. (Anchored here at the new max_section_lines wiring since line 134 itself isn't touched by this diff, but its behavior is what this PR's new warnings now depend on.) Separately, _human_doc_index in commands/doc_index.py never prints the target file name either.

How to reproduce

  1. Create a Markdown file with one duplicate heading title and no other TOC issues.
  2. Run cfs validate-toc file.md (human mode, no --json).

Expected behavior
The new warning (e.g. toc-heading-duplicate: ...) is printed, the same way an error would be under FAIL.

Actual behavior
Output is just file.md: WARN with no indication of what's wrong — the JSON output (--json) does contain the warning, so the information exists but never reaches the default human-facing path.

cmd_validate_toc()
   -> validate_toc() returns {status: "WARN", warnings: [...]}
   -> _human_validate_toc()
        status == "WARN"  -->  else: branch
                                   prints "path: WARN" only
                                   (warnings list never read)

Impact
The PR's headline feature (JIT-readiness warnings) is effectively invisible to anyone using the default CLI output instead of --json.

Suggested correction
Add a branch for status == "WARN" (or extend the else) that iterates r.get("warnings", []) the same way the FAIL branch iterates r.get("errors", []); print data["file"] in _human_doc_index.

How to verify
Add/extend a TestCmdValidateToc case: a file with exactly one warning and no errors, run through cmd_validate_toc's human-output path, and assert the warning text appears in the captured output.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 1854c12: _human_validate_toc now has an explicit elif status == "WARN": branch that iterates r.get("warnings", []) the same way the FAIL branch iterates errors, and _human_doc_index now prints data['file']. Regression tests: test_warn_only_file_prints_warnings_in_human_output, test_all_four_jit_warnings_zero_errors_exits_clean_via_cli.

)

errors = report.get("errors", [])
Expand Down Expand Up @@ -108,7 +115,7 @@
# @cpt-end:cpt-studio-algo-traceability-validation-validate-toc:p1:inst-toc-return

# @cpt-begin:cpt-studio-algo-traceability-validation-validate-toc:p1:inst-toc-format
def _human_validate_toc(data: dict) -> None:

Check failure on line 118 in skills/studio/scripts/studio/commands/validate_toc.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 18 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=constructorfabric_studio&issues=AaBarO0cMC2nKLsW5k8e&open=AaBarO0cMC2nKLsW5k8e&pullRequest=108
ui.header("Validate TOC")
for r in data.get("results", []):
path = r.get("file", "?")
Expand All @@ -123,6 +130,10 @@
ui.substep(f" ✗ {e}")
for w in r.get("warnings", []):
ui.substep(f" ⚠ {w}")
elif status == "WARN":
ui.warn(f"{path}: {warns} warning(s)")
for w in r.get("warnings", []):
ui.substep(f" ⚠ {w}")
else:
ui.substep(f"{path}: {status}")
overall = data.get("status", "")
Expand Down
Loading
Loading