Every handoff should include:
- Current status (
Complete,Partial, orBlocked) - Exact command evidence used for verification
- Files changed and rationale
- Known risks and recommended next actions
Agents: append a new block below using this template after every multi-step task.
History: Older completion blocks live in
docs/archive/handoff-history.md. When this file grows past roughly 200 lines of log content, archive older blocks there in the same change set.
Status: Complete Date: 2026-05-10 Agent/Session: Cursor agent (Claude Opus 4.7)
Implemented hybrid documentation versioning: Python API + Typer CLI reference are now built per-release-tag and surfaced through Starlight's <VersionPicker>, while operator docs, ADRs, getting-started, the landing page, and the embedded Quarto report remain evergreen (always reflect main). The default release is also aliased at the un-versioned URL so inbound links (/api/forensics_pipeline/, /cli/forensics-preflight/) keep resolving without redirects.
- New:
website/scripts/sync-versions.mjs— single source of truth for which tags ship. Reads.release-please-manifest.jsonfor the current version, cross-referencesgit tag --list 'v*.*.*', sorts by semver, keeps the most recent 5 (override viaKEEP_VERSIONS=Nor--keep N), marks the manifest version asdefault, and writesversions[]into both autodoc configs. Falls back to single-version (main) mode and removes staleversions[]if no tags exist (bootstrap-safe). - New:
website/scripts/build-cli-docs.mjs— per-version CLI orchestrator that mirrorsbuild-python-docs.mjs. For each tag:git worktree add --detach→uv sync --frozen --extra dev→uv run --directory <wt> python <REPO_ROOT>/scripts/generate_cli_docs.py --out <outDir>/<safeTag> --version <tag> [--version-default]. Worktrees cleaned on exit/SIGINT. The default version is re-rendered with--version-segment ""and copied (top-level.mdonly — never wipes subdirs) into the un-versioned output dir to alias the default URL. - New:
website/scripts/cli-autodoc.json— mirror ofpython-autodoc.jsonfor the CLI orchestrator. SpecifiesoutputDir,urlBasePrefix,repoUrl,repoBranch,generatorScript, anduvExtras: ["dev"].versions[]populated dynamically bysync-versions.mjs. - Modified:
scripts/generate_cli_docs.py— added--version,--version-label,--version-default, and--version-segmentflags. Writesversion:,versionLabel:,versionDefault: truefrontmatter andsidebar.hidden: trueon versioned-subdir pages (default version's un-versioned alias stays sidebar-visible). Rewrites cross-page links (subcommand tables,cli/index.md) to honorversion_segment, and pointseditUrlat the version tag rather thanmainso "Edit this page" goes to the released code. Newsafe_tag()helper mirrorssafeTag()from the JS orchestrator. Backward compatible: no--versionflag → unchanged behavior. - Modified:
website/scripts/build-python-docs.mjs— addedsidebar.hidden: trueto versioned-subdir page frontmatter (without it the sidebar would show every module N+1 times). Fixed the "Older version" banner link target: now correctly prependscfg.urlBasePrefixand points at the un-versioned default-aliased URL (/mediaite-ghostink/api/forensics_pipeline/) instead of the versioned subdir, satisfyingstarlight-links-validatorand giving readers the latest canonical page. - Modified:
website/package.json— addedsync-versionsanddocs:cliscripts. Existingdevandbuildunchanged sobun run devkeeps Just Working in single-version repos. - New:
website/src/components/SocialIcons.astro— top-level project override of Starlight'sSocialIconsslot that renders TWO<VersionPicker>instances (one for/api, one for/cli) plus the default theme social icons. Required because the theme's existingSocialIconsoverride ships only ONE picker bound to a singleapiBase. Each picker is render-gated by its own URL prefix so at most one is visible on any given page. - Modified:
website/astro.config.mjs— wired the newSocialIconsoverride at the top level (components: { SocialIcons: './src/components/SocialIcons.astro' }) so it takes precedence over the theme's plugin-level override. - Modified:
Makefile— addeddocs-versionsphony target (bun run sync-versions);docs-clianddocs-pythonnow depend ondocs-versionsand shell out to the new orchestrators;docs-quartodescription updated to note evergreen status;docs-builddescription updated to note versioned CLI + Python API + evergreen report. - Modified:
.github/workflows/deploy-docs.yml— addedfetch-depth: 0to the checkout step (REQUIRED — shallow clones can't materialize tag commits forgit worktree), insertedbun run sync-versionsafterbun install, replaced the single CLI generator step withbun run docs:cli(which now orchestrates per-tag worktrees), and extended the smoke test to verify all evergreen paths, all default-aliased paths, AND per-version subdir paths (resolves currentsafeTagfrom the release-please manifest so the assertion tracks releases without manual edits). - Modified:
docs/RUNBOOK.md— expanded the "CI / hosting cutover" section to 11 explicit build steps including the worktree-based versioning machinery, plus a new "Versioned documentation (Option C)" subsection covering how versions are resolved, how per-version pages get built, the frontmatter contract (version:,versionLabel:,versionDefault:,sidebar.hidden:), and a diagnosis quick-reference table for the common failure modes.
website/scripts/sync-versions.mjs(new)website/scripts/build-cli-docs.mjs(new)website/scripts/cli-autodoc.json(new)website/src/components/SocialIcons.astro(new)scripts/generate_cli_docs.pywebsite/scripts/build-python-docs.mjswebsite/package.jsonwebsite/astro.config.mjsMakefile.github/workflows/deploy-docs.ymldocs/RUNBOOK.md
$ make docs-clean && make docs-build
... (157 page(s) built in 5.94s) ...
03:46:46 ✓ Completed.
03:46:46 [@astrojs/check] Getting diagnostics for Astro files in /Users/.../website...
Result (157 files):
- 0 errors
- 0 warnings
- 0 hints
03:46:54 [check] All internal links are valid. ← starlight-links-validator
03:46:54 [build] Complete!
# Smoke test (mirrors deploy-docs.yml exactly)
$ CURRENT_SAFE_TAG=$(node -e 'const m=require("../.release-please-manifest.json"); process.stdout.write(m["."].replace(/[^a-zA-Z0-9_-]/g,"-"));')
$ echo "default tag = $CURRENT_SAFE_TAG"
default tag = 0-1-2
$ for p in dist/index.html dist/getting-started/index.html \
dist/cli/index.html dist/cli/forensics/index.html \
dist/cli/forensics-preflight/index.html \
dist/synced/architecture/index.html dist/synced/runbook/index.html \
dist/adr/index.html dist/api/forensics/index.html \
dist/api/forensics_pipeline/index.html dist/report/index.html \
dist/sitemap-index.xml \
"dist/cli/${CURRENT_SAFE_TAG}" \
"dist/cli/${CURRENT_SAFE_TAG}/forensics-preflight/index.html" \
"dist/api/${CURRENT_SAFE_TAG}" \
"dist/api/${CURRENT_SAFE_TAG}/forensics_pipeline/index.html"; do
[ -e "$p" ] && echo "OK $p" || echo "MISS $p"
done
... (all 16 paths OK) ...
missing = 0
# Sample versioned page frontmatter
$ head -10 website/src/content/docs/api/0-1-2/forensics_pipeline.md
---
title: forensics.pipeline
description: "End-to-end pipeline orchestration (scrape → extract → analyze → report)."
version: "v0.1.2"
versionLabel: "0.1.2"
versionDefault: true
sidebar:
hidden: true
---
# Lint
$ uv run ruff check scripts/generate_cli_docs.py && uv run ruff format --check scripts/generate_cli_docs.py
All checks passed!
1 file already formatted
bun run sync-versions resolved 3 tags (v0.1.2 default, v0.1.1, v0.1.0). Total make docs-build wall time ~6 min on a warm uv cache (per-tag uv sync --frozen dominates; subsequent runs are faster).
- Default version aliased at un-versioned URL, not redirected. Re-rendering the default version with
--version-segment ""(CLI) / a secondversion: nullbuild (API) into the bare output dir keeps the URL shape stable for inbound links. The alternative — a redirect from/api/<page>/to/api/<safeTag>/<page>/— would have polluted analytics and brokenstarlight-links-validator's pages set. - Keep last 5 versions by default. Configurable via
KEEP_VERSIONS=Nor--keep N. Per-tag worktree +uv syncis the slowest part of the build; 5 is the point where wall time stays under 10 min on a warm cache while still covering enough history to be useful. - Tag-list as source of truth, manifest only for default selection. If
release-please-manifest.jsondeclares a version that isn't tagged locally yet (common in CI right after a release-please commit lands but before tag fetch), the orchestrator falls back to the newest tag and logs a warning instead of failing. - One orchestrator per docs surface, not a shared generic. The Python and CLI generators have different shapes (
pydoc-markdownvs. a custom Typer walker), different per-tag dependency requirements, and different default-alias rules. A shared orchestrator would have needed enough switches that it'd be harder to read than two near-identical scripts. - Versioned pages are
sidebar.hidden: true, reachable only via the picker. Otherwise the sidebar shows every module/command N+1 times (once per version + the default alias). The default alias remains sidebar-visible — it's the canonical "latest" entry. - Failed
uv sync --frozenfor old tags downgrades to a warning, not a hard fail. Older lockfiles can resolve to wheels yanked from PyPI; in that case the orchestrator logs and skips, so a single dead historical version doesn't tank the whole build. The default version is always in the kept window so the latest never has this risk.
- Should we surface a banner on aliased default pages saying "you're viewing v0.1.2 / latest"? Currently the alias copies omit version frontmatter entirely (so they don't trip the "stale version" banner). A future enhancement could re-add a soft "viewing latest (v0.1.2)" banner to set expectations, but that's a UX call, not a correctness gap.
- Quarto report versioning is deliberately out of scope. It's evergreen by design — the investigation report describes a state of work, not a software release. If we later need pinned-historical reports, the manifest-driven pattern here would extend naturally with
--out website/public/report/<safeTag>plus a banner.
- Risk: CI worktree-add fails on first run. Mitigation:
fetch-depth: 0is now pinned in the deploy workflow. Anyone copying that workflow to another repo MUST copy that line — shallow clones breakgit worktree add <tmp> <tag>silently from the user's perspective (the orchestrator catches it and logs). - Risk: a future
release-pleaseconfig change that dropsinclude-v-in-tag: true. The orchestrator'sgit tag --list 'v*.*.*'filter is the contract; if tags become0.1.3instead ofv0.1.3, sync-versions will silently see zero matches and fall back to single-version mode. Belt-and-braces: pininclude-v-in-tag: trueinrelease-please-config.jsonand don't change it. - Next step: wait for the next release tag to land, watch one CI run end-to-end to confirm the per-tag worktree path resolves on GitHub Actions runners (different
uvcache state, differentgitconfig). If it works, no follow-up. Ifuv sync --frozenfails inside a worktree because of a wheel-cache layout, the fix isuv sync --frozen --refreshinside the orchestrator — but don't preemptively make that change; it doubles per-tag build time. - Operational note: to widen the kept-versions window from 5 to N, set
KEEP_VERSIONS=Nin the workflow env, or add--keep Ntobun run sync-versionsindocs-versionstarget. No code changes needed.
Status: Complete Date: 2026-05-10 Agent/Session: Cursor agent (Claude Opus 4.7)
.github/workflows/deploy-docs.ymlrewritten to matchmake docs-buildend-to-end:- Added
pipx install pydoc-markdownstep so the Python API reference is generated in CI (previously the workflow skippedbun run docs:python, which left/api/*empty on production). - Added explicit
Generate Python API referencestep (bun run docs:python) betweenbun install --frozen-lockfileandbun run build, matching the local Makefile order. - Added a post-build smoke-test step that hard-asserts the canonical entry points exist in
website/dist/(/,/getting-started/,/cli/,/cli/forensics/,/cli/forensics-preflight/,/synced/architecture/,/synced/runbook/,/adr/,/api/forensics/,/api/forensics_pipeline/,/report/,sitemap-index.xml). A regression in any generator (sync-docs, CLI generator, pydoc-markdown, Quarto) now hard-fails the workflow before Pages sees it. - Aligned with the repo's existing CI pattern:
astral-sh/setup-uv@v4+enable-cache: true+uv sync --frozen --extra dev(was@v5without freeze). - Broadened path filters:
src/forensics/**(not justcli/), pluspyproject.tomlanduv.lock(so dependency changes that affect generated reference docs re-deploy). - Allowed
workflow_dispatchfrommainto also publish (previously dispatch built but never deployed). - Split concurrency: build job uses
deploy-docs-${{ github.ref }}withcancel-in-progressonly for PRs; deploy job pins the shared GitHub-recommendedpagesgroup withcancel-in-progress: falseso a live deploy is never interrupted mid-flight.
- Added
website/scripts/python-autodoc.json: fixedsearchPathfrom../../src→../src(the script resolves relative towebsite/, so the prior value pointed at/Users/.../PyCharmProjects/src, outside the repo). AddedurlBasePrefix: "/mediaite-ghostink"so the build-python-docs script can emit base-prefixed Starlight URLs.website/scripts/build-python-docs.mjs: the auto-generated## Submodulessection on package landing pages previously emitted relative./<safeName>.mdlinks, whichstarlight-links-validatorrejects (defaulterrorOnRelativeLinks: true). Patched to emit absolute base-prefixed Starlight URLs (/mediaite-ghostink/api/<safeName>/), honoringcfg.urlBasePrefixand version directories when versions are configured.Makefile:docs-pythonnow precheck-fails with an actionable message (Install with: pipx install pydoc-markdown) whenpydoc-markdownis not onPATH, matching the CI dependency exactly.docs/RUNBOOK.md: documented the 9-step CI build pipeline (uv sync → pydoc-markdown → Quarto setup → Bun setup → CLI gen → Quarto render → bun install → Python API → astro build + smoke-test), the broadened path triggers, and the concurrency split.
.github/workflows/deploy-docs.ymlMakefilewebsite/scripts/build-python-docs.mjswebsite/scripts/python-autodoc.jsondocs/RUNBOOK.md
# Cold rebuild mirroring CI exactly
make docs-clean && make docs-build
# → [build] 58 page(s) built in 8.08s
# → ✓ All internal links are valid.
# Workflow smoke-test block (12/12 OK)
for f in website/dist/index.html website/dist/getting-started/index.html \
website/dist/cli/index.html website/dist/cli/forensics/index.html \
website/dist/cli/forensics-preflight/index.html \
website/dist/synced/architecture/index.html \
website/dist/synced/runbook/index.html website/dist/adr/index.html \
website/dist/api/forensics/index.html \
website/dist/api/forensics_pipeline/index.html \
website/dist/report/index.html website/dist/sitemap-index.xml; do
test -f "$f" && echo "OK $f" || echo "MISS $f"
done
# → All 12 entries OK
# Python lint/format on the only Python file in the CI generator path
uv run ruff check scripts/generate_cli_docs.py # All checks passed!
uv run ruff format --check scripts/generate_cli_docs.py # 1 file already formatted
# Workflow YAML parses
uv run python -c "import yaml; yaml.safe_load(open('.github/workflows/deploy-docs.yml'))"
# → YAML OK
- Install
pydoc-markdownviapipxrather thanuv pip installbecause (a) it keeps the docs toolchain out of the project venv, (b)pipxis preinstalled onubuntu-latest, and (c) it matches the upstream Abstract Data docs theme's recommended install path (see the script's own banner). - Embed an explicit smoke-test (12 path checks) rather than relying solely on
astro buildexit code. The Astro build can succeed even if a generator silently produced zero pages (e.g. pydoc-markdown returns 1 per module but the script logs and continues). The smoke-test catches that class of regression deterministically. - Broaden trigger paths to
src/forensics/**(not justcli/) because pydoc-markdown introspects every documented module; changing any function signature undersrc/forensics/may need a fresh deploy to keep the API reference accurate. - Did not refactor the rest of the repo's CI; quality/test workflows (
ci.yml,ci-tests.yml,ci-quality.yml,ci-report.yml,agents-governance.yml,release-please.yml) are unrelated to docs deployment and already ignore docs-only paths viapaths-ignore: ['**.md', 'docs/**', 'LICENSE']. Leaving them untouched per scope.
- Maintainer follow-ups for the Cloudflare → GitHub Pages migration (carried over from the prior docs-site handoff and still pending):
Settings → Pages → Source: GitHub Actionson the GitHub repo.- Watch the first
Deploy Docsrun onmain, confirm the site renders athttps://abstract-data.github.io/mediaite-ghostink/. - Retire the Cloudflare
ai-writing-forensicsPages project and removeCF_API_TOKEN/CF_ACCOUNT_IDrepo secrets. - Retire the legacy
make deploytarget (still referenceswrangler pages deploy) in a follow-up change once the CF project is gone.
bun.lockis committed (workspaces.[\"\"].name = \"website\", not the renamedmediaite-ghostink-website). Bun's--frozen-lockfilechecks dependency tree integrity, not the workspace name, so this is fine — but if you ever regenerate the lockfile cleanly the workspace name will update and that should be a non-issue.- First production deploy will be the first time pydoc-markdown runs against the live
src/forensics/tree on a GitHub runner. If a module raises at import (e.g. spaCy model not available), the CLI step would still pass but the Python API step would fail loudly. The smoke-test would then catch any drop in expected pages.
Status: Complete
Date: 2026-04-29
Agent/Session: Cursor agent
- REST parser:
extract_article_text_from_restnow runs_strip_stray_angle_bracketsafter sanitize so malformed fragments (e.g. a lone<) do not survive in plain text; satisfiestest_parser_rest_fuzzidempotence + no-</>invariants. refresh.py: Comment on__all__for patch targets; split_run_isolated_author_parallel_jobs; refactoredrun_parallel_author_refreshinto helpers with docstring; parallel analyze path seeds audit /run_metadatalike serial paths.analyze_dispatch.py:_conflicting_analyze_flags+ validation in_compare_only_or_parallel_early_exitfor invalid flag mixes (--comparewith other stages;--parallel-authorswith serial stages).analyze_models.py: Docstring onAnalyzeContext.build.config_cmd.py: JSON mode emits{"status":"ok","diffs":[...],"count":N}when overrides exist (no prose lines).settings.py:mode="before"mirror: whensurveyhasexcluded_sectionsandfeaturesis missing, injectfeaturesfrom survey.per_author.py: Polars LazyFrame pipeline for_clean_feature_series(ruff-formatted).test_parser_rest_fuzz.py: Stronger assertions + sentinel-preservation Hypothesis test.
src/forensics/scraper/parser.py,tests/unit/test_parser_rest_fuzz.pysrc/forensics/analysis/orchestrator/refresh.py,per_author.pysrc/forensics/cli/analyze_dispatch.py,analyze_models.py,config_cmd.pysrc/forensics/config/settings.py
uv run ruff check . && uv run ruff format --check .
uv run pytest tests/ -v --tb=line -q
1095 passed, 4 skipped (TUI), 1 xfailed; coverage 80.44% (meets fail-under).
- Stripping
</>in the REST path only removes markup-like characters from flattened text; legitimate angle brackets in article prose are rare for this corpus. If needed, narrow to known-malformed patterns only.
Status: Complete
Date: 2026-04-29
Agent/Session: Cursor agent
- RF-DRY-001:
CONFIG_HASH_EXTRAinsrc/forensics/config/constants.py; replaced repeatedjson_schema_extradicts inanalysis_settings.pyandsettings.py. - RF-DRY-002 / P3-SEC-001:
worker_errors.pywith recoverable-exception policy;parallel.py/comparison.pyupdated;RuntimeErrorincluded so isolated-refresh tests still pass. - RF-DRY-003:
survey.excluded_sectionscanonical;mode="before"validator onForensicsSettingsmirrors intofeatures(dict and model init). - P2-ARCH-001:
--verify-raw-archives/--no-verify-raw-archives,--log-all-generations/--no-log-all-generationson analyze; wired viaAnalyzeCustodyParams. - P3-SEC-002:
_METADATA_INGEST_RECOVERABLEnarrowed toIntegrityError+OperationalError(not allsqlite3.Error). - P3-TEST-001:
tests/unit/test_parser_rest_fuzz.pyforextract_article_text_from_rest. - P3-PERF-001: Polars-native
_clean_feature_seriesinper_author.py. - RF-COMPLEXITY-001:
analyze_models.py,analyze_dispatch.py,analyze_section.py; trimmedanalyze.py. - RF-COMPLEXITY-002:
parallel_shared.py,refresh.py, slimparallel.py+ lazyimport_modulere-exports for patch surface. - RF-SMELL-002: Nested
AnalyzeRequest(stages,baseline,custody);pipeline.pyand tests updated. - P2-CQ-002:
forensics analyze runandforensics analyze compare-onlysubcommands. - P2-CQ-001 / RF-SMELL-003:
forensics config audit; flat[analysis]deprecation warning incompat_analysis.py(once per process); ADR-017 note. - RF-SMELL-001:
ForensicsSettingsshortcutspelt,bocpd,convergence,content_lda,hypothesis,embedding. - RF-ARCH-001: Docstrings in
runner.pyandanalyze_dispatch.pycross-referencing CLI vsrun_full_analysis.
src/forensics/config/(constants.py,settings.py,compat_analysis.py,analysis_settings.py)src/forensics/cli/(analyze.py,analyze_models.py,analyze_dispatch.py,analyze_section.py,analyze_options.py,config_cmd.py,__init__.py)src/forensics/analysis/orchestrator/(parallel.py,parallel_shared.py,refresh.py,worker_errors.py,per_author.py,comparison.py,runner.py)src/forensics/scraper/crawler.py,tests/unit/test_parser_rest_fuzz.py,tests/unit/test_config_audit.py,tests/unit/test_analyze_compare.py,tests/test_preregistration.py,tests/unit/test_settings.py,scripts/merge_embedding_manifest_shards.py(E501 wrap)docs/RUNBOOK.md,docs/adr/017-analysis-config-change-control.md,src/forensics/pipeline.py
uv run ruff check . && uv run ruff format --check .
uv run pytest tests/ -q --no-covAll tests passed (4 skipped TUI, 1 xfail section-residualize).
- Parallel refresh tests must patch
forensics.analysis.orchestrator.refresh._validate_and_promote_isolated_outputs(implementation lives inrefresh.py). - ForensicsSettings excluded-sections mirroring uses
mode="before"so pydantic-settings applies the merged dict correctly. - Consider migrating internal code to
settings.hypothesisetc. incrementally; accessors are additive only.
Status: Complete
Date: 2026-04-27
- TASK-6: Expanded
forensics.analysis.orchestratormodule docstring with maintainer rules for_PATCH_TARGETSvs__all__; addedtest_patch_targets_subset_of_all_exports,test_patch_surface_tests_track_patch_targets,test_patch_targets_modules_nonemptyintests/unit/test_orchestrator_patch_surface.py. - TASK-7: Added ADR-017 (AnalysisConfig change control; no
HashableField— governance + compat split); moved flat TOML lift /_FLAT_TO_GROUP/_GROUP_ATTRSintosrc/forensics/config/compat_analysis.pywithanalysis_settingsimporting from it; amended ADR-016 references. - TASK-8: Factored
_run_repo_per_author_pipeline_with_artifactsinparallel.pyfor_per_author_workerand_isolated_author_worker(isolated path keepsemit_success_log=Falsefor log parity); added rootcoverage-tui.tomland RUNBOOK § item 7 forpytest --cov-config=coverage-tui.tomlwhen TUI extra is installed.
src/forensics/analysis/orchestrator/__init__.py,parallel.pysrc/forensics/config/compat_analysis.py(new),analysis_settings.pydocs/adr/017-analysis-config-change-control.md(new),docs/adr/016-analysis-config-nesting.md,docs/RUNBOOK.mdtests/unit/test_orchestrator_patch_surface.py,coverage-tui.toml(new),HANDOFF.md
uv run ruff check . && uv run ruff format --check .
uv run pytest tests/ -q
uv run pytest tests/integration/test_parallel_parity.py -v --no-cov -q
uv run pytest tests/unit/test_config_hash.py -v --no-cov -qFull suite: passed (1 known xfail); parallel parity: 3 passed. GitNexus MCP server not available in this Cursor session; run upstream impact on edited symbols before merge when enabled.
- None for this slice.
Status: Complete
Date: 2026-04-27
Agent/Session: Cursor agent (plan slice: doc-hash-migration, integration-hash-test, direction-concordance-filter, comparison-embedding-propagate, probability-trajectory-verify)
- RUNBOOK: Documented why
pipeline_b_modeparticipates inconfig_hash, symptoms (Analysis artifact compatibility failed/ stale hashes), and remediation (fullforensics analyzecohort vspipeline_b_mode = "legacy"to match old artifacts). - Integration:
tests/integration/test_analysis_config_hash_gate.pyassertsvalidate_analysis_result_config_hashesreturns failure and_validate_compare_artifact_hashesraisesValueErroron mismatched*_result.jsonconfig_hash. - Direction concordance:
classify_direction_concordancefilters hypothesis rows toconvergence_window.features_convergingwhen that list is non-empty; unit tests updated + new scoping test; Phase 17 golden fixtures already align feature names withfeatures_converging(no golden JSON edit). - Compare path: Introduced
orchestrator/embedding_policy.pywithembedding_fail_should_propagate(avoidsparallel↔comparisonimport cycle);_iter_compare_targetsre-raises embedding drift/revision errors in confirmatory mode;parallel.pyuses the shared helper; unit tests intest_comparison_target_controls.py. - Pipeline C: Comment on equal-weight monthly means and sparse Binoculars months in
probability_trajectories.py; unit testtest_sparse_binoculars_months_pipeline_c_score_finitefor length inequality + finite score.
docs/RUNBOOK.md—pipeline_b_mode/config_hashoperator subsectionHANDOFF.md— this blocksrc/forensics/analysis/orchestrator/embedding_policy.py— new shared policysrc/forensics/analysis/orchestrator/parallel.py,comparison.py— embedding propagationsrc/forensics/models/report.py— concordance scopingsrc/forensics/analysis/probability_trajectories.py— commentstests/integration/test_analysis_config_hash_gate.py— newtests/unit/test_direction_concordance.py,test_comparison_target_controls.py,test_probability_trajectories.py— tests
uv run ruff check . && uv run ruff format --check .
uv run pytest tests/integration/test_analysis_config_hash_gate.py tests/unit/test_direction_concordance.py tests/unit/test_comparison_target_controls.py tests/unit/test_probability_trajectories.py tests/integration/test_phase17_classification.py -v --no-cov- Embedding policy module:
comparison.pycannot importparallel.py(existingparallel→comparisonedge); policy lives inembedding_policy.pyinstead of duplicating logic.
- None.
- GitNexus:
gitnexus_impact/gitnexus_detect_changeswere not run (GitNexus MCP server not available in this Cursor session). Run onclassify_direction_concordance,_iter_compare_targets, andembedding_fail_should_propagatebefore merge when enabled. - Operators with pre–percentile-default
*_result.jsonshould follow the new RUNBOOK subsection before compare/report.
Status: Complete Date: 2026-04-28 Agent/Session: Claude Code (Opus 4.7, 1M context)
- Re-ran the full forensic pipeline end-to-end for all 12 configured authors and produced a confirmatory-mode PDF report.
- Diagnosed and patched a manifest-stomping bug in
forensics extract --author <slug>that would have silently destroyed the manifest under any multi-call workflow. - Added per-author manifest shards plus a merge step; verified shards accumulate correctly and the canonical manifest is rebuilt before
forensics analyze. - Recovered from a bash
set -egotcha (loop body inside&&chain) that left an ostensibly-killed extract loop racing a replacement chain. - Captured both failure patterns as Signs in
docs/GUARDRAILS.md.
src/forensics/features/pipeline.py— write per-author manifest shards (<slug>_manifest.jsonl) whenauthor_slugis set; legacy single-write path retained for unscoped runs.scripts/merge_embedding_manifest_shards.py— new helper; merges shards + canonical (last-wins byarticle_id), atomically rewrites canonical manifest, deletes shards.docs/GUARDRAILS.md— appended two Signs (set -efor-loop/&&interaction; pre-patch manifest-stomping behavior ofextract --author).data/reports/Mediaite-writing-analysis-—-technical-report.pdf— final confirmatory PDF (90 KB, mtime 2026-04-28 01:33:03 CDT).data/analysis/run_metadata.json,comparison_report.json, all per-author*_result.json/*_changepoints.json/*_convergence.json/*_hypothesis_tests.json/*_drift.jsonetc. — fresh artifacts.data/embeddings/manifest.jsonl— 49,126 rows / 12 distinctauthor_ids (canonical, post-merge).data/embeddings_archive_20260428T002003Z/— auto-archived prior embeddings (revision-pin mismatch).data/embeddings/manifest.jsonl.pre-revrepin-20260427T191951— operator-side manifest snapshot (pre-rerun rollback breadcrumb; safe to delete now).data/logs/path_b_parallel_20260427T220619.log— main run log; per-author logs atdata/logs/extract_<slug>.log.
$ tail -2 data/logs/path_b_parallel_20260427T220619.log
Tue Apr 28 01:33:03 CDT 2026
EXIT=0
$ stat -f '%Sm %z %N' data/reports/*.pdf
Apr 28 01:33:03 2026 92234 data/reports/Mediaite-writing-analysis-—-technical-report.pdf
$ uv run python -c 'import json; ids=set(); n=0
> for line in open("data/embeddings/manifest.jsonl"):
> line=line.strip()
> if line: ids.add(json.loads(line)["author_id"]); n+=1
> print(f"rows={n} authors={len(ids)}")'
rows=49126 authors=12
$ jq '{exploratory, allow_pre_phase16_embeddings, preregistration_status, config_hash}' \
data/analysis/run_metadata.json
{
"exploratory": false,
"allow_pre_phase16_embeddings": false,
"preregistration_status": "ok",
"config_hash": "6bffd326f0074688514c3d595ad2bc6065725ea17e783489"
}
Total wallclock: 22:06:19 → 01:33:03 = 3 h 26 min 44 s (4-way parallel extract via xargs -P 4 + merge + forensics analyze --max-workers 4 + Quarto/lualatex PDF render, 3 passes).
- Path B over A. Delivered confirmatory-mode report (
exploratory=false) instead of taking the faster A-mode shortcut, because the report is preregistration-clean and matches the locked thresholds at 2026-04-26T09:46:47. - Per-author manifest shards over fcntl locking. Cleaner — each writer owns its own path; the merge step is a single-process operation. Reader (
read_embeddings_manifest) needed no changes because last-wins-by-article_idsemantics already match. - Killed alex-griffing's 95-min run mid-extract to land the patch and restart with parallelism. Net savings on remaining 11 authors outweighed the loss.
- 4-way parallel chosen as the sweet spot for an M1 Max (10 cores, 64 GB, single MPS GPU). Measured ~36% wall slowdown per author vs serial baseline (much better than my 30% MPS-contention worst case); effective ~3× speedup.
- Skipped re-extracting
ahmad-austin— its rows from the (failed) earlier sequential run were already in the canonical manifest and itsbatch.npzwas on disk. The merge step preserved them.
- Section-residualized sensitivity flags (in
run_metadata.json): 4 authors havedowngrade_recommended=true(ahmad-austin,colby-hall,isaac-schorr,sarah-rumpf). Their primary findings drop substantially when section composition is residualized, suggesting section bias inflates the raw signal. Worth scrutinizing in the narrative before client delivery. - CLI behavior cleanup (deferred):
forensics extract(no--authorflag) iterates all 505 DB authors vialist_articles_for_extraction(author_id=None), whileresolve_author_rows(author_slug=None)returns only the 12 configured authors. The two are inconsistent.forensics analyzealready scopes to configured authors when no--authoris given. Worth a separate PR to alignextract's default scope withanalyze's — would let a singleforensics extractcall replace the 12 sequential--authorinvocations. - Manifest-shard cleanup is destructive.
merge_embedding_manifest_shards.pydeletes shards after merge. If a multi-author run is interrupted between extract and merge, partial state requires manual cleanup. Consider keeping shards underdata/embeddings/_shards/with a configurable retention policy.
pipeline.pypatch needs a unit test. Currently no test asserts that scoped extract writes shards rather than the canonical manifest. Suggested: add totests/test_parquet_embeddings_duckdb.pyor a newtests/unit/test_extract_manifest_shards.pycovering (a) shard-only write whenauthor_slugis set, (b) canonical write whenauthor_slugisNone(legacy path), (c) merge script idempotency on no-shards.- GitNexus impact analysis was not run on the patched
extract_all_featuressymbol or the new merge script (per the project's GitNexus rule). Run before any subsequent edits topipeline.py. - GUARDRAILS Sign: confirm
set -eclaim. I asserted the bash POSIX/bash-specific behavior from observed evidence (the OLD chain continued past a killed inner command). The exact set-of-conditions where bash suppressesset -einside compound commands is more nuanced than the Sign captures — if a future operator encounters edge cases, the canonical reference isbash(1)and POSIX 2.14.1.4. Keep the Sign as a heuristic. data/embeddings_archive_20260428T002003Z/anddata/embeddings/manifest.jsonl.pre-revrepin-20260427T191951can be deleted once the new artifacts are blessed. Combined ~hundreds of MB.- Operator hint: If you ever re-run extract for a subset of authors after this point, use
forensics extract --author <slug>(the patch makes that safe) followed byuv run python scripts/merge_embedding_manifest_shards.pybeforeforensics analyze. Or, for a full corpus, runforensics extract(no flag) — that path still uses the legacy single-write semantics, which are correct when there's only one writer for the whole manifest.
Status: Complete
Date: 2026-04-28
Agent/Session: Cursor agent
- Archived historical
HANDOFF.mdcompletion log todocs/archive/handoff-history.md; trimmed rootHANDOFF.mdto protocol + last three blocks + pointer; documented size discipline inAGENTS.md. - README: At a glance, Five-minute smoke test,
_quarto.ymlcorrections, repository layout row, Contributing section; documentation table entries forCONTRIBUTING.md,SECURITY.md,LICENSE. docs/ARCHITECTURE.mdanddocs/RUNBOOK.md:_quarto.ymlwording; E2E bullet uses_quarto.yml.- Moved root
TASK.mdtodocs/TASK.md; updateddocs/adr/ADR-003-agent-governance-and-hooks.md; removed redundant!TASK.mdfrom_quarto.yml. - Added
LICENSE(MIT),CONTRIBUTING.md,SECURITY.md;pyproject.tomllicense+RepositoryURL. - Moved
docs/coverage-tui.tomlfrom repo root; updated RUNBOOKpytest --cov-configpath. - Removed local ephemeral files where present (
coverage.json,.coverage,_freeze/,.DS_Store).
HANDOFF.md,docs/archive/handoff-history.md(new),AGENTS.md,README.md,docs/ARCHITECTURE.md,docs/RUNBOOK.md,docs/TASK.md(moved from root),docs/adr/ADR-003-agent-governance-and-hooks.md,_quarto.yml,pyproject.toml,LICENSE,CONTRIBUTING.md,SECURITY.md,docs/coverage-tui.toml(moved from root)
uv build
# sdist + wheel OK
uv run ruff check . && uv run ruff format --check .
uv run pytest tests/test_report.py -v --no-cov- License: Root
LICENSEis MIT with Abstract Data LLC copyright. If the org requires a different license, replace the file andpyproject.tomllicensemetadata in one commit. - GitNexus
impact/detect_changesnot run (no MCP server in this session); run before merge if your workflow requires it.
Status: Complete
Date: 2026-04-29
Agent/Session: Cursor subagent
- Makefile: Peer-oriented targets (
install-reviewer,install-baseline,install-probability,install-all-extras,peer-verify,peer-verify-network,peer-setup,peer-hints);peer-setupruns install + validate +uv run forensics peer-setup. - CLI:
src/forensics/cli/peer_setup.pywithpeer_setup_cmd(--check-ollama→asyncio.run(preflight_check(...)), PASS/FAIL),attach_peer_setup(app)registered fromforensics.cli.__init__. - Tests:
tests/unit/test_cli_peer_setup.pyassertsollama pulllines for configured[baseline] models. - Docs: README (five-minute smoke + local setup:
make peer-setup, spaCy wheel note),docs/RUNBOOK.md§ Peer reviewers.
Makefile,src/forensics/cli/peer_setup.py,src/forensics/cli/__init__.py,tests/unit/test_cli_peer_setup.py,README.md,docs/RUNBOOK.md,HANDOFF.md- Lint-only fixes encountered during
ruff check .:scripts/merge_embedding_manifest_shards.py,tests/unit/test_embedding_manifest_routing.py
uv run ruff check . && uv run ruff format .
uv run pytest tests/ -v --no-cov
Full suite: 1099 passed, 3 deselected, 1 xfailed (known test_section_residualize_suppresses_section_mix_only_change_point), ~110s.
- Baseline / Ollama hints print when
settings.baseline.modelsis non-empty (defaults include three tags). peer-setupMakefile recipe invokesuv run forensics peer-setupafter deps + validate;peer-hintsremains a hints-only target.
- None.
- GitNexus MCP
gitnexus_impactnot invoked in this session; new symbols are primary (peer_setup_cmd,attach_peer_setup);__init__.pyonly gained import +attach_peer_setup(app)call (duplicate inlineapp.commandremoved). Run impact/detect_changes before merge if required by team workflow.
Status: Complete
Date: 2026-04-29
Agent/Session: Cursor agent
- Makefile:
install-reviewer,install-baseline,install-probability,install-all-extras,peer-verify,peer-verify-network,peer-hints,peer-setup(sync dev+tui → validate →forensics peer-setup); top comment for peers. - CLI:
src/forensics/cli/peer_setup.py—forensics peer-setupprints uv tiers, spaCy/Quarto notes, config-drivenollama pulllines;--check-ollamausesbaseline.preflight.preflight_check; rejects--output json. Registered viaattach_peer_setup(app)inforensics.cli. - Tests:
tests/unit/test_cli_peer_setup.py— baseline models in stdout; JSON output rejected. - Docs:
README.md(spaCy wheel, Installation, models table),docs/RUNBOOK.md(peer subsection under Ollama).
Makefile,src/forensics/cli/peer_setup.py(new),src/forensics/cli/__init__.py,tests/unit/test_cli_peer_setup.py,README.md,docs/RUNBOOK.md,HANDOFF.md
uv run ruff check src/forensics/cli/peer_setup.py src/forensics/cli/__init__.py tests/unit/test_cli_peer_setup.py
uv run pytest tests/unit/test_cli_peer_setup.py tests/unit/test_cli_commands_dump.py -v --no-cov -q
# 5 passed
make peer-setuprunsforensics validate, which fails ifconfig.tomlstill has placeholder authors or other preflight hard-fails; peers should fix config before the meta-target or runmake install-reviewer+make peer-hintsseparately.
Status: Complete Date: 2026-05-10 Agent/Session: Cursor agent
- Scaffold: Ran
bun create @abstractdata/docs websiteat the repo root. Pulled in the Abstract Data Starlight theme (@abstractdata/starlight-theme@0.3.5), Bun-firstpackage.json, pydoc-markdown autodoc orchestrator, and the bundledabstract-data-setupskill. Cleaned the scaffold's embedded.git,.DS_Store, and tmp.fuse_hidden*files.bunx abstract-data-install-skillsran cleanly (all 11 skill markers detected as already present — kept existing). - Astro config: Set
site: 'https://abstract-data.github.io',base: '/mediaite-ghostink',trailingSlash: 'always'; rewrote sidebar to expose Get Started / Operator docs (synced) / CLI reference / Python API / Decision records / Forensic report; configurededitLink,lastUpdated, andstarlightLinksValidatorwith/report/**excluded. - Sync pipeline: New
website/scripts/sync-docs.mjscopies allow-listeddocs/*.md(ARCHITECTURE, RUNBOOK, TESTING, GUARDRAILS, DEPLOYMENTS, EXIT_CODES) intosrc/content/docs/synced/and alldocs/adr/*.mdintosrc/content/docs/adr/, injecting YAML frontmatter and rewriting internal links to base-prefixed Starlight URLs (off-list paths fall back to absolute GitHub URLs onmain). Emits a synthesizedadr/index.mdso the sidebar landing resolves. - CLI reference: New
scripts/generate_cli_docs.pywalksforensics.cli:appviatyper.main.get_commandand emits one Markdown page per command/subcommand (27 pages) plus an index, all witheditUrlfrontmatter pointing back tosrc/forensics/cli/__init__.py. Slugs are full-name dashed (forensics-analyze-section-profile) so the subcommand-table links match the on-disk filenames. - Python API: Configured
website/scripts/python-autodoc.jsonto targetforensics,forensics.config/models/scraper/features/analysis/reporting/storage/pipeline(uses the scaffold's pydoc-markdown orchestrator). Output lands atsrc/content/docs/api/(gitignored); regenerated locally viamake docs-pythononcepipx install pydoc-markdownis onPATH. - MDX pages: Hand-authored
website/src/content/docs/index.mdx(template: splash,<CardGrid>/<LinkCard>/<Aside>) andwebsite/src/content/docs/getting-started.mdx(<Steps>,<Aside>,<FileTree>,<Tabs>per acceptance criteria). Deleted the scaffold's placeholderquickstart.md. - Makefile: New targets
docs-cli,docs-python,docs-quarto,docs-dev,docs-build,docs-cleanchained off the new generators andbun runscripts (websiteis Bun-first). - CI cutover: New
.github/workflows/deploy-docs.yml(uv + Quarto + Bun, builds CLI ref + Quarto report + Astro site, uploadswebsite/distas a Pages artifact, deploys onmainpush). Removed the previous Cloudflare-only deploy at.github/workflows/deploy.ymlplus the deadwebsite/.github/workflows/{deploy,deploy-cloudflare,deploy-vercel}.ymlfiles that the scaffold dropped insidewebsite/. - Repo docs hygiene: Updated
docs/RUNBOOK.mdwith a "Documentation site (Astro Starlight)" section (commands, hosted URL, maintainer checklist), appended a build-artifacts Sign todocs/GUARDRAILS.md, refreshed the README's## Reports (Quarto)and## Documentationsections to point athttps://abstract-data.github.io/mediaite-ghostink/.
website/**— full new scaffold (theme, Bun lockfile,astro.config.mjs,package.json,scripts/,src/,.gitignore, skills, copilot instructions); generated output dirs gitignored.scripts/generate_cli_docs.py(new).github/workflows/deploy-docs.yml(new); removed.github/workflows/deploy.ymlMakefile(newdocs-*targets)docs/RUNBOOK.md,docs/GUARDRAILS.md,README.md(docs site section + Sign + Reports/Docs links)
# 1. Scaffold + bun install (Bun 1.3.3) — already ran during this session.
# 2. sync-docs idempotent
cd website && node scripts/sync-docs.mjs
# ✓ synced 6 operator docs + 11 ADRs
# 3. CLI generator deterministic
cd .. && uv run python scripts/generate_cli_docs.py
# generate_cli_docs: wrote 27 page(s) to website/src/content/docs/cli
# 4. Full production build
cd website && bun run build
# → 49 pages built; "✓ All internal links are valid."; pagefind index OK; sitemap-index.xml created.
# 5. Quarto + CI invariants
test ! -f .github/workflows/deploy.yml && echo OK
# OK
- Bun (not npm) as the canonical package manager because the
@abstractdata/docstemplate is Bun-first (bun.lockcommitted, scripts assumebun run, theme prefers it).deploy-docs.ymlusesoven-sh/setup-bun@v2andbun install --frozen-lockfile. - Generators produce base-prefixed internal links (
/mediaite-ghostink/...).starlight-links-validator@0.16joins the configuredbaseto the validator's page lookup, so unprefixed/cli/...URLs reported as invalid. Hand-authored MDX uses the same convention. docs/cli/is skipped at the repo root; the CLI generator writes directly intowebsite/src/content/docs/cli/. The repo's canonical CLI source remainssrc/forensics/cli/, and the generated reference is reproducible from it.- ADR/sync separation: ADRs land in
src/content/docs/adr/(not undersynced/) so the "Decision records" sidebar entry doesn't duplicate them inside "Operator docs." - Quarto sources untouched:
_quarto.yml,index.qmd,notebooks/,src/forensics/analysis/, andconfig.tomlwere not modified, per the spec's scope boundary.
- Enable GitHub Pages in repo settings:
Settings → Pages → Source: GitHub Actions. - Run the
Deploy Docsworkflow once (push tomainor useworkflow_dispatch) and confirm the deploy succeeds athttps://abstract-data.github.io/mediaite-ghostink/. - Retire the Cloudflare Pages project
ai-writing-forensicsin the Cloudflare dashboard. - Remove the
CF_API_TOKENandCF_ACCOUNT_IDrepo secrets underSettings → Secrets and variables → Actions. - Retire the legacy
make deploytarget (still callswrangler pages deploy) in a follow-up change once the CF project is gone. - Optional:
pipx install pydoc-markdownon contributor laptops somake docs-pythonpopulates the Python API section locally.
- The
@abstractdata/starlight-theme<VersionPicker>logs a "noversion:frontmatter" warning per page during build because we don't ship versioned API docs. Benign — warnings only, no build failure. Can be silenced later by adding aversions[]block topython-autodoc.jsonif/when we tag releases. starlight-links-validatoris base-aware but requires hand-written internal links to include the base prefix. Future contributors should follow the convention or use Starlight's slug-based linking; the existing generators encode this in one place each.- Quarto rendering is wired into the deploy workflow but skipped in local
make docs-dev. Runmake docs-quarto(ormake docs-build) when iterating on the report. - GitNexus impact analysis was not invoked; the changes are additive (new scripts, new workflow, gitignored generated content). Pre-merge, run
gitnexus_detect_changesto confirm the scope.
Status: Complete Date: 2026-05-12 Agent/Session: Copilot cloud agent (Claude Sonnet 4.6)
- Bumped all dependency minimum version pins in
pyproject.tomlto match the currently locked versions inuv.lock(reflecting the latest stable releases available as of ~7 days after the previous pinned minimums). - Added
DocumentationURL to[project.urls]inpyproject.toml. - Added a
Docsbadge (gh-pages URL) to the README header badge row.
pyproject.toml— bumped>=constraints for all main, dev, and optional dependencies; addedDocumentationURLREADME.md— added[](https://abstract-data.github.io/mediaite-ghostink/)badge
# All new constraints satisfied by uv.lock (verified via Python script):
# typer: locked=0.24.1 >= min=0.24.0 ✓
# polars: locked=1.40.0 >= min=1.40.0 ✓
# duckdb: locked=1.5.2 >= min=1.5.0 ✓
# ... (all 41 packages OK, none FAIL)
- Used the currently resolved versions in
uv.lockas the new minimums; no lock update needed. - Previous deploy-docs failure (run 25649887514) was a tag-timing race:
v0.1.3tag was not yet visible when the workflow ran. The tag now exists; subsequent runs should pass.
- The
uv.lockis still frozen — onlypyproject.tomlconstraints changed. Nouv lockregeneration needed unless new packages are added. - When the next release is cut (v0.1.4), ensure
v0.1.4tag exists before deploy-docs runs.