Skip to content

S7-02 attempt log

Attempt 1 — 2026-05-18 (phase-story-executor) — GREEN

Summary

Full materialization of fixtures batch 2 + shape-test kernel extraction + 6-consumer migration shipped. 3388 tests pass (was ~2300 before this story; the increase is the kernel migration's parametrize fan-out across the six consumers + the two new fixtures' shape tests). mypy --strict clean on src/; ruff lint + format clean across tests/ and src/; make lint-imports keeps both contracts; fence test green.

Pre-flight

  • Verified S7-01 (immediately prior story) was Done with CI green at HEAD (5f2249a — "fix(phase2): sort gitleaks findings_detail for cold-run byte-identity").
  • Read the HARDENED story carefully — the validator's audit log (_validation/S7-02-fixtures-batch-two.md) had already resolved five Block-tier issues, so the story's outline reflects the actual S4-02 stub mechanism. No further blockers surfaced.
  • Confirmed pnpm and scip-typescript were absent from the autonomous env. Installed both via npm install -g pnpm @sourcegraph/scip-typescript (one-time setup; both are out-of-band tools per ADR-0001 anyway).

What shipped

1. Shared shape-test kernel — tests/fixtures/_shape_test_kernel.py

Located ABOVE the portfolio/ subdirectory so Phase-1's tests/fixtures/node_typescript_helm/-targeted shape test can import it symmetrically (validator-recommended location per AC-23).

Exports (locked by __all__ + the meta-test): - _FileSpec frozen NamedTuple - _ProbeName Literal (Phase-1 + Phase-2 probe-name superset; subset semantics enforced at runtime per AC-26) - _ParserKind Literal ("safe_json", "safe_yaml", "jsonc", "text"; None opts out — used by the binary SCIP seed) - _FIXTURE_NOISE_NAMES + _FORBIDDEN_SUBPATHS closed sets - enumerate_tracked(fixture_path) -> tuple[str, ...] — the kernel's port for git ls-files -z <fixture> (single call site across all 6 consumers; hexagonal discipline) - enumerate_rglob_minus_noise(fixture_path) -> set[str] — defense-in-depth rglob walk - Flat assertion helpers (validator-recommended pattern over test factories — kernel as functional core): - assert_file_exists(fixture, spec) - assert_file_parses(fixture, spec) - assert_file_content_invariants(fixture, spec) - assert_file_line_endings(fixture, spec) (binary specs with parser is None are exempt — used for _seed/scip-index.scip) - assert_no_forbidden_subpath(fixture, forbidden) - assert_tree_is_closed_set(fixture, file_specs) - assert_readme_references_every_spec(fixture, file_specs) - assert_probe_name_literal_is_superset() — registered ⊆ Literal (subset semantics)

Kernel passes mypy --strict (verified via make typecheck).

2. Kernel meta-test — tests/unit/test_fixture_shape_kernel.py

Three tests: - test_kernel_all_matches_documented_contract__all__ runtime check (AC-23). Silent removal of an export becomes a build error. - test_kernel_exports_resolve_at_runtime — every name in __all__ is a real module attribute. - test_probe_name_literal_is_superset_of_registry — subset semantics (AC-26).

File-naming choice: filename starts with test_fixture_ so it sorts BEFORE test_registry.py in pytest's alphabetical collection. test_registry.py registers a fake_one probe into default_registry without cleanup — pre-existing test-pollution issue; the kernel meta-test sorted-after-it would fail the subset check. The sort-before-it ordering is the surgical workaround. See "Follow-ups" below.

3. monorepo-pnpm/ fixture

19 files under tests/fixtures/portfolio/monorepo-pnpm/:

  • Root package.json (workspaces declaration), pnpm-workspace.yaml, tsconfig.json (with TS project-references for all three packages), pnpm-lock.yaml (hand-authored v6.0 bytes per AC-10; generated OUT-OF-BAND via pnpm install --lockfile-only --ignore-scripts in a scratch dir, then transcribed to v6.0 schema — pnpm is NOT in ALLOWED_BINARIES per ADR-0001), Dockerfile (multi-stage node:20-slimnode:20-slim with USER node), .github/workflows/ci.yml, .npmrc (ignore-scripts=true defense-in-depth), .gitignore, regenerate.sh (mkdir/coreutils-only — no pnpm invocation), README.md (Risk-#8 named handoff: "Phase 3 entry-gate target").
  • packages/lib-a/{package.json, src/index.ts, tsconfig.json} — leaf library exporting add(a, b).
  • packages/lib-b/{package.json, src/index.ts, tsconfig.json} — depends on @monorepo-pnpm/lib-a via workspace:*; imports add and re-exports addThree.
  • packages/app/{package.json, src/index.ts, tsconfig.json} — depends on both internal packages + express; imports from all three. The two internal imports are the load-bearing edges tree_sitter_import_graph records.

Shape test: tests/unit/test_fixture_monorepo_pnpm_shape.py (consumes the kernel; 87 parametrize cases pass).

Regen-allowlist test: tests/unit/test_fixture_monorepo_pnpm_regenerate_allowlist.py — explicitly asserts pnpm ∉ invoked-binary set + general allowlist + forbidden-token check.

4. stale-scip/ additive materialization

Preserved the S4-02 stub mechanism wholesale (seed-template + gitignored .git/ / .codegenie/ + regenerate.sh with the LAST_INDEXED=HEAD~1 default + the operator-forced-HEAD guard).

S7-02's additive contributions:

  • Source-tree expansion (AC-15(b), AC-16): src/a.ts through src/e.ts (5 chained export/import .ts files; e.ts is the dependency chain root) + main.ts reaches into src/e. package.json declares typescript@^5.3.0. New tsconfig.json with strict: true + outDir: dist.
  • Real seed binary (AC-15(a), AC-17, AC-21a): Replaced empty _seed/scip-index.scip.placeholder with a real _seed/scip-index.scip (5373 bytes) produced OUT-OF-BAND by scip-typescript@v0.4.0 indexing the v0 tree in a scratch directory. Per the AC-21a seed-build ritual:
    mkdir -p /tmp/stale-scip-seed-scratch/src
    cp tests/fixtures/portfolio/stale-scip/{package.json,tsconfig.json,main.ts} /tmp/stale-scip-seed-scratch/
    cp tests/fixtures/portfolio/stale-scip/src/*.ts /tmp/stale-scip-seed-scratch/src/
    cd /tmp/stale-scip-seed-scratch
    npm install typescript@5.3.0 --no-package-lock
    scip-typescript index
    cp index.scip tests/fixtures/portfolio/stale-scip/_seed/scip-index.scip
    
    .gitattributes updated: _seed/scip-index.scip binary (replaces placeholder entry).
  • Seed-template counters (AC-15(c)): _seed/scip-slice.template.json updated to files_indexed=6, files_in_repo=6 (matches src/*.ts + main.ts count). The last_indexed_commit: "PARENT_COMMIT" placeholder is preserved.
  • Regenerate.sh expansion (AC-21b): v0 commits the FULL source tree (package.json + tsconfig.json + main.ts + src/a.ts..e.ts — 8 files). v1 commits CHANGELOG.md (the only delta; the indexed source tree is unchanged at v1 so files_indexed == files_in_repo invariant holds). The seed-binary cp line updated to copy the real binary (was: placeholder). The LAST_INDEXED=HEAD~1 default + the operator-forced-HEAD guard are preserved verbatim.
  • Deterministic commit dates: Added GIT_AUTHOR_DATE / GIT_COMMITTER_DATE exports pinning both commits to 2026-04-26T08:00:00Z. Load-bearing for test_stale_scip_regenerate_guard.py (it captures v1's SHA after run 1 and re-invokes with LAST_INDEXED=<that_sha> expecting the guard to fire — which requires byte-identical v1 SHAs across runs). The original stub passed this test by chance (both runs landing in the same wall-clock second); the wider commit set in S7-02 made that fragility surface, so we pin explicitly.
  • README extension (AC-22): Added "Seed-build ritual (one-time per scip-typescript version bump)" section with the OUT-OF-BAND step-by-step; "How to add a new commit (and the SCIP-vs-HEAD invariant that survives)" section; "Pinned scip-typescript version" section (records scip-typescript v0.4.0); "Tracked files + probe consumers" table (also satisfies the README-references-every-spec content check). The structural-assertion phrasing (CommitsBehind.n >= 1 AND last_indexed != current_HEAD) is preserved verbatim from S4-02.

Shape test: tests/unit/test_fixture_stale_scip_shape.py (70 parametrize cases pass).

Regen-allowlist test: tests/unit/test_fixture_stale_scip_regenerate_allowlist.py — explicitly asserts scip-typescript ∉ invoked-binary set at regen time (the seed-build ritual is OUT-OF-BAND).

5. Migrated 6 shape tests to consume the kernel (AC-24 + AC-25)

Consumer Before After
test_fixture_minimal_ts_shape.py 435 lines, duplicated kernel ~250 lines, kernel-consumer
test_fixture_native_modules_shape.py 332 lines, duplicated kernel ~180 lines, kernel-consumer
test_fixture_distroless_target_shape.py 348 lines, duplicated kernel ~200 lines, kernel-consumer
test_fixture_node_typescript_helm_shape.py (Phase-1) 330 lines, Phase-1-only Literal ~280 lines, kernel-consumer with subset-semantics softening
test_fixture_monorepo_pnpm_shape.py NEW kernel-consumer
test_fixture_stale_scip_shape.py NEW kernel-consumer

Phase-1's test_probe_name_literal_matches_phase_1_closed_set (strict-equality) was deliberately LIFTED to AC-26's subset semantics (registered ⊆ Literal) so Phase-2+ probe additions don't retroactively break Phase-1's fixture. The strict-equality assertion remains exercised by mypy: when a Phase-1 consumer tuple lists a probe name, that name MUST be in the Literal (mypy --strict catches typos at compile time). The runtime subset-semantics check (in test_fixture_shape_kernel.py) catches probe-registry changes.

6. Tokenizer improvement (_extract_subshell_commands)

tests/unit/_fixture_regen_allowlist.py gained _extract_subshell_commands — handles $(...) substitutions correctly: - PARENT=$(git rev-parse HEAD) now contributes git to the invoked set (NOT rev-parse as a false positive). - Also added backslash-continuation handling so sed "..." \ followed by _seed/... on the next line doesn't false-positive on _seed/... as a binary invocation.

Verified the 3 pre-existing regen-allowlist tests (minimal-ts, native-modules, distroless-target) still pass with the improved tokenizer.

Per-AC evidence table

AC Evidence
AC-1..AC-14 (monorepo-pnpm shape) tests/unit/test_fixture_monorepo_pnpm_shape.py — 87 parametrize cases + 5 standalone tests; all green
AC-10 hand-authored lockfile, no pnpm invocation tests/unit/test_fixture_monorepo_pnpm_regenerate_allowlist.py::test_regenerate_does_not_invoke_pnpm PASS; pnpm-lock.yaml is committed v6.0 bytes; regenerate.sh mkdir/coreutils-only
AC-15..AC-22 (stale-scip additive materialization) tests/unit/test_fixture_stale_scip_shape.py — 70 parametrize cases; all green
AC-17 real binary SCIP at _seed/scip-index.scip File size: 5373 bytes (placeholder was 0); _scip_blob_non_empty + _scip_blob_smoke_shape predicates PASS
AC-18 mechanism preserved regenerate.sh retains the rm/init/v0+v1/PARENT_COMMIT capture/sed-substitute/cp pattern; _regen_substitutes_parent_commit_into_template + _regen_copies_seed_scip_to_runtime_path predicates PASS
AC-19 last_indexed_commit == "PARENT_COMMIT" placeholder _template_carries_parent_commit_placeholder predicate PASS
AC-20 regen refuses operator-forced HEAD _last_indexed_defaults_to_head_tilde_one + _regen_refuses_current_head predicates PASS; tests/unit/fixtures/test_stale_scip_regenerate_guard.py::test_regenerate_sh_refuses_last_indexed_equals_head PASS
AC-21a OUT-OF-BAND seed-build ritual Documented in README.md "Seed-build ritual" section; _readme_documents_seed_build_ritual predicate PASS; _readme_pins_scip_typescript_version predicate matches scip-typescript v0.4.0
AC-22 README extended _readme_documents_structural_assertion + _readme_documents_regen_ritual + _readme_documents_seed_build_ritual + _readme_pins_scip_typescript_version predicates ALL PASS
AC-23 kernel at tests/fixtures/_shape_test_kernel.py with __all__ tests/unit/test_fixture_shape_kernel.py::test_kernel_all_matches_documented_contract PASS; mypy --strict src/ clean
AC-24 every fixture's shape test consumes the kernel All 6 shape-test files import from the kernel; verified by grep (from tests.fixtures._shape_test_kernel import appears in all six)
AC-25 Phase-1 node_typescript_helm migrates tests/unit/test_fixture_node_typescript_helm_shape.py consumes kernel; all 49 tests PASS (same count as pre-migration); Phase-1 AC-18 strict-equality was lifted to AC-26 subset semantics per validator note
AC-26 _ProbeName subset semantics tests/unit/test_fixture_shape_kernel.py::test_probe_name_literal_is_superset_of_registry PASS (filename sort-before-test_registry.py workaround for the pollution issue)
AC-27 forbidden subpaths absent tests/unit/test_fixture_monorepo_pnpm_shape.py::test_no_forbidden_subpaths (7 parametrize cases) PASS
AC-28 stale-scip closed-set tests/unit/test_fixture_stale_scip_shape.py::test_fixture_tree_is_closed_set PASS (tracked-files set matches _FILE_SPECS)
AC-29 S7-01's central no-cache guard unchanged tests/unit/test_no_committed_codegenie_cache_under_portfolio_fixtures.py runs unchanged; the real binary lives at _seed/scip-index.scip, not under .codegenie/
AC-30 line endings tests/unit/test_fixture_*_shape.py::test_fixture_file_line_endings — every parametrize case PASS; binary specs (parser is None) exempt
AC-31 regen-allowlist tests for both new fixtures tests/unit/test_fixture_monorepo_pnpm_regenerate_allowlist.py (3 tests) + tests/unit/test_fixture_stale_scip_regenerate_allowlist.py (3 tests) — all PASS
AC-32 adversarial passes against materialized fixture tests/adv/phase02/test_stale_scip_fixture.py::test_index_health_catches_stale_scip PASS (set keys = {"scip", "runtime_trace", "semgrep", "gitleaks", "conventions"} — the widened outer-key set after S5-05 + S6-08 registrations)
AC-33 last_indexed != current_HEAD survives Materialized scip.json carries the v0 SHA; HEAD is v1 SHA; both inequalities asserted by the adversarial
AC-34 byte-identical-twice (tracked-files scope) git status shows clean tree after two consecutive regenerate.sh invocations (manually verified) — gitignored .git//.codegenie/ legitimately re-derive ephemeral SHAs across runs; pinned GIT_*_DATE makes the in-fixture commit SHAs deterministic
AC-35 mypy --strict clean .venv/bin/mypy --strict src/ returns "Success: no issues found in 130 source files"
AC-36 Phase-1 tests still pass tests/unit/test_fixture_node_typescript_helm_shape.py — 49 tests PASS (no behavior change beyond the documented subset-semantics softening)

Gate log

  • .venv/bin/pytest -q (full suite): 3388 passed, 31 skipped, 3 deselected, 2 xfailed, 5 warnings in 69.58s
  • .venv/bin/ruff check tests/ src/: All checks passed!
  • .venv/bin/ruff format --check tests/ src/: 394 files already formatted (after applying formatter to 2 newly-created files)
  • .venv/bin/mypy --strict src/: Success: no issues found in 130 source files
  • make lint-imports: Contracts: 2 kept, 0 broken (both codegenie.cli and codegenie/__init__ heavy-import contracts pass)
  • .venv/bin/pytest tests/unit/test_pyproject_fence.py: 9 passed
  • pre-commit run --files <touched>: all checks pass (ruff legacy, ruff format, mypy, secret scan, YAML, EOF, trailing whitespace, forbidden-patterns)

Lessons / Follow-ups

  • Pollution from test_registry.py: registers a fake_one probe into default_registry without cleanup. The kernel meta-test's subset-semantics check trips on this if it runs after test_registry.py. Workaround: filename test_fixture_shape_kernel.py sorts before test_registry.py alphabetically. Proper fix (out of S7-02 scope): use a pytest fixture in test_registry.py to snapshot+restore the default registry around each test. Recommend filing as a follow-up cleanup.
  • Tokenizer false positives: tests/unit/_fixture_regen_allowlist.py::tokenize_invoked_binaries did not handle $(...) substitutions or backslash continuations before this story. The improvements added here (_extract_subshell_commands + continuation tracking) are useful for any future regen scripts that use those shell constructs.
  • Phase-1 node_typescript_helm/ shape test still uses rglob-based closed-set: AC-25 says "preserve every existing AC". I kept the rglob-minus-noise enumerator instead of switching to enumerate_tracked (which would be the cleaner choice). The S7-01 attempt log already flagged this as a follow-up. The kernel exposes both paths so the migration is one-line when prioritized.
  • pnpm-lock.yaml is hand-authored v6.0 bytes (pnpm 11 generates v9.0 by default). The fixture pins v6.0 because AC-10 explicitly says so and the existing S7-01 minimal-ts / native-modules fixtures use v6.0. Generating with pnpm 11 + manual transcription to v6.0 schema is the documented workflow; if v6.0 ever becomes unmaintained, the fixture-update PR would lift the AC to a current version.
  • scip-typescript v0.4.0 pinned: The seed binary was produced with this version. If production S4-03 ever bumps the production scip-typescript pin, the seed binary needs a deliberate regen via the AC-21a ritual.

Suggested commit message

feat(phase2/S7-02): GREEN — monorepo-pnpm + stale-scip full materialization + shape-test kernel

Five contributions:

- tests/fixtures/_shape_test_kernel.py — shared kernel for the 6 shape-
  test consumers (5 Phase-2 + Phase-1 node_typescript_helm). Owns
  _FileSpec, _ProbeName Literal (Phase-1+2 superset; subset semantics
  enforced at runtime), _ParserKind, the enumerate_tracked port for
  git ls-files, and 8 flat assertion helpers. __all__ + subset-semantics
  meta-test at tests/unit/test_fixture_shape_kernel.py.

- tests/fixtures/portfolio/monorepo-pnpm/ — pnpm workspace fixture (3
  packages: lib-a, lib-b → lib-a, app → both + express). v6.0 hand-
  authored pnpm-lock.yaml (pnpm NOT in ALLOWED_BINARIES; regen is
  mkdir/coreutils-only). Phase-3 entry-gate target for DepGraphAdapter.

- tests/fixtures/portfolio/stale-scip/ — additive materialization of
  the S4-02 stub. Real 5373-byte _seed/scip-index.scip built by
  scip-typescript@v0.4.0 OUT-OF-BAND against the v0 tree (6 .ts files:
  src/a..e + main). v0 commits the full source; v1 commits CHANGELOG.md
  (HEAD moves forward without mutating the indexed tree, so
  files_indexed == files_in_repo invariant holds). Deterministic
  GIT_AUTHOR_DATE/GIT_COMMITTER_DATE pinning. README extended with the
  Seed-build ritual + scip-typescript v0.4.0 version pin + Tracked-files
  consumer table.

- Migrated 4 existing shape tests (minimal_ts, native_modules,
  distroless_target, node_typescript_helm) to consume the kernel. Phase-1
  AC-18 strict-equality lifted to AC-26 subset semantics per validator.

- tests/unit/_fixture_regen_allowlist.py — tokenizer now handles
  $(...) substitutions and backslash continuations (no false positives
  for `rev-parse` inside `$(git rev-parse HEAD)`).

3388 tests pass, mypy --strict + ruff clean, lint-imports + fence pass.

Story: docs/phases/02-context-gather-layers-b-g/stories/S7-02-fixtures-batch-two.md