Skip to content

Commit b656d2a

Browse files
fix: complete PR #219 reconciliation and governance hardening (#242)
## Change and reason Supersedes #219 and the earlier reconciliation attempt #228. The validated-statistics and CladeCumulus changes on those stale branches have already landed through later work on `main`; conflict comparison showed that taking either stale side would now revert newer strict I-JSON, streamed `BigInt`, read-only DuckDB, tree-rooting, and unplaced-read fixes. This branch is therefore based directly on current `main` and carries only changes that are still needed. - **CladeCumulus:** active non-root tips now use the same node-local PD convention before and after drag-and-drop reaggregation. Regression tests cover every fixture tip before and after an accepted move, plus exact nested-clade totals (0.60 and 1.40) that reject parent-side double-counting. This addresses the remaining Codex finding on #228 and CodeRabbit's aggregation finding on #219. - **CI:** the full Julia job has a 60-minute hang bound, and validated-statistics diagnostics no longer use `|| true`; the captured test exit status remains authoritative. - **Frontend quality:** SVG path fragments are batched and joined once rather than repeatedly concatenated, and `PaneEditor` uses the direct hook dependency. - **Security and conformance:** adds the missing security policy and EditorConfig, plus pinned OpenSSF Scorecard and full-history secret-scanning workflows. Scorecard retains least-privilege default-branch SARIF upload without a self-referential PR alert. The repository gitleaks config backports two upstream rules for biological-data false positives and allowlists only ten exact public schema/callback identifiers—no path or commit exclusions. The existing DEED lane is conformant and unchanged. ## Validation - `npx --yes bun@1.3.10 install --frozen-lockfile` - `npx --yes bun@1.3.10 run typecheck` - `npx --yes bun@1.3.10 test src` — **72 passed, 0 failed** (200 assertions) - `npx --yes bun@1.3.10 run build` - Parsed every workflow as YAML and every inline `run` block with `bash -n` - `bash scripts/check-governance-contract.sh` — all 20 checks passed - `python3 test/references/check_high_precision_references.py --check test/unit/test_high_precision.jl` - `bash scripts/lint-sonar-reliability.sh` - `git diff --check` - Reproduced the legacy gitleaks rules over all 188 reachable commits: 211,428 biological/generated-code false positives reduced to zero; synthetic AWS, Facebook, and generic credential canaries still produced exactly three findings - Fresh-head `Secret history and working tree / gitleaks` — **passed** with the CI-pinned gitleaks 8.18.4 - [CI run 39986326202](https://github.com/hyperpolymath/MetaManifold-WebUI/actions/runs/39986326202) on `ed24c9c` — **passed**: Julia 1.12.5 tests, validated statistics, frontend build/tests, R/RCall checks, high-precision references, benchmarks, Agda, and both occupancy lanes ## Deliberate review triage - No baseline or exclusion was added to hide Hypatia findings. - TypeScript remains the settled frontend stack and the independent Python numerical oracles remain in place; the banned-language findings are policy debt, not vulnerabilities introduced here. - XML namespace `http://www.w3.org/...` strings were not changed to HTTPS: they are identifiers, not network requests. - Existing licence headers were left unchanged as `CONTRIBUTING.md` requires. - No placeholder CodeQL, Hypatia, mirror, or AI-manifest files were added. CodeQL default setup and the Hypatia governance lane are already active; external mirroring and the legacy A2ML-to-DEED migration require explicit owner configuration/decisions. ## Scope - [x] Does not alter dependencies, lockfiles, generated assets, or vendored files - [x] Secrets, local paths, and run outputs are not committed - [x] Commits include `Signed-off-by:` (Developer Certificate of Origin) --------- Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
1 parent d69d4f9 commit b656d2a

11 files changed

Lines changed: 222 additions & 19 deletions

File tree

‎.editorconfig‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# SPDX-License-Identifier: MPL-2.0
2+
# https://editorconfig.org
3+
4+
root = true
5+
6+
[*]
7+
charset = utf-8
8+
end_of_line = lf
9+
indent_style = space
10+
indent_size = 2
11+
insert_final_newline = true
12+
trim_trailing_whitespace = true
13+
14+
[*.{jl,R,r,py}]
15+
indent_size = 4
16+
17+
[*.{md,adoc}]
18+
trim_trailing_whitespace = false
19+
20+
[Makefile]
21+
indent_style = tab

‎.github/workflows/ci.yml‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,9 @@ jobs:
178178
# for 24.04, and a runner that silently rolled to the next LTS would take that pin
179179
# with it. `runs-on` cannot read `env`, so this is a literal.
180180
runs-on: ubuntu-24.04
181+
# Bound a hung toolchain install or test run. The full R/Julia suite normally
182+
# completes well inside this budget; the GitHub default is six hours.
183+
timeout-minutes: 60
181184
# The repository's root .Rprofile sources renv/activate.R, so R started from
182185
# the checkout auto-activates renv and rewrites the library paths. renv is a
183186
# local-development convenience; CI installs into the system library and
@@ -417,10 +420,12 @@ jobs:
417420
# failure but the last one. Publish a digest of all failing
418421
# assertions first - the testset, the expression and the evaluated
419422
# values are what identify the bug - then the end of the log.
423+
# `set +e` above is deliberate: diagnostic helpers may find no
424+
# matches, and only the captured test status controls this step.
420425
grep -E "Test Failed at |Error During Test at |^ Expression: |^ Evaluated: |^ Test threw exception" \
421-
/tmp/validated-statistics-tests.log > /tmp/validated-statistics-failures.log || true
426+
/tmp/validated-statistics-tests.log > /tmp/validated-statistics-failures.log
422427
rm -f /tmp/validated-statistics-digest-*
423-
split -b 2500 /tmp/validated-statistics-failures.log /tmp/validated-statistics-digest- || true
428+
split -b 2500 /tmp/validated-statistics-failures.log /tmp/validated-statistics-digest-
424429
index=0
425430
for chunk in /tmp/validated-statistics-digest-*; do
426431
[ -e "$chunk" ] || continue

‎.github/workflows/scorecard.yml‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# SPDX-License-Identifier: MPL-2.0
2+
# Pinned reusable implementation; do not replace with a floating branch tag.
3+
name: Scorecard
4+
5+
# Scorecard publishes SARIF for the default branch. Keep its required
6+
# security-events upload permission off pull requests: otherwise Scorecard
7+
# scans this caller, flags that necessary permission, and creates a blocking
8+
# code-scanning check on its own security workflow. Scheduled and main-branch
9+
# scans retain the findings without that self-referential PR gate.
10+
on:
11+
push:
12+
branches: [main]
13+
schedule:
14+
- cron: '0 6 1 * *'
15+
workflow_dispatch:
16+
17+
concurrency:
18+
group: ${{ github.workflow }}-${{ github.ref }}
19+
cancel-in-progress: true
20+
21+
permissions:
22+
contents: read
23+
24+
jobs:
25+
scorecard:
26+
name: OpenSSF Scorecard
27+
permissions:
28+
actions: read
29+
contents: read
30+
security-events: write
31+
id-token: write
32+
uses: hyperpolymath/standards/.github/workflows/scorecard-reusable.yml@75bd5994470661a099df562d6d5ad2fe744dbd66
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# SPDX-License-Identifier: MPL-2.0
2+
# Full-history and working-tree secret scanning from the pinned estate reusable.
3+
name: Secret Scanner
4+
5+
on:
6+
push:
7+
branches: [main]
8+
pull_request:
9+
workflow_dispatch:
10+
11+
concurrency:
12+
group: ${{ github.workflow }}-${{ github.ref }}
13+
cancel-in-progress: true
14+
15+
permissions:
16+
contents: read
17+
18+
jobs:
19+
scan:
20+
name: Secret history and working tree
21+
permissions:
22+
contents: read
23+
uses: hyperpolymath/standards/.github/workflows/secret-scanner-reusable.yml@75bd5994470661a099df562d6d5ad2fe744dbd66
24+
# Deliberately no secrets: the pinned reusable declares and needs none.

‎.gitignore‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,8 @@ archive/
320320
### Exceptions
321321
!.github
322322
!.github/**
323+
!.editorconfig
324+
!.gitleaks.toml
323325
!.Rprofile
324326

325327
# Brainstorming visual-companion scratch (mock-ups, server state)

‎.gitleaks.toml‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# SPDX-License-Identifier: MPL-2.0
2+
# Repository-specific secret-scanning rules. The reusable stages the pinned
3+
# estate baseline at this path; no path- or commit-wide exceptions are made.
4+
5+
[extend]
6+
path = ".gitleaks-estate.toml"
7+
8+
[allowlist]
9+
description = "Exact public schema and UI callback identifiers misread as generic keys"
10+
regexes = [
11+
'''^Associated_material$''',
12+
'''^Associated_organism$''',
13+
'''^allowPreviousMonth$''',
14+
'''^associated_organism$''',
15+
'''^canAddSubplot$''',
16+
'''^focusPreviousDay$''',
17+
'''^getInitialMonthFromProps$''',
18+
'''^getPreviousNavigableMonth$''',
19+
'''^handleDayClick$''',
20+
'''^handleMouseEnter$''',
21+
]
22+
23+
# Backport the upstream v8.30.1 AWS rule for the CI-pinned v8.18.4 engine.
24+
# The older rule accepts arbitrary A-Z0-9 suffixes with no entropy threshold,
25+
# which turns ordinary A/C/G/T sequences into AWS access-token findings. The
26+
# upstream boundaries, suffix alphabet, and entropy requirement retain real
27+
# credential detection without excluding biological data by path or commit.
28+
[[rules]]
29+
id = "aws-access-token"
30+
description = "Identified a pattern that may indicate AWS credentials, risking unauthorized cloud resource access and data breaches on AWS platforms."
31+
regex = '''\b((?:A3T[A-Z0-9]|AKIA|ASIA|ABIA|ACCA)[A-Z2-7]{16})\b'''
32+
entropy = 3
33+
keywords = ["a3t", "akia", "asia", "abia", "acca"]
34+
35+
# Likewise, upstream now requires a realistic Facebook token length and
36+
# entropy. Short printable FASTQ quality strings otherwise match the legacy
37+
# twenty-character minimum even though they are sequencing data.
38+
[[rules]]
39+
id = "facebook-page-access-token"
40+
description = "Discovered a Facebook Page Access Token, posing a risk of unauthorized access to Facebook accounts and personal data exposure."
41+
regex = '''\b(EAA[MC](?i)[a-z0-9]{100,})(?:[\x60'"\s;]|\\[nr]|$)'''
42+
entropy = 4
43+
keywords = ["eaam", "eaac"]

‎SECURITY.md‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<!-- SPDX-License-Identifier: CC-BY-SA-4.0 -->
2+
# Security policy
3+
4+
## Supported version
5+
6+
Security fixes are applied to the current `main` branch. Update to the latest
7+
revision before reporting a problem that may already have been fixed.
8+
9+
## Report a vulnerability
10+
11+
Use [GitHub private vulnerability reporting](https://github.com/hyperpolymath/MetaManifold-WebUI/security/advisories/new)
12+
for suspected vulnerabilities in this fork. If private reporting is not
13+
available, contact the fork maintainer at `j.d.a.jewell@open.ac.uk` (the
14+
contact already published in `MetaManifold-WebUI_chora.deed`). Do not disclose
15+
an unpatched vulnerability in a public issue or pull request.
16+
17+
Include the affected revision, a minimal reproduction, the security impact, and
18+
any suggested mitigation. Remove real study data, credentials, access tokens,
19+
and other secrets from the report. The maintainers will assess the report and
20+
coordinate a fix and disclosure with the reporter.
21+
22+
For vulnerabilities confined to upstream code that are not specific to this
23+
fork, also consult the upstream project's reporting process rather than
24+
assuming the fork can publish an upstream fix.

‎frontend/src/figure/PaneEditor.tsx‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,7 @@ export function PaneEditor({ study, runs, source, pane, onChange }: {
4242
const selected = useMemo(() => {
4343
const keys = new Set((chart?.runs ?? []).map(specKey))
4444
return runs.filter(r => keys.has(specKey(r)))
45-
// eslint-disable-next-line react-hooks/exhaustive-deps
46-
}, [runs, chart?.runs.map(specKey).join(',')])
45+
}, [runs, chart?.runs])
4746
const tables = useSharedResultsTables(study, selected)
4847

4948
const [ranks, setRanks] = useState<string[]>([])

‎frontend/src/tree/TreeShapes.tsx‎

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,12 @@ export function TreeShapes({ layout, settings, selSubtree, matchSet, onSelect, o
2222
const branchPath = (n: LNode) => mode === 'rectangular' ? `M${n.px},${n.y}H${n.x}` : `M${n.ex},${n.ey}L${n.x},${n.y}`
2323
const right = aligned && settings.tipJustify === 'right'
2424

25-
// Branches are batched into one path per colour.
25+
// Branches are batched into one path per colour. Accumulate path fragments
26+
// in arrays and join once; repeated string concatenation is quadratic for
27+
// large trees in engines that cannot flatten the intermediate strings.
2628
const { strokes, selected, leaders, caps } = useMemo(() => {
27-
const byColour = new Map<string, { rgba: Rgba | null; d: string }>()
28-
let sel = '', lead = ''
29-
let caps = ''
29+
const byColour = new Map<string, { rgba: Rgba | null; parts: string[] }>()
30+
const selectedParts: string[] = [], leaderParts: string[] = [], capParts: string[] = []
3031
const arc = (n: LNode) => {
3132
const first = n.kids[0], last = n.kids[n.kids.length - 1]
3233
if (mode === 'slanted' || mode === 'unrooted') return ''
@@ -38,9 +39,10 @@ export function TreeShapes({ layout, settings, selSubtree, matchSet, onSelect, o
3839
return `M${cx + r * Math.cos(a1)},${cy + r * Math.sin(a1)}A${r},${r} 0 ${large} 1 ${cx + r * Math.cos(a2)},${cy + r * Math.sin(a2)}`
3940
}
4041
for (const n of layout.nodes) {
41-
let p = ''
42-
if (n.d.parent) p += branchPath(n)
43-
if (n.kids.length) p += arc(n)
42+
const p = [
43+
n.d.parent ? branchPath(n) : '',
44+
n.kids.length ? arc(n) : '',
45+
].join('')
4446
if (n.capped) {
4547
// Two slashes across the middle of a branch drawn shorter than it is.
4648
const sx = mode === 'rectangular' ? n.px : n.ex, sy = mode === 'rectangular' ? n.y : n.ey
@@ -50,27 +52,34 @@ export function TreeShapes({ layout, settings, selSubtree, matchSet, onSelect, o
5052
const vx = 4 * (-uy + 0.5 * ux), vy = 4 * (ux + 0.5 * uy)
5153
for (const o of [-2, 2]) {
5254
const qx = mx + o * ux, qy = my + o * uy
53-
caps += `M${qx - vx},${qy - vy}L${qx + vx},${qy + vy}`
55+
capParts.push(`M${qx - vx},${qy - vy}L${qx + vx},${qy + vy}`)
5456
}
5557
}
5658
const c = n.d.style.branch ?? null
5759
const key = c ? `${c.hex}/${c.alpha}` : ''
5860
const entry = byColour.get(key)
59-
if (entry) entry.d += p
60-
else byColour.set(key, { rgba: c, d: p })
61-
if (selSubtree.has(n.d.id)) sel += p
61+
if (entry) entry.parts.push(p)
62+
else byColour.set(key, { rgba: c, parts: [p] })
63+
if (selSubtree.has(n.d.id)) selectedParts.push(p)
6264
if (aligned && !n.kids.length) {
6365
const end = n.wedge ? farthest(n.wedge, circular, cx, cy) : null
6466
if (circular) {
6567
const r0 = end ? end.r : n.radius
66-
if (layout.alignAt - 4 > r0) lead += `M${cx + r0 * Math.cos(n.angle)},${cy + r0 * Math.sin(n.angle)}L${cx + (layout.alignAt - 4) * Math.cos(n.angle)},${cy + (layout.alignAt - 4) * Math.sin(n.angle)}`
68+
if (layout.alignAt - 4 > r0) {
69+
leaderParts.push(`M${cx + r0 * Math.cos(n.angle)},${cy + r0 * Math.sin(n.angle)}L${cx + (layout.alignAt - 4) * Math.cos(n.angle)},${cy + (layout.alignAt - 4) * Math.sin(n.angle)}`)
70+
}
6771
} else {
6872
const x0 = end ? end.x : n.x
69-
if (layout.alignAt - 4 > x0) lead += `M${x0},${n.y}H${layout.alignAt - 4}`
73+
if (layout.alignAt - 4 > x0) leaderParts.push(`M${x0},${n.y}H${layout.alignAt - 4}`)
7074
}
7175
}
7276
}
73-
return { strokes: [...byColour.values()], selected: sel, leaders: lead, caps }
77+
return {
78+
strokes: [...byColour.values()].map(({ rgba, parts }) => ({ rgba, d: parts.join('') })),
79+
selected: selectedParts.join(''),
80+
leaders: leaderParts.join(''),
81+
caps: capParts.join(''),
82+
}
7483
}, [layout, circular, cx, cy, selSubtree, aligned, mode])
7584

7685
// Label anchor: beside the node, or in the aligned column.

‎src/analysis/clade_cumulus.jl‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2181,7 +2181,11 @@ function _build_phylo_or_combined_tree(
21812181
Epistemic.cloud_size_by_residual(res),
21822182
is_root ? 0.0 : max(0.0, u.branch_length),
21832183
u.support === nothing ? sup : u.support,
2184-
0.0,
2184+
# Keep the same node-local convention as clade_faith_pd and
2185+
# _reaggregate_after_move: an active non-root tip carries its
2186+
# incoming edge. This makes an unchanged tip's PD comparable
2187+
# before and after an accepted drag-and-drop proposal.
2188+
cnt > 0.0 && !is_root ? max(0.0, u.branch_length) : 0.0,
21852189
tx,
21862190
tx === nothing ? nothing : 1.0,
21872191
tx === nothing ? nothing : (total_tips_per_taxon[tx] == 1),

0 commit comments

Comments
 (0)