Repository navigation
feat(cmdb-import): read the archive sheet and let module and usage be archived - #1285
WilcoLouwerse wants to merge 6 commits into
Conversation
… archived Adds the OpenSpec change cmdb-import-archive-reconciliation (REQ-CMDB-015..017, REQ-CMDB-012 and REQ-CMDB-014 modified). The register fragment opts module (0.3.9) and usage (1.5.7) into OpenRegister's archive state, the import profile names the sheet "Gearchiveerde Applicaties" and the accepted missingRecords modes, and the workbook reader returns the APPIDs of that sheet, read under the same bounds as the CMDB sheets for its key column only. The archived-applications fixture covers it. First of three slices of #1281; nothing reads the archive sheet yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Quality Report — ConductionNL/stackiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| check-vue-demi | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| check-l10n-js | ✅ | ||||
| composer | ✅ | ✅ 133/133 | |||
| npm | ✅ | ✅ 807/807 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| lockfile sync | ✅ | ||||
| PHPUnit | ✅ 1233/1233 · 20 skipped | ||||
| Newman | ⏭️ | ||||
| Playwright | ⏭️ deferred: E2E runs locally and on the promotion path only. This pull request targets development, so the suite is asked once per promotion into beta and main rather than once per push per open pull request. Run it locally with npx playwright test, or from the Actions tab on a branch with no open pull request into development. |
||||
| Hydra gates | ✅ |
Quality workflow — 2026-10-12 10:38 UTC
Download the full PDF report from the workflow artifacts.
…rt-import spec The fragment test was renamed to testTheMergedModuleIsVersion039WithTheExternalIds when the module schema moved to 0.3.9; the live spec still named the 038 test. The archived change keeps its historical name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| - GIVEN OpenRegister's search results (no archive marker; no soft-deleted row, `_includeDeleted` or not) THEN the state comes from the lens that found the object and from the trash listing, read once per import (design D9) | ||
| - GIVEN `updateExisting` false THEN the row is skipped as `exists` and nothing is revived | ||
| - GIVEN a deleted and a working module (and usage) sharing one import key THEN the working one is updated and nothing is restored | ||
| - [x] Implement |
There was a problem hiding this comment.
🟢 Minor — tasks.md ticks tasks whose code is in #1286/#1287
🟡 Concern — tasks 3 to 7 are marked implemented and tested in a PR that contains none of that code
tasks.md ticks [x] Implement and [x] Test for Task 3 (matching revives, CmdbExportImportService.php, CmdbImportReport.php), Task 4 (reconciliation), Task 5 (controller option), Task 6 (settings section) and Task 7 (fixture, docs; only its Test is open). This diff touches none of those files, and the PR body says "nothing acts on it yet". Once this merges to development, anything that reads the checkboxes (/opsx-verify, /opsx-archive, a reviewer) sees a change that is nearly done.
Impact: the change's progress record is false on development between the merges of the three slices.
Suggested fix: leave Tasks 3 to 7 unticked here; tick each in the slice that implements it. Task 1 and Task 2 stay ticked.
Done when:
grep -c '^- \[x\]' openspec/changes/cmdb-import-archive-reconciliation/tasks.mdreturns 4 on this headgit diff origin/development --stat -- lib/Service/CmdbExportImportService.php lib/Controlleris empty, matching the unticked tasks
Split artefact: this slice was cut from a larger PR; the item resolves once the stack above it merges. Fix only if this slice may merge on its own for a while.
| path: $path, | ||
| limit: $profile->maxReferencedStringBytes(), | ||
| sheetCount: count($profile->sheetNames()), | ||
| sheetCount: count($profile->readSheetNames()), |
There was a problem hiding this comment.
🟢 Minor — Referenced-string budget shrinks for workbooks without the archive sheet
🟢 Minor — count($profile->readSheetNames()) always counts the archive sheet, also when the workbook has none
assertReferencedStringBytes divides maxReferencedStringBytes by sheetCount (CmdbWorkbookBounds.php:207). The count goes from 2 to 3 for every workbook, so a workbook without the archive sheet now gets limit/3 (about 21 MiB of the 64 MiB default) instead of limit/2 (32 MiB). At the default limit this will not matter for a 10 MB upload, but it is a tightening nobody decided, the @param of assertReferencedStringBytes still says "the number of source sheets the profile reads", and the sheet list is only known after this call. Counting the sheets actually read would need sourceSheets() to run first. Optional: leave as is and say so in the docblock.
| limit: $limit | ||
| ); | ||
| $archive['present'] = true; | ||
| $archive['appIds'] = array_column(array_column($archiveRows, 'cells'), $profile->keyColumn()); |
There was a problem hiding this comment.
🟢 Minor — appIds are raw cell values, not match keys
🟢 Minor — archive.appIds holds raw typed cell values; say so, or normalise
Read-only probe: cells 7.0, ' 8 ', 'app-9' come back as 7 (int), ' 8 ' and 'app-9'. The source rows go through the normaliser and matchKey() (trim, non-breaking space, lower case; winningSheets() does exactly that), so #1286 must apply the same to these or APP-1 on the archive sheet will not match app-1 on the CMDB sheet. The new tests hide the types behind array_map('strval', ...). The read() @return and design D3 do not say the values are raw (D3 also omits the sheet key the array carries).
Suggested fix: state in the @return that appIds are raw cell values to be compared through the normaliser and CmdbExportImportService::matchKey(), and assert the raw types in one test.
| "module": ["publicationDate", "depublicationDate"] | ||
| }, | ||
| "missingRecords": ["keep"] | ||
| "missingRecords": ["keep", "archive"] |
There was a problem hiding this comment.
🟢 Minor — Profile accepts 'archive' while the endpoint still refuses it
🟢 Minor — heads-up: until #1286 merges, the profile and the endpoint disagree
The profile now lists ["keep", "archive"] and missingRecordsModes() reads it, but nothing in lib/ calls missingRecordsModes() on this head (only the unit test does): CmdbExportImportService::supportsMissingRecords() is still $mode === 'keep' and CmdbImportController still answers details.accepted: ['keep']. This matches "nothing reads it yet" for the reader, but the profile is the declared source of the accepted values (spec REQ-CMDB-012, MODIFIED here), and for a release cut between the slices the declared value is one the endpoint refuses. Fine if the three slices always ship together; if not, say so in the PR stack note.
Split artefact: this slice was cut from a larger PR; the item resolves once the stack above it merges. Fix only if this slice may merge on its own for a while.
There was a problem hiding this comment.
Verdict: APPROVE (Thorough) — self-review posted as COMMENT (GitHub blocks self-APPROVE)
No blockers: the archive sheet is read under the CMDB sheets' bounds and the readBounded() refactor returns the same archive array in all three branches. The concerns are about silent gaps (uncached formulas, untested bounds) that matter once #1286 acts on these APPIDs.
Update 2026-10-12: All three concerns were fixed on the branch after posting; the four minors stay open as follow-ups.
🟡 Concerns (3)
- Uncached archive-sheet formulas vanish without a warning —
lib/Service/Cmdb/CmdbWorkbookReader.php:366. 🟡 Concern — the archive sheet drops a formula without a cached value silently, and reportspresent: truewith fewer APPIDs The archive sheet is formula-derived: the new fixture's APPID cell is<f>..</f><v>1234</v>(build-fixtures.py,archive_appid). ✅ Addressed in16337d91. - Archive sheet bounds now apply to imports that keep —
lib/Service/Cmdb/CmdbWorkbookReader.php:187. 🟡 Concern — an export that imports today can be refused because of a sheet the admin chose not to act onread()takes no mode, so from this slice on every import, includingmissingRecords=keep, loads the archive sheet and enforces on it the part-size bound (here), the row span, the referenced-string budget andTOO_MANY_ROWS. ✅ Addressed incdbec3d3 (#1287). - 'Same bounds as the CMDB sheets' is tested for one of five bounds —
tests/Unit/Service/Cmdb/CmdbWorkbookReaderTest.php:1005. 🟡 Concern — the totality promise "the archive sheet is read under the same bounds as the CMDB sheets, APPID column only" has one test REQ-CMDB-015, design D3 and the PR body promise that. ✅ Addressed in11d9a23c.
🟢 Minor (4)
- tasks.md ticks tasks whose code is in #1286/#1287 —
openspec/changes/cmdb-import-archive-reconciliation/tasks.md:37. 🟡 Concern — tasks 3 to 7 are marked implemented and tested in a PR that contains none of that codetasks.mdticks[x] Implementand[x] Testfor Task 3 (matching revives,CmdbExportImportService.php,CmdbImportReport.php), Task 4 (reconciliation), Task 5 (controller option), Task 6 (settings section) and Task 7 (fixture, docs; only its Test is open). - Referenced-string budget shrinks for workbooks without the archive sheet —
lib/Service/Cmdb/CmdbWorkbookReader.php:313. 🟢 Minor —count($profile->readSheetNames())always counts the archive sheet, also when the workbook has noneassertReferencedStringBytesdividesmaxReferencedStringBytesbysheetCount(CmdbWorkbookBounds.php:207). - appIds are raw cell values, not match keys —
lib/Service/Cmdb/CmdbWorkbookReader.php:366. 🟢 Minor —archive.appIdsholds raw typed cell values; say so, or normalise Read-only probe: cells7.0,' 8 ','app-9'come back as7(int),' 8 'and'app-9'. - Profile accepts 'archive' while the endpoint still refuses it —
lib/Settings/cmdb-import/topdesk-profile.json:48. 🟢 Minor — heads-up: until #1286 merges, the profile and the endpoint disagree The profile now lists["keep", "archive"]andmissingRecordsModes()reads it, but nothing inlib/callsmissingRecordsModes()on this head (only the unit test does):CmdbExportImportService::supportsMissingRecords()is still$mode === 'keep'andCmdbImportControllerstill answersdetails.accepted: ['keep'].
✅ Fixed before posting (1)
- Renamed test is still named by the live spec — fixed in
388181a.
✅ Verified clean
readBounded()refactor: the returnedarchivearray matches the pre-refactor version (softwarecatalog141ea249) for no sheet, a sheet without the key column and a sheet with it — checked by reading and a read-only probe.- Schema bumps module 0.3.9 / usage 1.5.7 follow the fragment-version convention and are pinned by
TopdeskCmdbFragmentTestandPublicationFieldRulesTest. x-openregister-archivematches OpenRegister'sSchema::ARCHIVE_ANNOTATION.
Scope decision: slice 1 of 3 of the closed #1281, stacked on development; reviewed against its own base only.
Gates: CI quality / Hydra Gates and the required PHP/Vue quality checks green on {h}; quality / PHPUnit was still running at submission.
ADRs verified: ADR-005, ADR-031, ADR-032, ADR-037 — all resolve on hydra main.
Jira context: WOO-587 — CMDB-import: gearchiveerde en verdwenen applicaties bij herimport (status: Refinement)
Quality Report — ConductionNL/stackiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| check-vue-demi | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| check-l10n-js | ✅ | ||||
| composer | ✅ | ✅ 133/133 | |||
| npm | ✅ | ✅ 807/807 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| lockfile sync | ✅ | ||||
| PHPUnit | ✅ 1233/1233 · 20 skipped | ||||
| Newman | ⏭️ | ||||
| Playwright | ⏭️ deferred: E2E runs locally and on the promotion path only. This pull request targets development, so the suite is asked once per promotion into beta and main rather than once per push per open pull request. Run it locally with npx playwright test, or from the Actions tab on a branch with no open pull request into development. |
||||
| Hydra gates | ✅ |
Quality workflow — 2026-10-12 12:53 UTC
Download the full PDF report from the workflow artifacts.
The archive sheet is formula-derived. A row whose APPID formula carries no cached value read as an empty row and was skipped without trace, so the sheet reported present: true with one APPID fewer. The reconciliation would then see that application on no sheet and soft-delete it instead of archiving it, with nothing in the report saying why. REQ-CMDB-002 already treats such a cell on the CMDB sheets as empty plus a row warning. readRows() now hands back the numbers of rows that read as empty only because a formula in them is uncached, and the reader returns them as archive.uncachedRows. The spec delta (REQ-CMDB-015, new scenario), design D3/D5 and task 2 state the rule: such a sheet is incomplete, so the import warns naming the sheet and rows and archives every missing application, deleting none. The service side follows in the stacked PR. Review #1285 f1 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
REQ-CMDB-015 and design D3 promise that the archive sheet is read under the same bounds as the CMDB sheets, for its key column only, but only the row limit had a test. The part size, the row span, the referenced-string budget, the key-only read filter and the null cases of archiveSheetName() could regress unnoticed; swapping readSheetNames() for sheetNames() in read() left the reader tests green, which would blank an oversized archive sheet instead of refusing it. Adds one test per bound. RecordingXlsxReader now also keeps the read filter of each load, so the key-only claim is asserted on what the data pass is allowed to read, not on the returned array. Each test was checked by mutating the reader (archive sheet left out of that bound) and goes red. Review #1285 f5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The rule "archive, never soft-delete" was written as "when the workbook has no archive sheet" in the design risk list, the proposal summary and Risk 2, and in task 4. It also applies when the sheet has no APPID column or holds an APPID the reader cannot see (a formula without a cached value). Review pre-push p1 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
D8 said the facet narrowing's _ids composes with the Archived quick filter. The Archived tab of the Applications page overrides _ids with an empty value, so it lists the whole archive and ignores facets and search. Review pre-push p2 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ry import The docs said the import "ignores all other sheets" and that a sheet it does not read may exceed the part size limit. The archive sheet is read (its APPID column only) in every import, also with Keep, so it counts against the row span and part-size limits like a CMDB sheet and can refuse a keep import. The file section, the row limit, the part-size limit and the WORKBOOK_TOO_LARGE row now say so, and that deleting the sheet from a copy makes the import archive and never delete. Review #1285 f2 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Quality Report — ConductionNL/stackiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| check-vue-demi | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| check-l10n-js | ✅ | ||||
| composer | ✅ | ✅ 133/133 | |||
| npm | ✅ | ✅ 807/807 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| lockfile sync | ✅ | ||||
| PHPUnit | ✅ 1239/1239 · 20 skipped | ||||
| Newman | ⏭️ | ||||
| Playwright | ⏭️ deferred: E2E runs locally and on the promotion path only. This pull request targets development, so the suite is asked once per promotion into beta and main rather than once per push per open pull request. Run it locally with npx playwright test, or from the Actions tab on a branch with no open pull request into development. |
||||
| Hydra gates | ✅ |
Quality workflow — 2026-10-12 16:18 UTC
Download the full PDF report from the workflow artifacts.
What
First of three stacked PRs that replace #1281 (archive or soft-delete applications that left a newer CMDB export). This slice reads the archive sheet; nothing acts on it yet.
cmdb-import-archive-reconciliation: ADDED REQ-CMDB-015..017, MODIFIED REQ-CMDB-012 and REQ-CMDB-014. The design records why object state is read per lens and from the trash (D9).module(0.3.9) andusage(1.5.7) opt in to archiving throughx-openregister-archive. Run Force Update after installing.missingRecordsmodes (keep,archive), readable before the packs are validated.topdesk-archived-applications.xlsx, a later export in which APPID 1234 moved to the archive sheet and APPID 2 is on no sheet.Stack
developmentJira: WOO-587 (sub-task of WOO-586). Plan: WOO-587 plan. Review and merge are tracked in WOO-590.
Review fixes (2026-10-12)
388181a316337d91uncachedRows), not dropped; the spec delta, design D3/D5 and task 2 say what follows (archive, never delete — #1286).11d9a23carchiveSheetName()null cases), each mutation-checked.aa3b6648,70d6f934Tests
🤖 Generated with Claude Code