Repository navigation
Issue #1223: Define data portability implementation plan - #1315
Conversation
bjagg
left a comment
There was a problem hiding this comment.
This is excellent verification work. I checked the load-bearing claims against the code and all of them hold: the three export failures (including the new PartnerLIF one), the seed counts (9/24 groups, 6/1,378 transformations, 21 models), the 8 unique indexes, #717, and CI run 35801872026. Thank you for recording the reasoning that turned out wrong; that's rare and useful.
Filed from this: the PartnerLIF failure is now #1321. It has a second entry point, GET /datamodels/base/{id}, which 500s for any non-OrgLIF id. It's filed now rather than with the proposed tickets because #1212 exposes it on merge.
Asks:
- #1063: keep the CI bullet, but make it "fail, don't skip". The Postgres fixture calls
pytest.skipwhen Postgres is missing, and CI gets Postgres only becauseubuntu-latestpreinstalls it. Skip locally, fail whenCIis set. - Document-store section: please narrow it to "not needed for this epic". Portability is solvable on Postgres, which your reason 1 shows well. Reason 2 isn't accurate as written: MongoDB supports unique and partial unique indexes, so those rules wouldn't become racy application checks. The broader MDR-storage question belongs on #1132.
- Decisions → decisions needed. Several of the five are product and architecture choices that haven't been reviewed yet: destructive import (D1), what identifies a mapping (D3), and investing in the ancestor chain (D5), which depends on whether extensions and inclusions stay. Could you present them as proposed, with options and evidence, so they can be decided and recorded as ADRs? The findings and plan stand either way.
An alternative for D3, for that discussion: identify a mapping by (group, target path), with sources and expression as content. That keeps identity stable through source and expression edits, which is your main requirement, without a generated key. It rests on one rule: within a group, mappings and target fields correspond one-to-one. In the three versioned transforms (185 mappings), every well-formed mapping already satisfies it. The only violations are the two target-less CLR rules behind #1144, which the rule would have rejected. JSONata's ??, ?: and $exists() cover fallback sources within one mapping. Target renames imply a new target schema version, and so a new group, so recreating there is expected.
|
From Claude: Thanks — this was a useful review, and all three asks are in as of 1. #1063 — agreed, bullet restoredYou're right, and my reasoning was wrong rather than just my conclusion: I read "the test ran in CI" as "the test is guaranteed to run in CI." Bullet kept and re-scoped to skip locally, fail when 2. Document store — narrowed, and reason 2 withdrawnThe section is now "Out of scope for this epic," and the storage question is explicitly #1132's. Reason 2 is withdrawn in the text rather than quietly deleted — MongoDB has unique and partial unique indexes, so the #746 rules port rather than degrading into racy application checks. It wasn't load-bearing; the conclusion rests on the other two points, and the "1 and 2" reference now names them inline instead of pointing at numbers. The overlay question — extensions and inclusions are an overlay between models, a document is a tree — moved to #1132 as genuinely open, rather than being used here as an argument against the migration. 3. Decisions — options and evidence, recorded in the docEach of D1, D3 and D5 now carries the options that were weighed, the evidence, and what each costs, which is what you asked for. The difference is where the calls live: they are recorded in this doc rather than deferred to separate ADRs. The lean is to reserve ADRs for larger, system-wide decisions and let the smaller related ones live in a living feature guide for import/export. That is also the repo's own documented lifecycle for a proposal — "promote to ADR or guide on acceptance" ( If one of them is system-wide enough to deserve its own ADR, D3 is the candidate — it constrains what a mapping is allowed to be, which reaches past import/export into authoring. Where they landed: 1a (mirror, with a preflight preview), 3b (yours), 5a (one helper, full chain everywhere). D2 and D4 stay as they were. Your D3 alternative — adopted, with two corrections from measuring itI checked the rule rather than taking it, and the core holds exactly: 183 of 183 well-formed mappings, zero duplicate target paths in any group. Groups 29/49/50 at 83/70/30 mappings, distinct targets 83/70/30. The only exceptions are 1671 and 1672 — your two #1144 rules — and the rule would have rejected both at authoring time. Two premises moved, though: Many sources → one target is not supported — narrower than "fallback sources live inside one mapping." 179 of 183 mappings record exactly one source, and all four exceptions are unfinished drafts whose expression reads a single source anyway:
Two of the four write to placeholder targets ( Target renames don't imply a new schema version. This is the one place the alternative costs something, and the correction is Chris's rather than anything I measured: a target field can be renamed in place, with no new version and so no new group. The mapping's identity changes with it, so an import processes the rename as a delete plus a create and loses notes, contributor and dates. The doc now states that as the accepted price of 3b rather than arguing it away, with the preflight making it visible before anything is written. Surviving a rename in place is 3a's one real advantage, and it costs a column, a migration and a backfill. Also: Gap F is re-framed around what actually breaks identity. A mapping writing several targets is fine — identity is its target set. What is rejected is two mappings claiming the same target, which was already broken, just silently, since export overwrites at Beyond the asks
|
bjagg
left a comment
There was a problem hiding this comment.
Thanks for the revision. It's a big improvement: the #1063 bullet, the narrowed storage section with reason 2 withdrawn, options and evidence on every decision, and the Gap C reversal. Your FK finding held up and is now #1338. Checking it against the code changed the picture: the delete path never 500s, because the API's soft delete silently removes every mapping that uses the attribute, and a rename silently breaks them, since EntityIdPath is stored and the expression spells names inline.
We worked through the open decisions today. Here's what changes in the doc.
D1, import mode: explicit mode with an always-on preview. Every import shows the preflight preview. The caller picks merge (create and update only; the default) or replace (the mirror you described: omissions deleted, listed and confirmed). A replace is blocked, not warned, where a delete or rename would leave a mapping dangling (#1338). This is your 1a plus a safe default.
D3, mapping identity: 3b, with three clarifications.
- Exactly one target per mapping, and no target field produced by two mappings in a group. That replaces "a set of targets is fine". An expression that writes beyond its declared target is a validation error, which is the #1144 failure.
- Multiple sources are supported and are content, not identity. Import saves every entry in
SourceAttributes(transformation_service.py:282,:314), and #1296 has 13 real multi-source rules, e.g.Learner.displayNamefromfirstName+lastName. So "many sources → one target is not supported" should go. Your 183 are right for groups 29/49/50, where the four multi-source mappings are drafts. The declared set must list every field the expression reads. - In-place renames of a field a mapping depends on are blocked, with the dependent mappings listed. Renames go through a new schema version or an explicit re-point. That settles the cost you named for 3b, and it's #1338's fix.
D5: 5b, not 5a. No new investment in extensions and inclusions for now. Where import/export has to walk the ancestor chain, keep it local to import/export. Replacing the overlay model (a variant becomes a source schema plus a mapping to target LIF) is the direction for a later phase. Live overlay bugs like #1321 still get fixed.
D2, near-term scope: fixes and design. The three export 500s (#1210, #1211, #1321), #1338's dependency blocking, the converter contract (#1333) and #1142. The round-trip proof and migration slimming move to a later phase; D4 (lossless) stands as the requirement it will meet.
Recording: your lean holds for the local decisions (modes and preview, lossless, converter, scope), which go in the living import/export guide once this is promoted into docs/design/cross-cutting/. Three decisions reach past import/export, so they'll be ADRs: mapping identity (as you suggested), the overlay direction (amending metadata_repository/0004), and the reference model. I'll open those as a separate PR so this one isn't blocked on them.
#1321, #1338 and #1333 are assigned to you for the next sprint. With these edits in, I'm happy to approve.
…404 when there is none (#1348) ##### Description of Change **Problem.** `get_base_model_for_given_orglif` (`components/lif/mdr_services/datamodel_service.py`) looked up the extension with `DataModel.Type == "OrgLIF"`, then read `.BaseDataModelId` off the result without checking it. For anything that isn't a live OrgLIF, the query returns `None` and the next line raises `AttributeError`, which surfaces as a 500. That hits both callers: - `export_datamodel`, which calls the lookup for OrgLIF **and PartnerLIF**, so every PartnerLIF export would 500 once #1210 stops failing first. - `GET /datamodels/base/{id}`, which has no type guard, so it returned 500 for a PartnerLIF id, and also for a BaseLIF, deleted or nonexistent id, where 404 is the right answer. **Fix.** The lookup now matches any live data model that has a `BaseDataModelId`, rather than one type name. That covers OrgLIF and PartnerLIF, and follows #1315's plan to "key the converter off structure, not the type name". When nothing matches, it raises a 404. The change is 4 lines, and the function name is unchanged so the diff stays small. **Side effects.** `GET /datamodels/base/{id}` now answers 404 where it used to 500. I searched `frontends/`, `bases/` and `components/` and found no caller of that endpoint besides its own route, so no client depends on the old behavior. Only `lif_mdr_api` packages `mdr_services`, and `lif_mdr_api.yml` already watches `components/lif/mdr_services/**`. **Limitation.** A full PartnerLIF export still fails on #1210 (`check_base` passed to functions that don't accept it), which runs before this lookup. PR #1212 fixes that. Until it merges, this fix shows up on `GET /datamodels/base/{id}` but not yet on the export. **How to test.** `uv run pytest test/bases/lif/mdr_restapi/test_base_model_lookup.py`. These tests need Postgres (`initdb`): CI has it, and on a machine without it they skip. ##### Related Issues Closes #1321 Refs #1223 ##### Type of Change - [x] Bug fix (non-breaking change which fixes an issue) ##### Project Area(s) Affected - [x] components/ - [x] test/ or e2e/ - [x] API endpoints --- ##### Checklist - [x] commit message follows commit guidelines (see commitlint.config.mjs) - [x] tests are included (unit and/or integration tests) - [x] code passes linting checks (`uv run ruff check`) - [x] code passes formatting checks (`uv run ruff format`) - [x] code passes type checking (`uv run ty check`) - [x] pre-commit hooks have been run successfully ##### Testing - [x] Automated tests added/updated `test/bases/lif/mdr_restapi/test_base_model_lookup.py` adds 6 cases against the backup.sql seed: 1 BaseLIF, 17 live OrgLIF, 18 live PartnerLIF, 19 deleted PartnerLIF. | Case | Before the fix | After the fix | |---|---|---| | `GET /datamodels/base/17` (OrgLIF) | pass (pins existing behavior) | pass | | `GET /datamodels/base/18` (PartnerLIF) → base 1 | **fail**, `AttributeError` | pass | | `GET /datamodels/base/{1, 19, 999999}` → 404 | **fail** ×3, `AttributeError` | pass | | `export_datamodel(18)` includes base model 1 | **fail**, `AttributeError` | pass | The export test stubs `get_export_dto`, because without the stub it fails first on #1210. The test is about the base-model lookup that runs after that. The full suite, run in a container that has Postgres so nothing skips, gave **982 passed, 0 skipped**: the 976 baseline on `main` `9c64575` plus these 6. Project-wide `ty check --error-on-warning` and `pre-commit run --files` on both changed files pass. ##### Additional Notes - **Overlap with #1345** (groybal, draft): it rewrites only an import line in `datamodel_service.py` (`lif.datatypes.mdr_sql_model` → `lif.mdr_sql_model.core`), far from this hunk, so either merge order should merge cleanly. The new test file imports only `import_export_service`, so it is unaffected by #1345's move. - **Relation to #1212 / #1210:** either merge order is fine. #1212 only changes the inside of `get_export_dto`, in `import_export_service.py`, which this PR doesn't edit. The export test stubs `get_export_dto`, and #1212 leaves its 7-value return shape unchanged. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: dereck <dereck.haskins@gmail.com> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-authored-by: Benito Gonzalez <bgonzalez@unicon.net>
bjagg
left a comment
There was a problem hiding this comment.
Re-reviewed on 660da0e. Every point from my 9/30 review is in, so I'm approving. That covers explicit mode with always-on preview, merge by default and replace listed-and-confirmed, one target per group, multiple sources as content, in-place renames blocked, 5b plus the freeze, the near-term scope, and the FK finding. The design and phases are sound, and I'm fine with you validating the flows empirically as the tickets go.
A few follow-ups, as an appended commit after merge or in your first #1333 PR. Most come from things that landed on main after your last push, so none of it is on you.
Worth fixing soon:
- Two of the "three live export failures" were fixed yesterday.
- #1321 (the PartnerLIF base lookup) is fixed by #1348.
- #1211 (
export_multipleunpacking 6 of 7 values) is fixed by #1349. - Both have regression tests, so L83's "none of the three has a regression test" no longer holds.
- #1210 is still live; dev logs show the
check_baseTypeErrortoday. - Where to update it: "What is broken today", Phase 0 and the INDEX line. As written, Phase 0 sends a reader to two finished items. #1348 also adopted exactly the rule you argue for, keying the lookup on having a base model instead of the type name.
- The overlay ADR link points at the wrong ADR. L384 and L658 say the overlay direction "amends
metadata_repository/0004". That error came from my 9/30 review, sorry. The ADR isdata_model/0002, and it amendsmetadata_repository/0008. Please link all three ADRs from #1339 (data_model/0002,data_model/0003,metadata_repository/0009). - The mapping path format is decided. L895-897 (and #1333) leave the target path open ("drop it or document it as advisory"). ADR 0009 Decision 1 settles it: name-based
EntityIdPathwith the numeric model prefixes removed. - The INDEX entry describes the earlier decisions. It says "mirror-with-preflight" and "full ancestor-chain resolution", which are 1a and 5a. The doc now says 1c and 5b. The INDEX line is what most people read.
- An estimate to remove, since this repo is public. L960 says "Most are 1–3 days". Effort estimates belong on the private board.
Small corrections:
- L921 offers
transformation_service.py:132as a structural "self-contained" test, but that line checksType in [BaseLIF, SourceSchema], which is the type-name proxy. - L296 says
get_base_model_for_given_orglifexists only for export. TheGET /datamodels/base/{id}route (datamodel_endpoints.py:142) calls it too, which is the second entry point you name at L81. - L933 "the UI hardcodes the parent to
1": both defaults (Dialog.tsx:94,DataModelSelector.tsx:93) apply only to PartnerLIF. - Line anchors: 7 of the 53 now miss. Four
import_export_service.pyanchors moved +2 with #1349.schema_generation_service.py:802should be 892-895,mdr_sql_model.py#L26-L28should be 28-30, and:681,709should be 682 and 710. - Bug table and status: #1334, #1335 and #1336 aren't in the bug table. #1171 closed yesterday (#1274). The header still says Proposed, 2026-09-25, and L23 and L980 cite different base commits.
- 5b leftovers: L262 still recommends consolidating the copies, which the 5b section says is out of scope. L279's "Name lookups must search the whole chain" should be scoped to the converter.
- Heads-up: if Gary's draft #1345 merges, it moves
components/lif/datatypes/mdr_sql_model.pytocomponents/lif/mdr_sql_model/core.pyunchanged. That breaks four links here (L244, L428, L625, L862); the line numbers stay valid.
Two fixes on my side, in #1339, not yours:
…0009's mapping count From the review of LIF-Initiative#1315: - 0008's Data Portability paragraph still described import with a default Data Model ID plus a map of Data Model IDs, which the portability plan replaces (it closes LIF-Initiative#768 and LIF-Initiative#775). It now carries an amendment note pointing at 0009 Decision 1 (models, groups and mappings identified by name and version) and data_model/0003 (a row ID is never a reference). The original text stays as the record. The status note lists the ID-based import among 0009's amendments. - 0009 said "1 of 1,368 mappings". The V1.1 seed's Transformations table has 1,378 rows, the plan's figure. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ences (#1339) ##### Description of Change **What this records.** Three data-model decisions that came out of reviewing the #1223 implementation plan (#1315). They reach past import/export, so they are ADRs; the import/export-local decisions (merge/replace modes with a preview, lossless round trips, the converter contract) belong in the living import/export guide #1315 will become. | ADR | Decision | | --- | --- | | [`data_model/0002`](docs/design/adr/data_model/0002-lif-variants-freeze-the-overlay-model.md) | **Freeze the Org LIF / Partner LIF overlay model.** No new investment in inclusions and extensions; live defects (#1321) still get fixed; ancestor-chain work stays local to import/export. Direction: a LIF variant becomes a Source Schema plus a mapping to the target LIF. | | [`metadata_repository/0009`](docs/design/adr/metadata_repository/0009-mapping-identity-and-cardinality.md) | **A mapping is identified by `(group, target path)`.** Exactly one target per mapping and one mapping per target field; sources are a set and part of the content; an expression may write only its declared target; renaming a mapped field in place is blocked. | | [`data_model/0003`](docs/design/adr/data_model/0003-references-between-shared-entities.md) | **References are part of the data model and its output, never storage artifacts.** Explicit schema-level references (#1026, #1062); full JSON-LD `@id` references in records; layered organization identifiers (authoritative external IDs, then `did:web`, then a LIF registry); copies only of attested fields; provenance stays per node. | **Evidence** is in each ADR's Context, measured on `main`: mapping uniqueness across the 185 mappings in the three versioned transformation groups and the 56 in #1296; the one overlay-only uniqueness rule; entity sharing in Base LIF; organization references in a demo record. **Amendments.** [`metadata_repository/0008`](docs/design/adr/metadata_repository/0008-data-model-use-cases.md) gains a pointer to the two ADRs that amend it: its Org LIF / Partner LIF behavior is frozen rather than extended, and its "many-to-many" mapping statement is narrowed (several sources per mapping yes, several mappings per target no). **Status.** The three are marked **Accepted**, since the architect made these decisions. Most existing ADRs sit at *Proposed*, so if you'd prefer these start there for team review, say so and I'll change them. **Other changes.** `docs/INDEX.md` entries for the three ADRs. `cspell.json` gains `IPEDS`, `NCES`, `OPEID` and `postsecondary`. **How to review.** Read the three ADRs. The parts most worth pushing on are the Alternatives sections, which record what was rejected and why. ##### Related Issues Refs #1223 Refs #1315 Refs #1338 Refs #1321 Refs #1026 ##### Type of Change - [x] Documentation update ##### Project Area(s) Affected - [x] Documentation (docs/, READMEs, ARCHITECTURE.md, CLAUDE.md) --- ##### Checklist - [x] commit message follows commit guidelines (see commitlint.config.mjs) - [x] documentation is changed or added (in /docs directory) - [x] pre-commit hooks have been run successfully: `uv run pre-commit run --files` on all six changed files (cspell, ty and pytest pass; the Python hooks have no files to check) ##### Testing - [x] Manual testing performed: all relative links in the changed files resolve 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
- Deploy workflows now watch mdr_sql_model (mdr_api, learner_data_export_api) and lif_schema_config (translator); mdr_api drops datatypes, which it no longer packages. check_workflow_paths.py passes. - Repoint the #1315 proposal's links to mdr_sql_model/core.py (file moved unchanged, so line anchors still hold). - Revert unrelated uv.lock churn in lif_orchestrator_api. - README/comment accuracy fixes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#1345) ##### Description of Change **Problem.** `components/lif/datatypes/mdr_sql_model.py` held MDR's SQLModel ORM tables, so every consumer of `datatypes` pulled in MDR's SQLAlchemy/SQLModel dependency, and the project brick tables had gaps around it (#1344). **Solution.** - Moved the ORM tables into a new component, `components/lif/mdr_sql_model/` (`core.py`, contents unchanged), and moved `datatypes/mdr_consumer.py` to `mdr_dto/mdr_consumer_dto.py`. Updated every import in `mdr_dto`, `mdr_services`, `mdr_restapi`, `learner_data_export_api`, and the tests. - Brick tables: `lif_mdr_api` now packages `mdr_sql_model` instead of `datatypes`. `lif_learner_data_export_api` packages `mdr_sql_model` and its transitive deps. `lif_translator_api` adds `lif_schema_config` (a type-only import via `mdr_client`). - Deploy workflow `paths:` filters now watch the newly packaged bricks (`mdr_api`, `learner_data_export_api`, `translator`). `scripts/check_workflow_paths.py` (#1274) passes. - Repointed the 4 links in `docs/operations/proposals/mdr-import-export-portability.md` (#1315) to `mdr_sql_model/core.py`. The file moved byte-identical, so the line anchors still hold. **Heads-up @cbeach47 :** please use the new path for any future links. - **Incidental fix, not part of #1344:** `projects/dagster_oss_ecs/pyproject.toml` declared `name = "dagster_docker_compose"` (copy-paste). It's now `dagster_oss_ecs`. **⚠️ Merge order with #1347.** Both PRs touch the imports in `components/lif/mdr_dto/transformation_group_dto.py` (confirmed with `git merge-tree`). When resolving, **keep this PR's `from lif.mdr_sql_model.core import …`** and take #1347's other edits. Taking #1347's side as-is restores `from lif.datatypes.mdr_sql_model import …`, a module this PR removes, and MDR won't start. **How to test:** `uv run pytest test/`, `uv run python scripts/check_workflow_paths.py`, `uv run poly check`. ##### Related Issues Closes #1344 ##### Type of Change - [x] Bug fix (non-breaking change which fixes an issue) - [x] Infrastructure/deployment change - [x] Code refactoring ##### Project Area(s) Affected - [x] bases/ - [x] components/ - [x] projects/ - [x] test/ or e2e/ - [x] Documentation (docs/, READMEs, ARCHITECTURE.md, CLAUDE.md) --- ##### Checklist - [x] commit message follows commit guidelines (see commitlint.config.mjs) - [x] tests are included (unit and/or integration tests) - [x] documentation is changed or added (in /docs directory) - [x] code passes linting checks (`uv run ruff check`) - [x] code passes formatting checks (`uv run ruff format`) - [x] code passes type checking (`uv run ty check`) - [x] pre-commit hooks have been run successfully - [x] configuration changes: relevant folder README updated ##### Testing - [x] Automated tests added/updated (existing tests repointed to the new module paths; full `pytest test` passes in pre-commit) ##### Additional Notes Shared bricks touched: `mdr_dto` and `mdr_services`, packaged by `lif_mdr_api` and `lif_learner_data_export_api`. Both workflows' `paths:` filters cover every brick they package. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Benito Gonzalez <bgonzalez@unicon.net>
Description of Change
Adds the implementation plan for epic #1223 (MDR import/export portability) as a proposal doc.
No code changes — this is the design and ticket-triage work that the epic's ~25 issues need before
anyone estimates them.
Problem. The epic's work was spread across ~25 issues with no shared design, several of them
written against assumptions that no longer hold. Export was known to be broken but not how
broken, and nobody had established what a portable file actually has to contain.
What the doc does. Every claim is verified against
mainrather than reasoned from the issuetext. That turned up material the epic did not have:
AttributeErrorbecause the base-model lookup filters on
Type == "OrgLIF". It is currently masked by MDR /import_export/export/{id} 500: check_base passed to entity service functions that don't accept it #1210,which fails first — so PR Issue #1210: fix MDR /import_export/export/{id} 500 from check_base passed to entity service functions #1212 will expose it. Seed model 18 (
Org2 LIF) triggers it.value mappings), no update path for schemas at all, and a third kind of reference — the
TargetEntityIdcolumn added inV1.6— that is a raw row ID already carried on the exportedattribute record and handled nowhere on import.
(8 uniqueness rules already exist), and one bullet of MDR schema round-trip: true data-portability test matrix (follow-up to #1007) #1063 (the round-trip test already runs in
CI against real Postgres).
PortableKeyrather than by name — names cannot work, and the reason usually given for that (118 duplicates in
seed) is itself wrong, since all 118 are soft-deleted history.
superseded; Duplicate CreateTransformationGroupDTO / UpdateTransformationGroupDTO across two modules (definitions differ) #1138 and MDR Transformation Portability: Expose a UX widget to delete a transformation group #774 to keep but outside the epic).
Side effects / limitations.
cspell.jsongains five words (conftest,demoable,demoability,runbook,runbooks). The last two are pre-existing findings inINDEX.mdthatonly surface because this PR touches that file and the cspell hook is scoped to changed files.
The plan defers goal 2 (pulling seed data out of the migrations) to a follow-on effort, per
Decision 2, while keeping the round-trip proof that was the epic's done-test.
How to review. The doc is self-contained and every claim cites a file and line. The parts most
worth a second opinion are the five decisions, the eleven proposed new tickets, and the argument in
"Key the converter off structure, not the type name."
Related Issues
Refs #1223
Type of Change
Project Area(s) Affected
Checklist
Testing
No code changed, so the usual suites do not apply. Verified:
cspellpasses on all three changed files (the hook is scoped to changed files, soINDEX.mdhad to come clean too).
docs/operations/proposals/.docs/INDEX.mdentry added per thedocs-indexskill convention, appended within the proposalssection rather than reordering it.
operations/proposals/follows the decision tree indocs/README.md; thelifecycle there is "promote to ADR or guide on acceptance", which is the intent once the epic
closes.
Additional Notes
Two things reviewers may want to push on:
why mapping export fails, and a header warning not to infer structural limits from the seed
data. Both cost a few lines and both were things this review got wrong once before measuring.
decisions landing first, and filing them before review would presume the outcome.
🤖 Generated with Claude Code