docs(content-drive): spec and design for Status filter (Archived, Unpublished, Locked) (#37066) - #37170
docs(content-drive): spec and design for Status filter (Archived, Unpublished, Locked) (#37066)#37170zJaaal wants to merge 10 commits into
Conversation
Spec-Kit PR 1 for #37066: the Archived / Unpublished / Locked filter on drive search and in the Content Drive toolbar. Carries spec.md alone, per the two-PR spec-driven flow. The plan artifacts land in the next PR of the stack, based on this branch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…37066) Phase 1 design artifacts for the Status filter, stacked on the spec PR. Carries only the two artifacts .specify/CUSTOMIZATIONS.md considers durable: data-model.md (the ContentStatus enum, the `status` transport shape, the filter-bag entry) and contracts/ (the `status` field on POST /api/v1/drive/search, including the AND semantics and the 400 on an unknown value). plan.md, research.md and quickstart.md stay local by design — the repo gitignores them as process-only artifacts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Claude finished @zJaaal's task in 31s —— View job Claude finished @zJaaal's task in 2m 3s —— View job SDK Compatibility Analysis — Complete ✅
Verdict: No SDK breaking change. This PR is documentation-only — the entire diff is three new Markdown files under There is no change to any of the surfaces the breaking-change categories cover:
No comment or label added, consistent with the "no action when not breaking" instruction. |
Restores stock Spec-Kit behavior for the three artifacts a reviewer needs to judge a design. Only tasks.md and checklists/ stay gitignored. The original policy in #36416 kept all five local on the "process artifact" test. That predates this repo adopting GitHub's native stacked pull requests (public preview 2026-07-30), which changes the calculation: the spec-driven flow now lands as a stack whose middle layer exists purely to review the design. Gitignoring plan.md makes that layer a PR with nothing to review. tasks.md and checklists/ stay ignored — both are regenerated by their own command, and a merged task list that no longer matches what was built is worse than no task list. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…37066) Now trackable after the preceding gitignore change. These are the artifacts /speckit-plan already generated for this feature; they were sitting on disk, invisible to review. - plan.md design, Legacy Impact, Constitution Check, ADR Alignment gate - research.md R1-R10, each verified against the code rather than the issue text - quickstart.md build/test commands and the end-to-end validation walkthrough Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…7066) Two non-blocking notes from the automated review on #37170, both verified against the code before acting: 1. FR-004 vs FR-006 read as a contradiction to a strict "pure AND of three independent flags" implementer. They are not: excluding archived content is the drive's pre-existing baseline (appendExcludeArchivedQuery runs on every request today), the statuses AND on top of it, and ARCHIVED is the only one that lifts it. data-model.md now spells this out with a per-selection predicate table, and contracts/ points at it. 2. LOCKED and version scoping compose, as the reviewer suspected. Confirmed in the legacy portlet: ContentletAjax.java:1018 appends +locked:true and :1035 unconditionally appends +working:true. Documented, including the one deliberate difference — legacy always scopes to working, whereas here only ARCHIVED and UNPUBLISHED force it, so live:true + LOCKED is a coherent "live content that is locked" query rather than a bug. Adds the two integration cases the review asked for, including the fixture note that the FR-004 assertion needs an item that is both archived and unpublished or it passes vacuously. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
I added this to the spec PR by mistake. It is not a spec artifact: it is a per-developer pointer to the feature you are currently on, rewritten by /speckit-specify and read by get_feature_paths() so the downstream commands can locate the spec folder without depending on branch naming. Committing it would aim every teammate's next /speckit-plan at whatever feature last merged, and two concurrent feature branches would conflict on its single line every time. It has never been committed on main — the 36605 and 36834 features both shipped without it — but it was not gitignored either, so it was untracked by luck rather than by rule. Now it is a rule. Callers who want to target a feature explicitly should set SPECIFY_FEATURE_DIRECTORY, which takes precedence over the file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Both 🟡 notes addressed in 1. FR-004 vs FR-006 — right that a strict reader would get this wrong. They're not contradictory, but the spec never said why. Excluding archived content is the drive's pre-existing baseline ( 2. Both now have integration cases in the plan's Test Strategy. The FR-004 one carries a fixture note: it needs an item that is both archived and unpublished, or the assertion passes vacuously. 3. On Also confirmed the Analysis and reply by Claude (Claude Code), posted from @zJaaal's account. |
Reverses the ticket's original AND semantics after review. The case for AND was that these are independent flags one item can hold at once, so intersecting them is meaningful. It is — for one of four combinations. Under AND, ARCHIVED+UNPUBLISHED is redundant (archiving removes the live version, so every archived item is already unpublished), ARCHIVED+LOCKED is almost always empty, and all three is empty in practice. Only UNPUBLISHED+LOCKED says anything. Under OR all four are meaningful. The decisive point is consistency: every other chip in that toolbar row widens on selection, and no UI affordance can convey that one of them inverts the rule. A user who checks a second box and gets fewer results reads that as a bug. The other filters' OR-ness being forced by their single-valued nature is invisible to the user; all they learn is the pattern. Accepted cost, recorded in Assumptions: "unpublished AND locked" is no longer expressible in Content Drive. Implementation shape changes from N independent `and` clauses to one OR-ed group AND-ed against the archived baseline, which stays OUTSIDE the group — folding it in would make [UNPUBLISHED, LOCKED] read `(deleted = false or ...)` and match nearly every row. Also settles the toolbar position (between the shared-assets and content-type filters, so the row reads broadest-scope-first) and makes the navigation requirement explicit: status persists across deep link, reload, folder browsing, Back/Forward and an editor round-trip, exactly like every other filter, because it rides the shared filters bag rather than its own query param. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Restores the commit policy from #36416: plan.md, research.md and quickstart.md are process artifacts and stay local. They remain on disk, so /speckit-implement and /speckit-converge are unaffected. I proposed un-ignoring them because a stacked design PR with a gitignored plan.md has nothing in it to review. That was solved differently instead — the spec and its design artifacts are now one PR, so there is no design layer needing its own diff. Keeps two things from the reverted change: - .specify/feature.json stays gitignored. It is a per-developer pointer, not a spec artifact, and had never been committed on main — untracked by luck rather than by rule. - data-model.md no longer links to the now-local research.md; each reference is summarized inline so the committed document stands on its own. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ea2d38c to
c829fec
Compare
Review note on #37170: the design rationale cited research.md items (R2, R4, R7), but research.md is gitignored by the commit policy this PR reaffirms, so a reviewer could not verify the load-bearing claims. The R-numbers read as citations to something unreviewable. Fixed both ways the review suggested: the reasoning is now inlined in full and the R-references are gone, so nothing in the committed docs points outside the diff. The showWorking passage gained the most: it now names the two call sites that make the flag load-bearing (BrowserAPIImpl:1947 picking the joined inode column, :615 choosing +working:true vs +live:true), says why the join can never match for archived/unpublished rows, and notes that the drive path is safe today only by coincidence of live() defaulting to false — plus that LOCKED alone does not need the flag, since a locked item may have a live version. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Thanks — one real finding, and two that were made against a stale snapshot of the PR description. 🟡 R-references pointing at a gitignored file — valid, fixed in The 🟡 "PR description vs committed spec — stale AND/OR narrative" — already fixed before the review ran. The body was rewritten at 14:32 UTC and no longer contains the "The one thing to check in the spec" section you quote. It now leads with To answer the substantive half anyway, since it deserves a direct answer: yes, OR is deliberate, and yes it flips the ticket's original requirement. That AND wording was my own earlier rewrite of #37066, not a team decision, and it did not survive planning — under AND, two of three pairs are degenerate and the first draft of the spec had to document them as "expected, not a bug," which was the design objecting. The decisive point is that every other chip in that toolbar row widens on selection and nothing in the UI can convey that one of them inverts the rule. #37066's body and acceptance criteria are now updated to match, with a comment recording why, so the ticket and the spec no longer diverge. The Minor — "PR body still lists plan.md/research.md/quickstart.md under Proposed Changes" — same stale snapshot. Proposed Changes lists only the four committed items; the three process artifacts appear in one line stating they stay local per #36416. Analysis and reply by Claude (Claude Code), posted from @zJaaal's account. |
Comply with the existing policy rather than amending it. This PR now touches no policy files at all — .gitignore and .specify/CUSTOMIZATIONS.md are identical to main. feature.json stays out of the commit (it was never tracked on main, and 36605 and 36834 both shipped without it). It is simply untracked rather than ignored, which is exactly main's state today. If the team wants it ignored, that belongs in its own PR against the policy, not carried along inside a feature branch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Spec-Kit PR 1 of 2. Carries the spec and the two design artifacts that clear the commit-policy bar in
.specify/CUSTOMIZATIONS.md. Needs a developer approval (not a merge) — the implementation PR stacks on this branch rather than waiting for it to land onmain.Resolves the spec + plan phases of #37066.
Proposed Changes
spec.md— the reviewed contract: 5 prioritized user stories, 20 functional requirements, 7 success criteria, edge cases, and the dotCMS Legacy Considerations section.data-model.md— theContentStatusenum and its column/index mapping, thestatustransport shape and validation rules, the per-selection predicate table, theBrowserQueryfield, the frontend filter-bag entry.contracts/drive-search-status.md— thestatusfield onPOST /api/v1/drive/search.plan.md,research.mdandquickstart.mdwere generated and stay local, per the commit policy in #36416. The two committed design docs are written to stand on their own — no citations point at files outside this diff.The ticket originally specified these statuses combine with AND. This spec says OR — selecting more statuses returns more content, exactly like the Content Type and Language filters beside it. #37066's body and acceptance criteria have been updated to match, with a comment recording why, so the ticket and the spec do not diverge.
The case for AND was that these are independent flags one item can hold at once, so intersecting them is meaningful. It is — for one of four combinations:
Archived + UnpublishedArchivedalone, since archiving removes the live versionArchived + LockedUnpublished + LockedThe first draft of this spec had to document two of those as "expected, not a bug" — that disclaimer was the design objecting. The decisive point is consistency: every other chip in that toolbar row widens on selection, and nothing in the UI can convey that one of them inverts the rule. A user who checks a second box and gets fewer results reads that as broken. The other filters being OR is forced by their single-valued nature (an item has exactly one base type), but that reasoning is invisible to the user; all they learn is the pattern.
The thing to sign off on deliberately: "unpublished and locked" — drafts currently checked out — is no longer expressible anywhere in Content Drive. Called out in
spec.mdAssumptions rather than absorbed quietly. If it turns out to matter, it belongs in a later refinement that makes the combining rule explicit in the UI.Design decisions worth a reviewer's eye
1. All three predicates resolve in the database. ADR-0018 routes version-info flags (archived/deleted) to the DB and says structural criteria must never be re-routed to the index for speed. All three statuses are version-info flags:
ARCHIVEDcvi.deleted = truedeleted:trueUNPUBLISHEDcvi.live_inode is nulllive:falseLOCKEDcvi.locked_by is not nulllocked:trueThe index terms exist only because
BROWSE_API_HEURISTIC_TYPE=PURE_ESis a supported (non-default) configuration where the filter would otherwise silently no-op, returning a wider set than asked for. Under the default hybrid heuristic the SQL clauses apply with and without a keyword.2. The archived baseline sits OUTSIDE the OR group. Excluding archived content is the drive's pre-existing default, not a fourth status. The selected statuses are OR-ed together and that group is AND-ed against the baseline, which only
ARCHIVEDlifts:Folding the baseline into the group is the bug to avoid —
[UNPUBLISHED, LOCKED]would become(deleted = false or …)and match nearly every row: a filter that silently stops filtering.3.
statusis typedList<String>, notList<ContentStatus>. FR-010 wants a 400 on an invalid value "consistent with howuserSearchablerejects unknown keys", and that precedent is an explicitBadRequestExceptioninContentDriveHelper. A typed field would let the Immutables/Jackson layer reject it as anInvalidFormatExceptionwhose mapping to a useful 400 is not under this code's control. The typed field would read better; the deterministic error is worth more.4. No schema or mapping change.
deleted,live_inodeandlocked_byalready exist oncontentlet_version_info(postgres.sql:550-552);lockedis already indexed (ESMappingAPIImpl.java:527). Nothing here is rollback-unsafe.5. The existing
showArchivedis left alone. It is inclusive — archived content plus everything else — and the legacy Site Browser checkbox (view_browser.jsp:145) depends on that. The exclusive Archived behavior is added alongside (FR-008), with an integration assertion guarding the old one.The risk this design carries into PR 2
Two existing code paths already have an opinion about
cvi.deleted, and both must learn aboutARCHIVEDor the filter breaks:BrowserAPIImpl:2006) would otherwise emitcvi.deleted = falseAND-ed against a group containing= true, soARCHIVEDwould always return nothing.:1980) —appendWorkflowQueryownscvi.deletedper branch when such a step is selected, soARCHIVEDneeds the same treatmentshowArchivedalready gets.ContentDriveWorkflowArchiveStepTestis the regression guard.A third, quieter one:
BrowserQuery'sshowWorkingderivation (:151) must also be true forARCHIVED/UNPUBLISHED, orselectQueryjoins onlive_inode— which those rows never have — and the filter returns nothing with no error. Seedata-model.md.With no
statussent, every generated query stays byte-identical to today.UI placement and navigation
The chip sits between the shared-assets and content-type filters, so the row reads broadest-scope-first: which assets are in play → which state of them → what kind → where in the workflow → which locale. The selection rides the shared
filtersbag rather than its own query param, so it inherits deep link, reload, folder browsing, Back/Forward and the legacy-editor round-trip identically to every other filter (FR-016).Checklist
PURE_ESparity and the workflow archive-step regression; Jest/Spectator for the chip, the store payload and the URL round-trip (including the single-value decode case, where a lonestatus:ARCHIVEDwould otherwise decode as a string rather than an array)content-drive.status-filter.*keys, in PR 2Additional Info
Absorbs #37067 (closed, merged into #37066). Parent epic #33999. Shared Assets / System Host toggle out of scope (#34760). #37171 was an earlier split of the design into its own stack layer, folded back here.
/speckit-adr-contextran as the mandatorybefore_planhook. ADR-0018 (proposed) governs and this design complies; ADR-0009 (accepted) constrains only the index terms, all core fields mapped identically under ES and OpenSearch; ADR-0020 surfaced but does not apply. No new ADR proposed.🤖 Generated with Claude Code
This PR fixes: #37066