Skip to content

fix: resolve final 9 SonarQube issues - #7

Merged
thomasluizon merged 1 commit into
mainfrom
fix/sonarqube-final-9
Apr 5, 2026
Merged

fix: resolve final 9 SonarQube issues#7
thomasluizon merged 1 commit into
mainfrom
fix/sonarqube-final-9

Conversation

@thomasluizon

Copy link
Copy Markdown
Owner

Summary

  • Change habit-card <article> to native <button> (fixes S6847, S6845, S6842)
  • Change app-overlay backdrop to <button> (fixes S6847, S6848, S1082)
  • Reduce complexity: login 33->15, upgrade 22->15, habit-form-fields 17->15
  • Fix useState destructuring in page.tsx
  • 1338 tests passing, 0 type errors

Test plan

  • All 1338 tests pass
  • 0 TypeScript errors

🤖 Generated with Claude Code

- S6847/S6845: Change habit-card article to native button element
- S6847/S6848/S1082: Change app-overlay backdrop to button element
- S3776: Reduce complexity in login (33->15), upgrade (22->15),
  habit-form-fields (17->15)
- S6754: Fix useState destructuring in page.tsx
- Update tests for button element change

1338 tests passing, 0 type errors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Apr 5, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
orbit-ui-mobile-web Building Building Preview, Comment Apr 5, 2026 3:53am

@thomasluizon
thomasluizon merged commit fbc4ade into main Apr 5, 2026
3 of 4 checks passed
@sonarqubecloud

sonarqubecloud Bot commented Apr 5, 2026

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
66.7% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

@thomasluizon
thomasluizon deleted the fix/sonarqube-final-9 branch April 5, 2026 17:52
thomasluizon added a commit that referenced this pull request Apr 15, 2026
formatChatMessage previously only ran a hand-rolled regex that escaped
& < > ". It was then passed straight into dangerouslySetInnerHTML. That is
safe today, but:
- Single-quote and backtick were not escaped.
- A malicious AI reply could emit RTL overrides, zero-width joiners, or
  homograph URL fragments that the regex never touches.

The formatter now:
1. Escapes &, <, >, ", ', and `.
2. Applies the **bold** / *italic* markdown replacements.
3. Runs DOMPurify as defense-in-depth with ALLOWED_TAGS=['strong','em']
   and no attributes, so even if future markdown rules introduce a tag
   (e.g. <a>), it cannot leak executable markup.

DOMPurify was already a direct dependency (used in retrospective/page.tsx
and app-overlay.tsx), so this adds no new package weight.

Adds tests for single-quote/backtick escaping and DOMPurify's strip-unknown-
tag behavior.

Covers frontend plan Area A #3 (P0 #7).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
thomasluizon added a commit that referenced this pull request Jun 27, 2026
…-bug fix (#7) (#327)

* feat(upgrade): 3-column redesign + yearly-only matrix + convert-state CTA (#7)

Chooser = 3 cards (Free / Pro-Yearly hero / Pro-Monthly); feature list = one 2-col Free-vs-Pro matrix (kills the 3x repetition). New cell state pro:'yearly'|true|false fixes the retrospective bug (now checkmark+Yearly pill, was proEnabled:false). Matrix reflects #186 free-tier (streaks/XP/freeze free). Trial-state CTA: Subscribe to keep Pro / Upgrade to Pro (trial is a backend grant). Mobile: stacked cards + accordion matrix + sticky CTA. web+mobile, both locales.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: remove stray scratchpad file accidentally committed

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
thomasluizon added a commit that referenced this pull request Jul 10, 2026
…442)

Ticks the final two Stage-7 boxes of the harness-upgrade plan and marks the
whole plan complete. Batches the durable stage-tick edits accumulated across
this multi-session build (7a-7h were already ticked in the working tree; this
adds 7g + 7i + the completion note).

The build itself lives in the separate agentic-dev-workflow repo (PRs #1-#7):
7g = /update-harness (web-grounded monthly staleness audit); 7i = end-to-end
dogfood on a fixture. Documentation-only change here — no code touched.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
thomasluizon added a commit that referenced this pull request Jul 23, 2026
…ce closed

Third review round on this PR found bypass #7, and it was right about my own
test too. Every path rule matches a CONTIGUOUS literal in the raw command text,
so splitting the path across concatenated string literals means no rule fires
and the segment is never examined at all. The test I added last round fragmented
only the FILENAME, so the directory rule still matched an untouched token - it
passed for the wrong reason and left the hole open. Verified before fixing:
three of four fragmentation shapes were ALLOWED, including writes to both
signoff.json and surfaces.json.

Adjacent same-quoted literals are now joined before the path rules run, so the
path is judged by the value it builds rather than the way it is spelled.

Separately, and worse: the adapter caught every internal error and exited 0. A
fence that opens when its own code throws is not a fence. This was not theory -
my first attempt at the #6 fix built a regex inside a template literal, where \b
and \s are string escapes rather than regex ones. It threw on every call, and
every payload sailed through while it did. The catch now exits 2 with an
explanation, so a command the guard cannot classify is refused rather than waved
through. Proven by stubbing the rule core to throw.

Also carried forward from the review: WORKFLOW.md named the deleted /execute
skill and still described bundles as opening draft PRs, which contradicts the
ready-for-review canon; and the flag-new-todos removal left an orphaned fixture
whose MARK constant no longer existed.

Twenty write shapes across four adversarial batteries are refused; five honest
reads still pass, including require('./x.json').cells and a jq read. 755
assertions, 0 failures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pgh7M5YPWLyoDmZpWSTDLt
thomasluizon added a commit that referenced this pull request Jul 23, 2026
* chore(harness): bring the /drive work-order harness to main

The harness that hands an unattended agent a scoped work order and then
measures whether it actually did the work has lived only on
feature/539-b5-apply-design. Nothing on main could run it, so /drive was
usable for exactly one issue. This ports the tooling itself.

What lands:
- tools/workorder.mjs, drive-queue.mjs, check-diff-ownership.mjs,
  surface-manifest.mjs, check-surface-coverage.mjs, visual-signature.mjs and
  the visual-capture helpers
- .claude/skills/drive/run.mjs, the engine SKILL.md references but main lacked
- forbid-gate-tamper + surface-coverage-gate hooks and their shared rule core
- the context reorganisation: .claude/rules/core.md stays always-loaded,
  activity-scoped judgement moves to .claude/playbooks/
- a surfaces manifest and 169 work orders derived from MAIN's own router and
  component graph, not the design branch's

Five independent review rounds hardened this before it moved. The load-bearing
outcomes: the ownership gate judges an explicit base...head range and refuses
to guess one, so a child that commits an escape and checks out the base branch
no longer scores clean; an empty range is a failure, not a pass, which is what
stops a child that produced nothing from being recorded ready-for-review; and
debt clearance is labelled a lint-count axis with the depth measurement printed
beside it, because clearing lint is not evidence that a pixel moved.

Deliberately NOT included, because they are #539 design work rather than
harness: the spacing-scale and no-arbitrary-zindex lint rules, the eslint
config changes that flip no-decorative-glow and no-raw-gradient to error (main
still has the glow those rules ban), and the populated suppressions ledgers.
The ledgers land empty so the tools have their required input; they fill when
the rules arrive with the design work.

Two fixes main needed to accept the port:
- the typed-uppercase corpus guard becomes a ratchet instead of asserting zero.
  main carries two pre-existing values per locale, and one of them,
  confirmPlaceholder "ORBIT", is load-bearing: the user types it verbatim to
  confirm account deletion, so it must never be recased.
- tools/visual-signature.mjs carried a stray NUL byte that made git treat a
  source file as binary and its diff unreviewable.

733 hook assertions pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pgh7M5YPWLyoDmZpWSTDLt

* fix(harness): close the alias bypass in the tamper guard and finish the port's deletions

Addresses the three findings from the review of this PR.

Critical - the signoff tamper guard allowlisted a call by the identifier at the
call site, never by what that identifier was bound to. Since the allowlist has
to carry generic names for their legitimate uses (map, filter, parse, test),
`const map = require("fs").writeFileSync; map(signoff, "fake")` walked straight
through. I reproduced all four shapes the reviewer traced by hand, plus a
destructured rename and a computed member, before changing anything: four of
five were ALLOWED. Now the member lifted off a MODULE must itself be a read API,
a computed member is refused because the name is not visible, and a destructured
binding is judged by the keys it lifts rather than the names it gives them. A
required .json stays DATA, so `require("./surfaces.json").cells` still reads -
that is the pattern this hook's own message advertises, and over-blocking it is
what tempts a future session to disarm the hook. Renaming even a read API is
refused, fail-closed: the guard judges names, and the Read tool covers the rest.
Six regression tests pin every shape; the suite is 740 assertions, 0 failures.

Medium - the reviewer found flag-new-todos.mjs left behind with an import of
checkNewTodos, which this port removes. The cause was broader than the one file:
`git checkout <ref> -- <dir>` adds and updates but never deletes, so every path
the harness branch had removed survived the port. That was 18 files, not one:
the dead hook, the rules README and the three rule files that became playbooks,
the /execute and /night-run skills that /drive replaces, and the eight
.opencode/agents mirrors whose deletion is a locked decision the ported CLAUDE.md
already documents. eslint-rules/no-draggable-onscroll.cjs is deliberately KEPT -
it is main's own rule from #568, absent from the branch only because the branch
predates it.

Low - removed the stale comment claiming the locale corpus carries zero
typed-uppercase violations, which contradicted the ratchet directly below it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pgh7M5YPWLyoDmZpWSTDLt

* fix(harness): track the module reference, not the shape of the require call

Second review round on this PR found bypass #6: the fix for #5 only recognised a
member chained straight onto require(), so splitting it across two statements put
the write behind an allowlisted call-site name again.

  node -e "const m=require('fs'); const map=m.writeFileSync; map(signoff,'{}')"

Reproduced before changing anything: three of four write shapes were ALLOWED, and
the reviewer traced it by hand because its sandbox could not execute node. The
one shape already blocked was blocked for an unrelated reason.

The classifier now tracks the module REFERENCE rather than the syntax around it.
An identifier bound to a require() of a module is followed through plain
rebinding, so a chain of aliases inherits it, and every member lifted off any of
them must be a read API. A computed member is refused because the name is not
visible. A required .json stays DATA, so reading .cells off the manifest still
works. Renaming even a read is still refused by the callee scan: this list grants
nothing, it only withholds.

Replaced the per-identifier dynamic RegExp with static patterns after the first
attempt built its source in a template literal, where \b and \s are JS escapes,
not regex ones - it threw at parse time and every command sailed through while it
did. Static patterns cannot fail that way.

Five regression tests cover the two-step, transitive-rebind, computed-member and
destructure-off-a-bound-ref forms, plus the honest read that must keep working.
745 assertions, 0 failures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pgh7M5YPWLyoDmZpWSTDLt

* test(harness): pin the rest of the tamper-bypass class

The reference tracking that closed bypass #6 should make the neighbouring shapes
fall out for free. Probed rather than waited for: write streams, deletions, the
promises submodule off fs, fs/promises, child_process, and a path built by
concatenation are all refused. Pinned so a later change to the classifier cannot
quietly reopen one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pgh7M5YPWLyoDmZpWSTDLt

* fix(harness): judge the path by the value it builds, and fail the fence closed

Third review round on this PR found bypass #7, and it was right about my own
test too. Every path rule matches a CONTIGUOUS literal in the raw command text,
so splitting the path across concatenated string literals means no rule fires
and the segment is never examined at all. The test I added last round fragmented
only the FILENAME, so the directory rule still matched an untouched token - it
passed for the wrong reason and left the hole open. Verified before fixing:
three of four fragmentation shapes were ALLOWED, including writes to both
signoff.json and surfaces.json.

Adjacent same-quoted literals are now joined before the path rules run, so the
path is judged by the value it builds rather than the way it is spelled.

Separately, and worse: the adapter caught every internal error and exited 0. A
fence that opens when its own code throws is not a fence. This was not theory -
my first attempt at the #6 fix built a regex inside a template literal, where \b
and \s are string escapes rather than regex ones. It threw on every call, and
every payload sailed through while it did. The catch now exits 2 with an
explanation, so a command the guard cannot classify is refused rather than waved
through. Proven by stubbing the rule core to throw.

Also carried forward from the review: WORKFLOW.md named the deleted /execute
skill and still described bundles as opening draft PRs, which contradicts the
ready-for-review canon; and the flag-new-todos removal left an orphaned fixture
whose MARK constant no longer existed.

Twenty write shapes across four adversarial batteries are refused; five honest
reads still pass, including require('./x.json').cells and a jq read. 755
assertions, 0 failures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pgh7M5YPWLyoDmZpWSTDLt

* fix(harness): stop matching obfuscated paths; refuse the interpreter instead

Fourth review round found bypass #8: joinConcatenatedLiterals needed the quote
character to match on both sides of the +, so a mixed-quote split walked past it.
Verified, and it is worse than reported - two more shapes I went looking for also
walked past: splitting the path across variables, and assembling it with
Array.join. The suggested fix (drop the backreference) closes only the first.

That is the whole lesson of the last four rounds. Each one closed a different
spelling of "hide the path" - an alias, a split literal, a mixed quote pair, a
variable - and the next round found the next spelling. Deciding what a path
argument evaluates to inside arbitrary code is not something a regex can do, so
the path is no longer what decides.

An interpreter one-liner that cannot be PROVEN a pure read is now refused
whatever it names. This is not a new doctrine, it is the one this file already
states - the code is judged by an allowlist of what it may call - applied at the
point that previously required a path rule to fire first. There is nothing left
to obfuscate, because the filename stopped mattering.

The cost is honest and worth naming: a legitimate `node -e` that writes any file
is now refused and must use the Write tool, and a `node -e` whose require()
argument is itself concatenated is refused one step earlier than before (it was
already unverifiable). The reads this guard advertises still work: node -p of the
manifest, a bound module ref used for a direct read, cat/jq/grep, and the Read
tool.

Twenty-three write shapes across five adversarial batteries are refused. 761
assertions, 0 failures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pgh7M5YPWLyoDmZpWSTDLt

* fix(harness): let the plan un-ignore reach .claude/plans/completed

Review found that `.claude/plans/*` plus `!.claude/plans/*.plan.md` cannot
re-include a plan inside a subdirectory: git never descends into an excluded
directory to evaluate a nested negation, so the `completed` entry was excluded
whole. That is exactly where implement/SKILL.md files a finished plan
(`mkdir -p .claude/plans/completed && mv ...`), and pr-review/SKILL.md reads it
back, so a completed plan would silently never be committed - recreating the
fresh-checkout-with-a-missing-contract failure the un-ignore was added to fix.

Verified with git add --dry-run rather than check-ignore, whose exit code is
ambiguous when the last matching rule is a negation: before, a top-level plan
staged and .claude/plans/completed was reported ignored; after, both stage. A
non-plan file in the same directory stays ignored, which is the half worth
keeping.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pgh7M5YPWLyoDmZpWSTDLt

* fix(harness): give PAUSED the same shape as the other four gate files

Review found the best defect of this whole series, because it needs no cunning.
PAUSED was the only one of the five path patterns without an optional directory
prefix, and the only one tested against the whole raw command rather than per
segment. So `cd .claude/manifests && touch PAUSED` matched nothing: the cd
segment is a read, and the touch segment carries no directory text at all. That
one command silently creates the human-only disarm switch, and the Stop gate
exits immediately when it exists. Verified: four of five shapes were allowed,
including `cd ... && echo x > PAUSED`.

PAUSED now carries the same optional-prefix bare-name shape as signoff,
surfaces and verdicts, and is judged per segment like them. Reads of it are
allowed as a consequence, which also retires a false positive that blocked an
ordinary `[ -f ... ]` existence test earlier in this session - a guard that
refuses honest reads is one a future session disarms.

The same review noted the sibling: plain shell adjacent-quote concatenation
(`sign''off.json`, which the shell parses identically) split the literal for the
other four. Empty quote pairs are now spliced out before path matching, next to
the JS concatenation join.

Also: WORKFLOW.md described /drive output as draft PRs in seven places, directly
contradicting its own top table and the implement agent specs, which state that
a draft PR silently skips reviewers. All seven now say ready-for-review.

Five regression tests. 766 assertions, 0 failures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pgh7M5YPWLyoDmZpWSTDLt

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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