Skip to content

feat(doc-index): infer real section granularity, hash sections for staleness - #1

Open
tkcoding wants to merge 8 commits into
jit-retrieval-doc-indexfrom
jit-retrieval-cascade
Open

feat(doc-index): infer real section granularity, hash sections for staleness#1
tkcoding wants to merge 8 commits into
jit-retrieval-doc-indexfrom
jit-retrieval-cascade

Conversation

@tkcoding

Copy link
Copy Markdown
Owner

Stacked on constructorfabric#108 (constructorfabric#108)

This branches from jit-retrieval-doc-index, the branch behind constructorfabric#108 — it needs doc_index.py/toc.py to exist first, so it's opened here (within this fork) rather than against upstream directly, since GitHub won't let a cross-repo PR base on a branch that only exists on the fork. Retarget to constructorfabric/studio:main once constructorfabric#108 merges.

Summary

Resolves two of constructorfabric#108's follow-up open questions (findings doc §13b/§13c).

  • infer_section_level(): picks which heading level represents one real retrievable section, using the level's frequency as the signal. A document's real recurring structure (its chapters) shows up as the level used most often; an occasional heading at an anomalous level — exactly what PDF-to-Markdown conversion produces, since it assigns levels by font-size heuristics, not semantic depth — is rare precisely because it's noise, not structure. Levels used only once are excluded as candidates outright.

    This is a direct, verified fix for a real failure found building feat(toc,doc-index): add JIT-retrieval readiness checks and cached doc index constructorfabric/studio#108: a real PDF-converted document put all 8 of its actual chapters on H5 and a single stray subsection on H3. Treating H3 as "the" section level (or any fixed level) turned the entire back half of the document into one fake 6,601-line "section". Re-run against that same document with this change: 12 correctly-sized real sections, not one.

  • retrieval_sections (new field on the built index): headings grouped at exactly the inferred level — off-level stray headings stay inside whichever section they geographically fall under, rather than splitting one apart — each with a SHA-256 hash of its own text.

  • diff_stale_sections(): compares a file's current content against its last cached build at this granularity and reports which sections actually changed, matched by position (not heading text — duplicate titles are real, see toc-heading-duplicate). This is the piece a future caller needs to re-summarize only what changed instead of the whole document — the whole-file etag from feat(toc,doc-index): add JIT-retrieval readiness checks and cached doc index constructorfabric/studio#108 can only say "something changed", not "what".

Existing sections/annotate_section_summary/etag behavior is untouched.

Test plan

  • pytest tests/test_doc_index.py tests/test_toc.py — 156 passed, 100% coverage on touched files
  • Full suite — 4815 passed; the 12 failures present are the same pre-existing macOS-local/flaky ones seen on feat(toc,doc-index): add JIT-retrieval readiness checks and cached doc index constructorfabric/studio#108, none in files touched here
  • pylint / vulture clean; cfs validate 0 errors; spec-coverage thresholds met
  • infer_section_level re-run for real against the actual PDF-converted document that originally exposed the bug (see commit message)

SanjeevSolanki and others added 2 commits August 31, 2026 09:33
…uctorfabric#105)

Token-overlap pre-filter surfaces weak links between a marked block and
the requirement it cites; an injected SemanticJudgeFn rules only on those
(covered/partial/wrong), advisory and never gating. Honest by construction:
unjudgeable-not-zero, an evidence guard on every quote, and full per-block
accounting (assessed + presumed_covered + unjudgeable). Library + tests;
report integration is the follow-up.

Signed-off-by: Sanjeev Solanki <sanjeev.solanki@constructor.tech>
Co-authored-by: Sanjeev Solanki <sanjeev.solanki@constructor.tech>
…ction is visible (constructorfabric#121)

* ci: run the enforcement gate corpus as its own check

The gates that reject an unbacked claim are exercised only in their passing
direction by the pipeline. `ci.yml` has ten jobs and none of them asserts that a
seeded violation is rejected, so a green run is evidence that the checks ran, not
that they can fail. The project's own finding applies to its CI as much as to the
commands: a gate that never fails is worthless, and nothing here demonstrates
otherwise.

The assertions already exist. `tests/test_enforcement_empty_codebase.py` and
`tests/test_enforcement_empty_scan.py` drive the real CLI and assert exit codes
in both directions -- a registered codebase holding one unmarked source file
exits 2 with `code-no-marker`, a correctly marked implementation exits 0, and a
repository that claims nothing stays green with no code at all. What they lack is
visibility: they run inside a suite of ~4,850 tests, so the red direction is
provable only by knowing the tests exist and reading them.

`make test-gates` runs those two files alone, and a `gates` job surfaces the
result as a named check. No new tests and no behaviour change -- this makes an
existing guarantee legible in the place people look for it.

Verified the job is load-bearing rather than decorative: restoring the
pre-existing early return in cross-validation
(`if not results.parsed_code_files_full:`) fails 5 of the 39, so the check
catches the regression it exists to catch. Green on `main` as it stands.

Deliberately scoped to the enforcement corpus. The semantic-coverage tests are
advisory and have an open interpreter-dependent failure of their own, so folding
them in would make this check red for a reason unrelated to enforcement.

Signed-off-by: ou <ou@constructor.tech>

* ci: drop the job token from the gates checkout

The gates job runs pull-request code (`make install`, `make test-gates`).
`actions/checkout@v4` defaults `persist-credentials` to true, which writes
the job token into `.git/config` where that code could read it back. Set
`persist-credentials: false` and pin the job to `contents: read`, matching
the least-privilege job-level `permissions` block already used in
code-ranker.yml.

Scoped to the job this PR adds. The nine pre-existing checkout steps share
the same default and are left for a separate decision.

Signed-off-by: ou <ou@constructor.tech>

---------

Signed-off-by: ou <ou@constructor.tech>
Co-authored-by: ou <ou@constructor.tech>
@tkcoding
tkcoding force-pushed the jit-retrieval-cascade branch 2 times, most recently from bd02778 to 2af77ae Compare September 1, 2026 03:50
@tkcoding
tkcoding force-pushed the jit-retrieval-doc-index branch from f877490 to 2a60188 Compare September 1, 2026 05:07
@tkcoding
tkcoding force-pushed the jit-retrieval-cascade branch 2 times, most recently from 30b0736 to 3493f4b Compare September 1, 2026 05:18
tkcoding and others added 6 commits September 1, 2026 09:43
…c index (constructorfabric#108)

Squash-merged after Deep Review: all 5 Major findings verified fixed (human-output warning visibility, cache shape validation, mtime+size etag documentation, duplicate-heading normalization, correct @Cpt-Flow traceability tag). 12 Minor findings remain as non-blocking follow-ups. CI green across all 23 checks.
…aleness

Neither of these existed before: doc_index.py indexed every heading at
every level, with no notion of "one retrievable section", and its
staleness check was whole-file only -- any edit anywhere invalidated the
entire cached index, making a real per-section partial rebuild impossible
regardless of how small the actual edit was.

infer_section_level() picks which heading level represents one real
section, using the level's *frequency* as the signal: a document's real
recurring structure (its chapters) shows up as the level used most often,
while an occasional heading at an anomalous level -- exactly what
PDF-to-Markdown conversion produces, since it assigns levels by font-size
heuristics, not semantic depth -- is rare precisely because it's noise,
not structure. Levels used only once are excluded as candidates outright.
This is a direct, verified fix for a real failure found earlier building
this feature: a real PDF-converted document put all 8 of its actual
chapters on H5 and a single stray subsection on H3; treating H3 as "the"
section level (or any fixed level) turned the entire back half of the
document into one fake 6,601-line "section". Re-run against that same
document with this change: 12 correctly-sized real sections, not one.

build_doc_index() now also computes retrieval_sections -- headings grouped
at exactly the inferred level (off-level stray headings stay inside
whichever section they geographically fall under, rather than splitting
one apart), each with a SHA-256 hash of its own text. diff_stale_sections()
compares a file's current content against its last cached build at this
granularity and reports which sections actually changed, matched by
position (not heading text -- duplicate titles are real, see
toc-heading-duplicate) -- the piece needed for a future caller to
re-summarize only what changed instead of the whole document.

Registered the three new instructions in traceability-validation.md;
whitelisted diff_stale_sections in vulture_whitelist.py alongside
annotate_section_summary (same "future caller, exercised by tests"
situation). New code is 100% covered; existing sections/annotate/etag
behavior is untouched and still passing.

See constructorfabric#104.

Verified: pytest (test_doc_index.py + test_toc.py: 156 passed; full suite:
4815 passed, the same 12 pre-existing macOS-local/flaky failures as on
main, none in the files touched here), pylint and vulture clean, cfs
validate 0 errors, spec-coverage thresholds met, and infer_section_level
re-run against the real PDF-converted document that originally exposed
the bug.

Signed-off-by: TECK KEAT WILSON <yeow.teck.keat@constructor.tech>
…abric#109

- cmd_doc_index() built its output from the index but omitted
  retrieval_sections/section_level in both JSON and human output -- the
  new data constructorfabric#109 added was invisible through the CLI. Both are exposed now,
  and the human formatter lists retrieval sections the same way it already
  lists the finer-grained ones.
- A write landing between read_text() and _compute_etag() in
  build_doc_index() could save headings parsed from the *old* content
  stamped with the *new* file's etag; load_doc_index() would then treat
  that stale index as valid until a later edit changed the etag again.
  _read_with_stable_etag() brackets the read with a stat snapshot on each
  side and retries on mismatch, so the saved etag is provably the one that
  matches what was actually parsed.
- diff_stale_sections() reported changed/unchanged sections by heading
  text alone; two sections sharing a duplicate title (a real,
  already-flagged possibility -- see toc-heading-duplicate) couldn't be
  told apart. Each entry now carries line_start alongside the heading
  text, which is what a caller should actually use to address "this
  specific section" afterwards.
- annotate_section_summary() updated only index["sections"], leaving the
  matching retrieval_sections entry at summary=None even on success -- a
  caller reading retrieval_sections (the more relevant list for a future
  per-section summarizer) couldn't see the annotation. Now updates both
  when both have an entry at line_start.
- toc.py's _frontmatter_has_description() accepted `description: # TODO`
  and `description: ""` as satisfying the check, since `#` and `"` both
  match \S. Now parses the field's actual value and rejects comments and
  empty/whitespace-only quoted strings.

Extracted _compute_fresh_retrieval_sections/_position_entry out of
diff_stale_sections() to stay under pylint's local-variable limit after
the line_start addition; registered the new instructions (stable-read,
diff-stale-helpers) in traceability-validation.md.

See constructorfabric#104.

Verified: pytest (test_doc_index.py + test_toc.py: 166 passed, 100%
coverage on touched doc_index files); full suite: 4825 passed, the same
12 pre-existing macOS-local/flaky failures as on constructorfabric#108/constructorfabric#109, none in files
touched here; pylint and vulture clean; cfs validate 0 errors;
spec-coverage thresholds met; infer_section_level/retrieval_sections
re-verified against the real PDF-converted document that originally
exposed the granularity bug -- still 12 correct sections.

Signed-off-by: TECK KEAT WILSON <yeow.teck.keat@constructor.tech>
…torfabric#109

- load_doc_index() returned a cached index whenever its etag matched,
  with no check that the cached shape matched what this version of the
  code expects. A cache written before section_level/retrieval_sections
  existed can still have a matching etag if the file hasn't changed since
  -- cmd_doc_index() would then hit a KeyError reading those fields on a
  legacy cache instead of a clean rebuild. Now treated the same as a
  stale cache: rebuilt, not returned as-is.
- _frontmatter_has_description() treated a YAML block-scalar marker
  (`description: |`, `description: >-`, ...) as a usable value, when the
  real content -- if any -- belongs on indented lines below it, not on
  the marker's own line. Now checks the first non-blank following line
  for real indentation before counting it as a description.

See constructorfabric#104.

Verified: pytest (test_doc_index.py + test_toc.py: 172 passed, 100%
coverage on touched doc_index files); full suite: 4831 passed, the same
12 pre-existing macOS-local/flaky failures as before, none in files
touched here; pylint and vulture clean; cfs validate 0 errors;
spec-coverage thresholds met; infer_section_level/retrieval_sections
re-verified against the real PDF-converted document -- still 12 correct
sections.

Signed-off-by: TECK KEAT WILSON <yeow.teck.keat@constructor.tech>
…on PR constructorfabric#109

- doc_index.py: documented the additive-only schema contract (new fields
  need no version bump; only an existing field's meaning/shape changing
  does); non-UTF-8/binary input read failures are now caught and either
  degrade gracefully (diff paths) or propagate as a typed, catchable
  UnicodeDecodeError (build paths); per-section hashing now strips
  trailing whitespace per line, so an editor's "trim trailing whitespace
  on save" no longer looks like a real content edit; three genuinely
  abnormal cache fallback paths (directory lookup failure, corrupt cache
  file, source file vanishing mid-check) are now logged at WARNING,
  visible at the CLI's default log level, while the one path with a
  documented, legitimately expected trigger (diffing against a since-
  deleted file) stays at debug; annotate_section_summary's read-modify-
  write cycle now runs under an exclusive file lock (mirroring
  decision_log.py's established fcntl pattern), closing a lost-update
  race between two concurrent calls annotating different sections of the
  same document.
- commands/doc_index.py: a non-UTF-8 file now reports a clean ERROR
  result instead of crashing with a raw traceback; --help now explains
  that section_level is inferred from heading frequency, not a fixed level.
- commands/validate_toc.py: a read failure on one file in a multi-file
  batch (permission denied, binary content, a TOCTOU race) is now
  recorded as that file's own ERROR result instead of raising out of the
  loop and discarding every result already collected for files validated
  earlier in the same invocation; extracted _validate_one_file() to keep
  cmd_validate_toc's own local-variable count under pylint's threshold.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: TECK KEAT WILSON <yeow.teck.keat@constructor.tech>
Whichever CLI test runs first in the full suite triggers cli.py's own
_configure_studio_logging(), which sets the "studio" logger's propagate
to False for the rest of the process -- a real ambient global-state
mutation that silently blocks pytest's caplog (listening on the root
logger) from seeing any "studio.*" child logger's records for every test
that runs afterward. The two new WARNING-level log assertions from the
previous commit passed in isolation but failed as part of the full suite
for exactly this reason. Added a studio_logger_propagates fixture that
temporarily restores propagation for the duration of a test, and used it
in both affected tests. Verified against the actual failure condition by
forcing propagate=False before running them.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: TECK KEAT WILSON <yeow.teck.keat@constructor.tech>
@tkcoding
tkcoding force-pushed the jit-retrieval-cascade branch from 3493f4b to dc3c8d3 Compare September 1, 2026 06:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants