Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion docs/adapters/azuredevops.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,10 +273,17 @@ adapter.import_artifact(
)

# Access imported proposal
proposal = project_bundle.change_tracking.proposals["123"]
proposal = project_bundle.change_tracking.proposals["add-feature-x"]
print(f"Imported: {proposal.title} - {proposal.status}")
```

Selective imports preserve the native Azure DevOps payload, including
`fields`, so the imported work item remains valid input for proposal parsing and
bridge sync flows. When no OpenSpec metadata is present, imported proposal names
fall back to a title-derived slug such as `add-feature-x`; if that slug already
exists, the adapter appends the source work item ID (for example,
`add-feature-x-123`) to keep the proposal name stable and readable.

### Status Synchronization

```python
Expand Down
1 change: 1 addition & 0 deletions openspec/CHANGE_ORDER.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ These are derived extensions of the same 2026-02-15 plan and are required to ope
| backlog-core | 06 | βœ… backlog-core-06-refine-custom-field-writeback (implemented 2026-03-03; archived) | [#310](https://github.com/nold-ai/specfact-cli/issues/310) | #173 |
| backlog-core | 07 | backlog-core-07-ado-required-custom-fields-and-picklists | [#337](https://github.com/nold-ai/specfact-cli/issues/337) | βœ… #310 |
| bugfix | 01 | bugfix-backlog-html-export-validation | TBD | β€” |
| bugfix | 02 | bugfix-02-ado-import-payload-slugging | [#427](https://github.com/nold-ai/specfact-cli/issues/427) | β€” |

### backlog-scrum

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# TDD Evidence: bugfix-02-ado-import-payload-slugging

## Failing First

- `2026-03-20`: `hatch run pytest tests/unit/adapters/test_ado.py tests/unit/adapters/test_github.py tests/integration/sync/test_ado_backlog_sync.py tests/integration/sync/test_backlog_sync.py tests/unit/sync/test_bridge_sync_import.py -q`
failed with `5 failed, 73 passed` before the fix. The failures showed two regressions:
selective ADO fetch reduced the work item to a summary payload without native
`fields`, and shared backlog import fell back to numeric-only change IDs such
as `123` instead of title-derived slugs.
- `2026-03-20`: audit of adjacent import paths confirmed that the shared
`import_backlog_item_as_proposal()` helper is used by both the ADO and GitHub
adapters, so the title-first normalization fix had to be applied in the
shared backlog adapter rather than only in the ADO implementation.

## Passing

- `2026-03-20`: `hatch run pytest tests/unit/adapters/test_ado.py tests/unit/adapters/test_github.py tests/unit/sync/test_bridge_sync_import.py -q`
passed with `76 passed`. This covers native selective fetch for ADO and
GitHub, title-first imported change IDs, deterministic collision suffixes,
and the bridge selective-import contract.

- `2026-03-20`: `openspec validate bugfix-02-ado-import-payload-slugging --strict`
passed.
- `2026-03-20`: `hatch run format`
passed after reformatting the touched files.
- `2026-03-20`: `hatch run type-check`
passed with `0 errors`.
- `2026-03-20`: `hatch run yaml-lint`
passed.
- `2026-03-20`: `hatch run contract-test`
exited `0` using cached results (`No modified files detected - using cached results`).
- `2026-03-20`: `hatch run smart-test`
exited `0`; the command skipped mapped test execution for this delta and emitted only its standard coverage warning.
- `2026-03-20`: `hatch run specfact code review run --json --out .codex-bugfix-02-review.json src/specfact_cli/adapters/ado.py src/specfact_cli/adapters/backlog_base.py src/specfact_cli/adapters/github.py tests/unit/adapters/test_ado.py tests/unit/adapters/test_github.py tests/unit/sync/test_bridge_sync_import.py tests/integration/sync/test_ado_backlog_sync.py tests/integration/sync/test_backlog_sync.py`
exported the governed review report to `.codex-bugfix-02-review.json`. The overall report still fails because the touched legacy adapter files already carry a large historical baseline, but filtering the report to the lines changed by this fix yields `0` findings.

## Outstanding Gate Caveat

- `2026-03-20`: `hatch run lint`
still exits non-zero in this worktree because the repository-wide lint script (`ruff format . --check && basedpyright ... && ruff check . && pylint src tests tools`) surfaces existing baseline findings outside this bugfix. The modified lines for this change were reviewed separately via `.codex-bugfix-02-review.json` and returned `0` changed-line findings.
54 changes: 54 additions & 0 deletions openspec/changes/bugfix-02-ado-import-payload-slugging/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Design: Fix ADO selective bridge import payload contract and title-based change IDs

## Overview

The failure in issue `#425` is a contract break inside the selective bridge import path:

1. `BridgeSync` fetches one backlog item by ID for import.
2. `AdoAdapter.fetch_backlog_item()` returns a reduced summary payload.
3. `AdoAdapter.import_artifact()` and `extract_change_proposal_data()` still expect the native ADO work item shape with `fields`.
4. Import fails before OpenSpec change creation.

Once the raw payload is restored, the fallback naming path still degrades to the numeric ADO work item ID when no OpenSpec metadata is present. That behavior is technically unique but operationally poor. The fallback should stay readable first, with the numeric source ID kept as provenance, not as the primary OpenSpec change name.

## Design Decisions

### 1. Preserve the provider-native payload on selective import

`fetch_backlog_item()` for ADO should return the native work item document needed by the import path. If older call sites benefit from convenience keys such as `title`, `state`, and `description`, those can remain as additive compatibility fields, but they must not replace or strip the provider-native shape.

This keeps the import contract coherent:

- `fetch_backlog_item()` returns an artifact suitable for `import_artifact()`
- `extract_change_proposal_data()` reads the same native payload
- contract tests can verify the round trip directly

### 2. Normalize imported change IDs in one shared place

The fallback naming rule should be centralized in the shared backlog import path rather than hidden in one adapter-specific edge case. The normalizer should:

- prefer an existing OpenSpec change ID if found in provider metadata
- otherwise derive a kebab-case slug from the proposal title
- append a deterministic suffix such as `-<source-id>` when the slug already exists or would otherwise be ambiguous
- avoid using a raw numeric source ID as the entire change name unless the source artifact truly has no usable title

This lets ADO fix its immediate bug while also protecting similar adapters and commands.

### 3. Audit adjacent import commands, not just the failing ADO branch

The regression appeared because one side of the contract evolved while the other kept assuming a richer payload. The implementation should therefore audit all nearby paths that do one or more of:

- call `fetch_backlog_item()`
- call `extract_change_proposal_data()`
- call `import_backlog_item_as_proposal()`
- translate provider IDs into OpenSpec change IDs

The goal is not a broad refactor. The goal is to prove that similar commands either already preserve the native payload correctly or are covered by targeted defensive tests after this fix.

## Implementation Outline

1. Add failing tests for ADO selective import and title-based slug fallback.
2. Restore native payload preservation in ADO selective fetch.
3. Add shared title-first change-ID normalization in the backlog import path.
4. Re-run targeted tests against adjacent adapters/call sites to confirm the contract holds.
5. Update docs and release notes for the patch behavior change.
45 changes: 45 additions & 0 deletions openspec/changes/bugfix-02-ado-import-payload-slugging/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Change: Fix ADO selective bridge import payload contract and title-based change IDs

## Why

Issue [#425](https://github.com/nold-ai/specfact-cli/issues/425) confirms a real regression in the ADO selective import path used by `specfact project sync bridge --adapter ado --mode bidirectional --backlog-ids <id>`. The current bridge flow calls `fetch_backlog_item()` and then imports the returned artifact as an OpenSpec change proposal, but `AdoAdapter._get_work_item_data()` strips the provider payload down to summary fields and drops the native `fields` object that `AdoAdapter.import_artifact()` and `extract_change_proposal_data()` still expect. Valid ADO work items therefore fail import with `ADO work item must have fields`.

After a local hotfix restores the raw payload, the follow-up behavior is still wrong: when the ADO item does not already contain OpenSpec metadata in its description or comments, the generated change ID falls back to the numeric work item ID instead of a readable slug derived from the title. That makes imported OpenSpec changes hard to review, easy to collide, and inconsistent with the rest of the backlog import workflow.

This change fixes both defects together and explicitly audits adjacent import commands that rely on the same `fetch_backlog_item()` to `import_artifact()` contract so we do not repeat the same summary-vs-native payload mistake in nearby adapters or bridge entry points.

## What Changes

- **MODIFY** `AdoAdapter.fetch_backlog_item()` and its internal work-item fetch helpers so selective ADO import returns the native work item payload, including `fields`, while preserving convenience keys such as `title`, `state`, and `description` for existing callers.
- **MODIFY** the ADO change-proposal extraction path so imported proposals derive a kebab-case change ID from the work item title when no OpenSpec metadata is already embedded in the source artifact.
- **MODIFY** shared backlog import normalization so duplicate or numeric-only fallback IDs get a deterministic suffix that keeps the readable slug and reserves the provider numeric ID for source tracking metadata instead of primary naming.
- **ADD** regression coverage for selective `project sync bridge` / bridge import flows, including raw payload preservation, title-based slug generation, duplicate-slug collision handling, and cross-checks for similar adapter import commands.
- **REVIEW** adjacent bridge/adapters that call `fetch_backlog_item()`, `extract_change_proposal_data()`, or `import_backlog_item_as_proposal()` so similar commands either share the same helper or are covered by explicit contract tests.

## Capabilities

### Modified Capabilities

- `devops-sync`: selective ADO bridge imports must preserve the provider-native work item payload required for OpenSpec proposal import and must create readable change IDs when no prior metadata exists.
- `backlog-adapter`: adapter import contracts must preserve required native fields during single-item import and normalize imported proposal IDs title-first instead of defaulting to raw numeric source IDs.

## Impact

- **Affected code**: `src/specfact_cli/adapters/ado.py`, `src/specfact_cli/adapters/backlog_base.py`, and the selective import orchestration in `src/specfact_cli/sync/bridge_sync.py`; adjacent adapter call sites may need small defensive updates if the audit finds the same contract gap elsewhere.
- **Affected tests**: targeted unit/integration coverage under `tests/unit/adapters/`, `tests/unit/specfact_cli/adapters/`, `tests/unit/specfact_cli/sync/`, and any command-audit coverage that validates bridge command surfaces.
- **Documentation**: user-facing sync and ADO adapter docs in `docs/` plus any command reference examples that show selective bridge import should be reviewed so the fixed import behavior and change-ID expectations are documented.
- **Release impact**: patch release. No new command surface, but behavior changes are user-visible because valid ADO work items will import successfully and produce stable human-readable OpenSpec change IDs.
- **Sequencing**: no hard blocker in `openspec/CHANGE_ORDER.md`; the change should still be linked under backlog feature #357 and epic #186 and back to the originating bug issue.

## Related Issues

- Originating bug report: [#425](https://github.com/nold-ai/specfact-cli/issues/425)
- Parent feature: [#357](https://github.com/nold-ai/specfact-cli/issues/357)
- Parent epic: [#186](https://github.com/nold-ai/specfact-cli/issues/186)

## Source Tracking

- **GitHub Issue**: #427
- **Issue URL**: <https://github.com/nold-ai/specfact-cli/issues/427>
- **Repository**: nold-ai/specfact-cli
- **Last Synced Status**: open
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
## MODIFIED Requirements

### Requirement: BacklogAdapter Interface

The system SHALL provide a standard `BacklogAdapter` interface that all backlog sources (GitHub, ADO, JIRA, GitLab, etc.) must implement.

#### Scenario: Selective proposal import preserves provider-native payload

- **GIVEN** an adapter supports selective backlog import by explicit item reference
- **WHEN** bridge sync fetches one item through `fetch_backlog_item()` and passes the result into proposal import
- **THEN** the fetched artifact preserves the provider-native fields required by `extract_change_proposal_data()` or `import_artifact()`
- **AND** adapter-specific convenience fields may be added without discarding the native structure
- **AND** contract tests cover the `fetch_backlog_item()` to `import_artifact()` round trip for supported adapters

#### Scenario: Imported proposal IDs normalize title-first across adapters

- **GIVEN** an imported backlog artifact has no embedded OpenSpec change ID metadata
- **AND** the source artifact has a usable human-readable title
- **WHEN** the adapter or shared backlog import path constructs the proposal change ID
- **THEN** the change ID is derived from a normalized title slug
- **AND** a numeric provider ID is used only as source tracking metadata or as a deterministic suffix when needed for uniqueness
- **AND** the system does not default to a numeric-only change name while a usable title is available
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
## MODIFIED Requirements

### Requirement: Azure DevOps Backlog Sync Support

The system SHALL support Azure DevOps work items as a backlog adapter in the DevOps sync workflow.

#### Scenario: Selective ADO import preserves native payload for proposal import

- **GIVEN** a user runs `specfact project sync bridge --adapter ado --mode bidirectional --backlog-ids 123456`
- **WHEN** bridge sync fetches that single ADO work item for import as an OpenSpec change proposal
- **THEN** the adapter returns the provider-native work item payload with a populated `fields` object
- **AND** the payload may include convenience keys such as `title`, `state`, or `description` without removing the native `fields` structure
- **AND** proposal import does not fail for a valid work item with `ADO work item must have fields`

#### Scenario: Selective ADO import derives a human-readable change ID when metadata is absent

- **GIVEN** an imported ADO work item has no existing OpenSpec change ID embedded in its description or comments
- **AND** the work item title is `Selective import keeps ADO payload`
- **WHEN** the adapter generates the OpenSpec change proposal during import
- **THEN** the resulting change ID is derived from the title as kebab-case
- **AND** the work item numeric ID remains in source tracking metadata instead of becoming the entire change name

#### Scenario: Duplicate title slug appends deterministic source suffix

- **GIVEN** a title-derived slug already exists in `openspec/changes/`
- **AND** another imported ADO work item with ID `123456` resolves to the same title slug
- **WHEN** the second proposal is created
- **THEN** the final change ID keeps the readable title slug and appends a deterministic suffix such as `-123456`
- **AND** the system does not fall back to using only the raw numeric work item ID as the change name
61 changes: 61 additions & 0 deletions openspec/changes/bugfix-02-ado-import-payload-slugging/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
## 0. GitHub sync

- [x] 0.1 Export or sync this change to a `[Change]` GitHub issue in `nold-ai/specfact-cli`
- [x] 0.2 Update `proposal.md` Source Tracking with the synced issue number, URL, repository, and status
- [x] 0.3 Ensure the synced change issue has labels `enhancement`, `openspec`, and `change-proposal`
- [x] 0.4 Add the synced change issue to the `SpecFact CLI` GitHub project
- [x] 0.5 Link the synced change issue under parent feature `#357` (epic lineage `#186`)
- [x] 0.6 Record the relation back to originating bug `#425`

## 1. Branch and baseline

- [x] 1.1 Create worktree: `scripts/worktree.sh create bugfix/bugfix-02-ado-import-payload-slugging`
- [x] 1.2 Bootstrap Hatch in the worktree: `hatch env create`
- [x] 1.3 Reproduce the current selective ADO import failure and record the failing command/output in `openspec/changes/bugfix-02-ado-import-payload-slugging/TDD_EVIDENCE.md`
- [x] 1.4 Audit adjacent import paths that use `fetch_backlog_item()`, `extract_change_proposal_data()`, or `import_backlog_item_as_proposal()` and record the scoped call sites before code changes

## 2. Write failing tests from spec scenarios

- [x] 2.1 Add unit tests in `tests/unit/adapters/test_ado.py` proving selective `fetch_backlog_item()` preserves the native ADO payload with populated `fields`
- [x] 2.2 Add unit tests in `tests/unit/adapters/test_ado.py` proving imported ADO change IDs derive from title slugs when no OpenSpec metadata exists
- [x] 2.3 Add collision tests proving duplicate title slugs append a deterministic source-ID suffix instead of degrading to numeric-only names
- [x] 2.4 Add bridge/import contract tests in the selective import path (`tests/unit/specfact_cli/sync/` or adjacent bridge tests) proving `fetch_backlog_item()` output remains valid input for proposal import
- [x] 2.5 Add or extend audit coverage for similar adapter commands so nearby `fetch_backlog_item()` implementations are checked for the same contract assumption
- [x] 2.6 Run targeted tests and capture the failing results in `TDD_EVIDENCE.md`

## 3. Implement payload preservation and title-first slugging

- [x] 3.1 Update `src/specfact_cli/adapters/ado.py` so selective ADO fetch returns the native work item payload while preserving compatibility summary keys
- [x] 3.2 Add shared title-first change-ID normalization in `src/specfact_cli/adapters/backlog_base.py` or a nearby shared helper used by proposal import
- [x] 3.3 Update ADO proposal extraction/import flow to use the shared normalizer and keep numeric source IDs in source tracking instead of primary naming
- [x] 3.4 Patch any adjacent adapter or bridge call sites found in the audit where the same summary-vs-native payload mistake or numeric-only fallback can occur
- [x] 3.5 Improve diagnostics or guard rails so missing native payload structure is reported clearly if an adapter violates the import contract in future

## 4. Verify and evidence

- [x] 4.1 Re-run the targeted adapter and bridge tests; record passing results in `TDD_EVIDENCE.md`
- [x] 4.2 Run `hatch run format`
- [x] 4.3 Run `hatch run type-check`
- [ ] 4.4 Run `hatch run lint`
- [x] 4.5 Run `hatch run yaml-lint`
- [x] 4.6 Run `hatch run contract-test`
- [x] 4.7 Run `hatch run smart-test`

## 5. Documentation research and update

- [x] 5.1 Review affected docs in `docs/`, `README.md`, and command references for selective bridge import and ADO adapter behavior
- [x] 5.2 Update the relevant ADO sync/import documentation to describe the corrected selective import behavior and title-based change-ID fallback
- [ ] 5.3 If new or moved docs are required, verify front matter and sidebar navigation entries in `docs/_layouts/default.html`

## 6. Module signing, version, and changelog

- [ ] 6.1 Run `hatch run ./scripts/verify-modules-signature.py --require-signature`
- [ ] 6.2 If any module manifests changed, bump module versions and re-sign before re-running verification
- [ ] 6.3 Bump patch version across the required version files
- [ ] 6.4 Add a `CHANGELOG.md` entry for the bugfix release describing the ADO import contract fix and title-based slugging correction

## 7. PR and cleanup

- [ ] 7.1 Open a PR from `bugfix/bugfix-02-ado-import-payload-slugging` to `dev`
- [ ] 7.2 Ensure CI passes and the PR links both the synced change issue and bug `#425`
- [ ] 7.3 After merge, remove the worktree and delete the local branch
Loading
Loading