Skip to content

fix(spec): repoint 11 dangling @spec anchors onto real file-actions headings (gate-46) - #2355

Open
rubenvdlinde wants to merge 3 commits into
developmentfrom
fix/spec-anchors-file-actions
Open

fix(spec): repoint 11 dangling @spec anchors onto real file-actions headings (gate-46)#2355
rubenvdlinde wants to merge 3 commits into
developmentfrom
fix/spec-anchors-file-actions

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

Replaces #2345, which became unmergeable after the fleet licence normalisation landed on development.

Why a new PR

#2345 bundled two things: 30 AGPL→EUPL licence headers (gate-28) and 11 dangling @spec anchors (gate-46). Between it being opened and now, chore(license): normalise licence declarations to EUPL-1.2 landed on development and did the licence half independently.

Measured before opening this:

update-branch on #2345 returns HTTP 422 merge conflict between base and head, and the branch is ahead=11 behind=7 diverged. Rebuilding it in place kept conflicting, so this branch is cut fresh from development@f98fcb4 and carries only the surviving work.

What this changes

file change
openspec/specs/file-actions/spec.md splits two catch-all requirements into five named for the behaviour they contract
lib/Service/FileService.php repoints 10 dangling @spec anchors
lib/Service/File/DocumentProcessingHandler.php repoints 1 anchor out of openspec/changes/ onto the canonical openspec/specs/ home

No licence header is touched — development already owns that.

Evidence

Anchors resolved against the spec's own headings (Requirement: prefix stripped, matching the gate):

OK    file-creation-and-upsert-run-a-fixed-validate-write-own-tag-pipeline
OK    file-retrieval-resolves-by-id-or-name-and-projects-nodes-to-metadata
OK    file-update-and-delete-enforce-per-action-node-permissions
OK    file-update-guards-locks-preserves-object-tags-and-persists-or-side-metadata-separately
OK    object-and-register-folder-provisioning

NEGATIVE CONTROL — the anchor being replaced:
MISS  file-crud-operations-on-objects

The negative control is the point: it shows the check can fail, and that the old anchors were genuinely dangling rather than the checker being permissive.

The two PHP files were rebuilt from development's current content with only the anchor lines re-applied, so their diff is anchors and nothing else. php -l clean on both. The resulting anchor multiset is identical to the one #2345 intended — verified by diffing the sorted anchor lists.

Replaces the two catch-all headings ("Object register folder management",
"File CRUD operations on objects", "Object tagging via Nextcloud system tags")
with requirements named for the behaviour they actually contract, so the @SPEC
anchors in FileService can point at something that exists and stays meaningful.
…al spec (gate-46)

The old anchor pointed into openspec/changes/, which is a change dir, not the
canonical openspec/specs/ home.
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/openregister @ 289b0fc

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-specs
test-l10n
composer ✅ 173/173
npm ✅ 713/713
PHPUnit
Newman
Playwright
Hydra gates

Quality workflow — 2026-08-05 20:20 UTC

Download the full PDF report from the workflow artifacts.

@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

Blocked by gate-47, which I believe is a false positive here — flagging rather than working around it.

Everything else is green (25 SUCCESS, E2E Tests (Playwright) passing after a real 221s run). The single gate failure is:

[hydra-gates] Scope: diff vs origin/development — 3 changed file(s)
[gate-47] security-change-has-tests: FAIL — 2 security-touching change(s) without a test co-change

This PR changes no code. Measured on the diff: all 30 changed lines under lib/ are docblock comment lines — the @spec anchor strings and their continuation lines. Filtering the diff for any added/removed line that is not a * docblock line returns nothing:

$ gh pr diff 2355 | awk '/^diff --git a\/lib/,/^diff --git a\/openspec/' \
    | grep -E "^[+-]" | grep -vE "^(\+\+\+|---)" | grep -vE "^\s*[+-]\s*\*"
(empty)

The third file is openspec/specs/file-actions/spec.md, markdown.

So gate-47 is asking for a test co-change to accompany an edit that changed only a comment in a file it classifies as security-touching. There is no behaviour here to test — writing a test to satisfy it would be writing a test for nothing, and I am not going to add a suppression, a baseline entry, or flip require-full-coverage to get past it. All of those turn a gate that is wrong once into a gate that is silent always.

Suggested fix for the gate owner: gate-47 should exclude diffs whose changed lines in the file are entirely comment/docblock. This is the same shape of problem as the other gate-advice hazards seen tonight — the gate's remedy, applied literally, makes the codebase worse rather than better.

The PR itself is sound and its own evidence is in the description: all five repointed anchors resolve against the spec's headings, and the anchor being replaced (#file-crud-operations-on-objects) does not resolve — a negative control proving the anchors were genuinely dangling.

Needs a human call on gate-47 before this can land.

rubenvdlinde added a commit that referenced this pull request Aug 5, 2026
Hydra gate-46 (spec-anchor-existence) failed on this PR with 6 unresolved
targets. All six are pre-existing debt in files this PR already touches, which
is what pulled them into the gate's ADR-020 diff scope; none was introduced
here. 62 of the other gates passed and coverage was 60 of 60 applicable, so
this was a single real failure, not a broken run.

lib/Service/VocabularyImportService.php (4 tags) pointed at
openspec/changes/skos-concept-registers/... - a CHANGE directory. That change
was archived on 2026-07-23, so the path stopped existing the moment it moved to
openspec/changes/archive/. A @SPEC tag must target the canonical
openspec/specs/ home, which is where the spec lives now; two of the four also
carried "#skos-002", which is not a heading, and now name the heading that
actually exists.

lib/ContextChat/ContentProvider.php (4 tags, 2 distinct anchors) named
"#requirement-getitemurl-must-resolve-through-the-existing-deep-link-registry"
and "#requirement-initial-import-must-walk-opted-in-schemas-in-batches-and-must-
be-re-runnable-via-occ". Neither heading exists; both requirements were merged
into one, "Requirement: getItemUrl and initial import reuse existing
OpenRegister infrastructure", and the tags were never moved with it.

Every target verified against gate-46's OWN two slug rules - slugify() and
gh_slugify(), which differ on punctuation inside a word - by resolving each
fragment back to the heading text it matches. No overlap with #2355, which
repoints a different set of anchors in file-actions.
rubenvdlinde added a commit that referenced this pull request Aug 6, 2026
…InLoopExpression, untrack the phpmd result cache (#2359)

* fix(quality): scope the Migration phpmd exclude to lib/, drop a foreign copyright claim, clear the coverage ratchet

phpmd-unusedparams.xml carried <exclude-pattern>*/Migration/*</exclude-pattern>.
PDepend compiles an exclude-pattern into an UNANCHORED regex (Input\ExcludePathFilter
preg_quote()s the pattern, then turns `\*` into `.*`), so that form matches ANY path
containing a `/Migration/` segment - lib/Service/Migration/, lib/Command/Migration/,
any future lib/*/Migration/. Those are ordinary classes with no interface-mandated
signature, so a genuine unused parameter in one would never be reported and the run
would still look clean. openconnector carried exactly such a file. openregister has
no lib/*/Migration/ directory today, which is precisely why this had to be fixed
before one appears: the broad form fails silently and only on the day someone adds
the directory. Now `*/lib/Migration/*`, matching the 19 repos that already carry the
corrected shape.

Twelve files carried `SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud
contributors`, scaffolding residue from the Nextcloud app skeleton. The licence sweep
in #2350 relabelled the adjacent SPDX-License-Identifier from AGPL-3.0-or-later to
EUPL-1.2 - which asserts that Nextcloud GmbH's copyright is EUPL-licensed. We cannot
relicense a third party's copyright. Every one of the twelve is Conduction-authored:
each carries `@author Conduction Development Team` and `@copyright Conduction B.V.`
in its own PHPDoc, and `git log --follow` shows only Conduction committers. The stray
holder is corrected to Conduction rather than deleted, so no file loses its REUSE
metadata. Side effect, measured by running phpcs on both versions at the identical
path: 2 pre-existing "Missing short description in doc comment" errors go away.

CountInLoopExpression retired entirely - all 3 baseline entries, all 3 findings. Two
are `do { … } while (count($page) === $limit)` where the page is replaced wholesale
each iteration and never mutated in the body, so the count is taken once per page
into a variable; one is `for ($i = 1; $i < count($rings); $i++)` over an array the
body does not touch, so the count is hoisted. Behaviour is identical in all three.

.phpmd.result-cache.php untracked and gitignored. It is generated output, and a
correctness hazard while committed: `composer quality:phpmd-score` passes --cache,
so a stale cache in the tree makes PHPMD replay a verdict for code that has since
changed - a gate reporting a result it never computed.

.coverage-baseline 58.87 -> 58.93. This was the ONLY red job on development: CI
measured coverage that had improved past its own committed baseline. Raising it
tightens the ratchet.

Unit suite before and after, same container and same vendor: 16030 tests,
35963 assertions, 0 failures, 0 errors - byte-identical totals.

* fix(spec): repoint 6 @SPEC anchors that gate-46 could not resolve

Hydra gate-46 (spec-anchor-existence) failed on this PR with 6 unresolved
targets. All six are pre-existing debt in files this PR already touches, which
is what pulled them into the gate's ADR-020 diff scope; none was introduced
here. 62 of the other gates passed and coverage was 60 of 60 applicable, so
this was a single real failure, not a broken run.

lib/Service/VocabularyImportService.php (4 tags) pointed at
openspec/changes/skos-concept-registers/... - a CHANGE directory. That change
was archived on 2026-07-23, so the path stopped existing the moment it moved to
openspec/changes/archive/. A @SPEC tag must target the canonical
openspec/specs/ home, which is where the spec lives now; two of the four also
carried "#skos-002", which is not a heading, and now name the heading that
actually exists.

lib/ContextChat/ContentProvider.php (4 tags, 2 distinct anchors) named
"#requirement-getitemurl-must-resolve-through-the-existing-deep-link-registry"
and "#requirement-initial-import-must-walk-opted-in-schemas-in-batches-and-must-
be-re-runnable-via-occ". Neither heading exists; both requirements were merged
into one, "Requirement: getItemUrl and initial import reuse existing
OpenRegister infrastructure", and the tags were never moved with it.

Every target verified against gate-46's OWN two slug rules - slugify() and
gh_slugify(), which differ on punctuation inside a word - by resolving each
fragment back to the heading text it matches. No overlap with #2355, which
repoints a different set of anchors in file-actions.

* revert(quality): drop the .coverage-baseline bump — the number drifts with development

I raised .coverage-baseline 58.87 -> 58.93 because that was the value CI itself
recomputed on development, and Coverage Baseline Check was development's only
red job. On this PR it then failed the OTHER direction:

    Coverage baseline: 58.93%
    Coverage current:  58.88%
    FAIL: Coverage dropped by 0.05%

Not a regression from this PR. Development moved between my two CI runs — the
suite went 16030 -> 16038 tests — so the merge base this PR is measured against
computes 58.88, not the 58.93 that development's own HEAD computed earlier. The
two jobs also check opposite things: development's runs coverage-guard.php
--update-baseline and fails when the committed value is STALE, while a PR runs
it plain and fails when coverage DROPS below the committed value. Pinning a
number from one tree to satisfy the other is what broke this.

So the bump leaves this PR. It belongs in a one-line change computed on
development's own HEAD, at a moment development is not mid-merge — not carried
in on a PHPMD branch whose merge base keeps moving underneath it. The job was
red before this branch existed and is unaffected by it either way.

Nothing is weakened: .coverage-baseline returns to development's committed
58.87, exactly as found.

* fix(quality): retire CountInLoopExpression from the baseline and drop 10 entries for a deleted file

phpmd.baseline.xml 519 -> 506 entries, and one rule family leaves ENTIRELY.

A PHPMD baseline entry is scoped to (rule, file) - optionally a method, NEVER a
line - so one entry covers every current AND future violation of that rule in
that file. It is an open licence, not a record. Shrinking the count is therefore
not the point; getting a family to zero is, because only then does a NEW
violation of that rule fail CI.

CountInLoopExpression: all 3 entries removed. The 3 findings behind them were
fixed in this PR, not suppressed. Verified with a single-rule ruleset over all
of lib and NO baseline in play: 3 findings before, 0 after.

lib/Service/Flow/FlowActionService.php: 10 entries for a file that no longer
exists (WeightedMethodCount, CouplingBetweenObjects, LongVariable, ShortVariable,
MissingImport, and Cyclomatic/Npath on run/runNamedFlow/runAction). Deleted with
the file; suppressing nothing; free to remove.

The other 509 entries are all LIVE and were left alone. I checked, and the first
answer was wrong in an instructive way: matching baseline entries against the
report by rule name reported FIVE families - NPath (62), LongMethod (33),
WeightedMethodCount (32), LongParameterList (14), LongClass (6) - as "entirely
stale", 147 free deletions. They are not. The baseline stores the rule CLASS
(PHPMD\Rule\Design\LongMethod) while the XML report writes the rule NAME
(ExcessiveMethodLength), and those five differ. With the mapping applied the
accounting closes exactly: 767 true findings, 767 suppressed by live entries,
nothing unexplained. Uniformity across five independent families was the tell.

Measured with the baseline file MOVED ASIDE, not by dropping --baseline-file:
PHPMD auto-discovers phpmd.baseline.xml sitting next to the ruleset and applies
it either way, so un-flagging it yields a silently baselined run that looks
clean. Independently corroborates #2347's 749 + 16.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant