Skip to content

Latest commit

 

History

History
71 lines (57 loc) · 86.4 KB

File metadata and controls

71 lines (57 loc) · 86.4 KB

Decision Log

Architectural and tech-stack decisions made across the project's lifetime. Each row captures: the decision, alternatives considered, why this option won, and the reversal cost.

This file is the canonical public-facing source. A mirror copy lives in the gitignored CLAUDE.md as a personal-overlay shortcut — but THIS file is authoritative. When a new decision lands, it goes here first.

Plan-reference: Phase 0.5 migration (PUBLIC_RELEASE_PLAN.md, DECISION_LOG D11).


When making a tech choice with multiple valid options, record it here BEFORE deploy. Skip trivial choices (which for loop, variable name). Record only choices where ALL of these are true:

  • Multiple valid options exist
  • The losing option could plausibly be re-proposed in 6 months by someone (or a future Claude session) who did not see this discussion
  • Reverting later would require non-trivial work (data migration, key rotation, schema change, user retraining)

Format (one row per decision; append, never edit historical rows):

Date Decision Alternatives considered Why this won Reversal cost
2026-04-12 HMAC key derived from Splunk GUID at runtime (1h cache TTL) Static key in app.conf; PBKDF2 from passphrase Auto-rotates on container rebuild; no key in source; no manual rotation Medium — every GUID change requires reset_cooldowns.sh + FIM rebuild
2026-04-12 Dual-store FIM baseline (file + KV collection) File-only baseline Attacker can not silently re-baseline by editing the file alone Low — delete both stores, FIM auto-rebuilds in 60s
2026-04-13 KV-store cooldowns instead of JSON file JSON file with HMAC + flock Survives concurrent writes without file locking; queryable from SPL Medium — requires wl_migrate_cooldowns.py for schema changes
2026-04-14 FIM polling 60s → 15s + 2s stat-watcher Keep at 60s; switch to inotify Benchmarked at 0.6% CPU; inotify needs Linux-only deps Splunk container lacks Low — change inputs.conf interval back to 60
2026-04-15 Lockdown-exempt deploy windows All actions blocked during lockdown Allows hotfix deploys during incident; sentinel mutations stay HIGH Low — remove from LOCKDOWN_EXEMPT_ACTIONS set
2026-04-19 Scheduled expiration cleanup compares legacy (no-suffix) rows against UTC (tz_offset_minutes=0) Query user's tz from Splunk prefs per row; keep prior server-local-time behavior Server-side scheduled job has no browser tz context; UTC is deterministic across Splunk hosts and matches the handler's contract. Analysts needing precise tz should use the new " UTC"-suffixed format. Low — change SCHEDULED_TZ_OFFSET_MINUTES constant in bin/wl_expiration_cleanup.py
2026-04-21 wl_fim_watch.py emits only state-change events (fim_watch_started/fim_watch_stopped); no periodic heartbeat Keep the 5-minute heartbeat (288 events/day); increase interval to 1h (24/day); route heartbeat to index=_internal At 288 events/day × N instances the heartbeat dominates index=wl_audit storage and floods the "File Integrity Monitor Alerts" panel, burying real alerts. splunkd supervises the watcher via interval=0, so a dead process is auto-restarted and each restart fires a fresh fim_watch_started — a flapping watcher surfaces as repeated starts without matching stops. Known trade-off: a watcher that hangs without exiting (no SIGKILL, no exit) is undetectable from the audit trail alone. Low — reintroduce a HEARTBEAT_INTERVAL constant + emit block in bin/wl_fim_watch.py around line 640; dashboard filter in audit.xml is belt-and-suspenders and harmless if left in place
2026-04-22 RequireJS urlArgs: "_b=<build>" cache-bust in appserver/static/whitelist_manager.js kept in sync with app.conf build Document "hard-refresh after every deploy" in README; rename JS files on every deploy; force Splunk to regenerate @<server-hash> (not user-controllable) Splunk sends Cache-Control: public, max-age=31536000 on /static/@<server-hash>/... assets. The @<hash> is the Splunk server-build identifier — bumping app.conf build does NOT change it, so bumped app versions don't invalidate browser cache. Users kept running stale JS for days; build-607 content_hash fix was invisible to them until I diagnosed cache layer. urlArgs appends ?_b=<N> to every AMD module URL, forcing a fresh fetch each build bump. Low — remove the require.config({ urlArgs: ... }) line from whitelist_manager.js; reverts to stale-cache behavior. Maintenance rule: bumping build in app.conf REQUIRES also bumping _b= in whitelist_manager.js (same number) — otherwise new JS will not reach users.
2026-04-23 Mechanical PreToolUse hook (.claude/hooks/block-synthetic-fixtures.js) blocks direct writes to Splunk-internal state during feature verification; paired with prose rule in CLAUDE.md ("Synthetic Fixtures — Banned for Feature Verification") Prose-only rule in CLAUDE.md (same as before the incident); rely on self-discipline; per-commit review checklist Prose-only was the existing state, and it failed to prevent the build 614 "Invalid Date" incident (synthetic _approval_queue.json injection hid the dual-admin timestamp vs submitted_at schema drift). Hooks cannot be rationalized away the way prose rules can; Claude reads CLAUDE.md once per session but the hook runs every Write/Edit/Bash. Path-gated exception for tests/unit/** preserves pure-helper unit testing; # JUSTIFIED: <reason> marker on Bash covers the first-install-bootstrap edge case. Low — delete the PreToolUse entry from .claude/settings.json. .claude/ is git-ignored so the hook is per-developer; if reversed, no shared state changes. Pairs with feedback_synthetic_fixtures_mask_schema_drift.md for the lesson.
2026-04-26 Strict-ASCII policy on detection rule names, CSV filenames, approval reasons, comments, and app_context; enforced at BOTH outer wrapper (_submit_create_delete_approval) AND inner choke point (_submit_approval) plus pipeline replay; null bytes + control chars + zero-width + bidi-override + fullwidth + combining marks all rejected. Frontend-only ASCII regex (existing wl_modals.js pre-flight check); Python c.isalnum() fallback (Unicode-aware); accept Unicode but normalize via NFC/NFKC before storage Frontend-only is bypassable with a 30-second curl call; c.isalnum() is Unicode-aware so it accepted CJK/Cyrillic/Greek "letters" and made the validator a no-op for the attacks we cared about. Strict ASCII (regex ^[A-Za-z0-9_\-. ]+$) closes the homoglyph + filesystem-path + SPL-identifier attack surface in one rule. The dual-gate placement was discovered during pre-release gate-coverage testing — the outer wrapper alone left a bypass via direct action=submit_approval POST. The bypass was identified and closed in the same hardening round. NFC/NFKC normalization rejected because (a) it doesn't help against bidi/zero-width attacks, (b) it adds a stateful step before validation that an attacker can probe for timing oracles, (c) ASCII is the operational reality — every dashboard, audit search, and rule_csv_map.csv export consumes ASCII anyway. Low — revert is_ascii_name/is_valid_app_context callers to c.isalnum()-style checks; queue entries with the old policy continue to work because _execute_replay_create_csv has an explicit path is None guard that fails cleanly. Maintenance rule: when adding a new payload field that flows into a filesystem path, SPL search, audit log, or display string, run validate_ascii_text on it at the gate AND add a unit test in tests/unit/test_ascii_validation.py pinning the rejection.
2026-05-01 Kill the wl-btn / wl-btn-primary / wl-btn-danger taxonomy entirely — migrate every site to Splunk-bundled btn / btn-primary / btn-danger (build 631-632). Preserve only wl-btn-locked (the opacity helper used by approval-lock UX in wl_table.js and wl_approval_ui.js); add proper CSS rules for btn-success / btn-danger / btn-warning which Splunk's bundle ships unstyled. (a) Keep both taxonomies and add CSS rules for wl-btn / wl-btn-primary / wl-btn-danger; (b) keep wl-btn as the canonical app system and provide rules for it; (c) migrate fully to Splunk-bundled and remove wl-btn from production. The 2026-04-30 UI consistency audit found that wl-btn, wl-btn-primary, and wl-btn-danger had ZERO matching rules in whitelist_manager.css — only .wl-btn-locked (opacity helper) existed. Sites using these classes rendered as plain text (transparent bg, 0 padding, 0 border): the Activate Emergency Lockdown button rendered as 12px red plain text; Admin Settings Save Changes / Reset to Defaults rendered as plain text; Trash Restore / Purge rendered as plain text. (a) Adding rules for wl-btn would mean owning a parallel button system that has to track every Splunk visual update — a long-term liability. (b) Same problem, plus it would require migrating from btn-primary (which IS shipped by Splunk and works) to wl-btn-primary (which we'd own forever). (c) Killing wl-btn fully aligns the app with Splunk's bundled visual language at the cost of a one-time search-and-replace in 9 sites in control_panel.js. Splunk's .btn rules are stable across major versions; we're piggybacking on them rather than maintaining a parallel system. The migration is class-rename only — handlers find by #id, not class — so click semantics are preserved. Low — git revert the build-631 + build-632 commits, redeploy at build 633. The .wl-btn-locked class is preserved so the approval-lock UX continues to work. Maintenance rule: do NOT introduce new .wl-btn* classes. Use Splunk-bundled btn / btn-primary and the four custom .btn-success / .btn-danger / .btn-warning rules in whitelist_manager.css (which we own and won't drift). When adding a NEW custom button colour, add it as .btn-<name> (a Bootstrap-style extension) not as .wl-btn-<name> (a parallel system).
2026-05-01 Drop light-theme support; force dark-only theme (build 637). (a) Keep the :root light + body.wl-dark override pattern and finish wiring light-mode tests; (b) force wl-dark always but leave the parallel CSS structure (~70 lines of dormant light vars); (c) collapse to single :root (dark only), remove body.wl-dark overrides, simplify detectDarkTheme(). The app has been dark-first for its entire history. Light-mode paths were half-implemented: body.wl-dark > .wl-modal-overlay re-tightened --wl-bg for modals (duplicate of the dark vars in the main block), several inline-styled banners assumed dark bg, and there are no E2E tests for light mode. The build-636 light-theme test surfaced this — toggling wl-dark off via JS produced a half-converted state (body white, panels still dark, dropdowns half-transitioning) that was worse than either pure theme. (a) finishing the light path is several hours of work for zero current users (open-source target, no paying customer demanding it). (b) is reversible-but-half-done — keeps the light infrastructure as a tech-debt anchor that future contributors will trip over. (c) deletes the parallel system entirely: collapses :root (light defaults) and body.wl-dark (dark overrides) into a single :root block, removes the modal-overlay duplicate, and simplifies detectDarkTheme() to unconditionally apply wl-dark. The 19 existing .wl-dark X selectors stay as harmless redundancy (they always match because the class is always applied) — they can be flattened to plain X selectors in a follow-up cleanup. Medium — re-introduce :root light vars (40 lines), re-add body.wl-dark { ... } wrapper around the dark vars, restore the brightness-based check in wl_ui.js :: detectDarkTheme(). Estimated 30 min if a customer ever requests light theme. Maintenance rule (theme decisions for vanilla-JS apps): do NOT half-implement theming. Either commit fully (every panel uses CSS vars, every modal tested in both modes) or commit to one. The half-built state always produces visible jarring states when something goes wrong — and "something goes wrong" includes any contributor adding a new panel without remembering to use vars.
2026-05-01 Kill .btn-success taxonomy too; collapse all green buttons to Splunk-bundled .btn-primary (build 635). (a) Keep .btn-success as a parallel rule with our own muted shade; (b) override Splunk's .btn-primary to our muted shade so all greens match; (c) migrate every .btn-success callsite to .btn-primary and delete the rule. The build-634 desaturation pass kept .btn-success muted (#388e3c) while .btn-primary rendered Splunk's vivid #1a8929. User-facing result: "+ Add Row" / "+ Add Column" (btn-primary) and Save Changes / Approve (btn-success) had two slightly-different greens side-by-side on the same toolbar — visible inconsistency that the user flagged. (a) Maintaining a parallel rule has the same long-term liability as .wl-btn did (see 2026-05-01 prior entry). (b) Overriding Splunk's bundled .btn-primary is risky — it could affect Splunk-native UI elsewhere on the dashboard (Edit / Export / Cancel buttons in Splunk modals). (c) is the same playbook as the wl-btn migration: 5 callsites in 4 files, single-line search-and-replace per file, zero handler changes (handlers find by #id, not class). Low — re-add the .btn.btn-success rule to whitelist_manager.css and revert the 5 callsite edits. The .btn.btn-danger and .btn.btn-warning muted rules (also build 634) are unaffected — those colours have no Splunk bundled equivalent so we keep owning them. Maintenance rule (search-and-replace as a workflow): when removing a parallel CSS taxonomy, the operation is a class-rename via grep, NOT a refactor. The pattern: (1) grep btn-success for every callsite, (2) Edit each callsite's class string in place, (3) delete the now-orphan CSS rule, (4) verify no callsites remain. Components-based apps would centralize this in one template; vanilla-JS Splunk apps render HTML strings inline per module, so consistency relies on grep + discipline. When facing a similar audit (ANY parallel-CSS-system removal), reach for the search-and-replace workflow first; resist the urge to "abstract" or "centralize" because there is no shared template layer to put the abstraction in.
2026-04-29 Declare security-hardening track CLOSED at build 629 (round 9). No further proactive hardening rounds. Future inbound work that does NOT match a re-opening signal (CVE, production incident, external audit finding, Q3 routine surfacing major-version compat work, or methodology shift to a fresh fuzz surface) is feature work, not hardening work. (a) Continue with rounds 10+ at the same cadence; (b) move to a 6-month re-audit cycle with no per-round structure; (c) formal closure with explicit re-open criteria. 9 rounds covered every reasonable security gap. Round 8 fuzz found 0 bugs; round 9 was pure housekeeping with no app.conf [install] build bump — the natural diminishing-returns signal. CI gates (4 Semgrep rules + doc-drift + quarterly pip-audit + unit tests on every PR), live FIM monitoring (15s + ~2s paths + dual-store baseline), recurring audits (Q3 2026 version-pinning routine), and Sigstore signing of releases now make the system self-sustaining without further rounds. Continuing past clear diminishing returns risks (a) hardening fatigue masking real signal, (b) churn-without-substance erosion of the changelog's signal-to-noise, (c) future contributors expecting a "round N" cadence and queuing low-value work to fit. Closing prevents this. Low — any re-opening signal (CVE, incident, audit finding, methodology shift, major-version compat) immediately re-opens with a new round entry. Closing is a status declaration, not an architectural change. The closing summary at the top of CHANGELOG.md is the canonical artifact; future contributors read it before queuing security work. Maintenance rule: do NOT propose "round 10" by default. The next round must be triggered by an inbound signal, not by schedule.
2026-05-19 Fix _execute_replay_revert_csv by delegation to canonical revert_csv_pipeline (build 663), mirroring the pattern used by create_rule/delete_rule/delete_csv handlers. Add "revert" as an alias for "revert_csv" in REPLAY_HANDLERS so the dispatch table matches the handler's stored action_type literal. (a) Delete _execute_replay_revert_csv and the dispatch entry entirely, since wl_handler.py:6761 short-circuits revert approvals to self._revert_csv and the replay path is dead in production; (b) fix the in-function bugs in place (resolve path before calling get_versions_dir, pass csv_path not csv_file to read_version_manifest, read version_filename not version_id, fix the manifest iteration); (c) replace the whole body with a delegation to revert_csv_pipeline. The replay function had three dormant bugs (no-arg get_versions_dir(), wrong-arg read_version_manifest, version_id vs version_filename field mismatch). (a) Removing the dispatch entry creates an architectural-contract trap: future contributors who consolidate approval paths will discover the replay layer is half-built and either silently broken or missing-by-design. (b) Fixing in place keeps a parallel implementation of revert logic that has to track every change to the direct-handler path (wl_handler.py:_revert_csvrevert_csv_pipeline); two sources of truth diverged exactly the way (b) would invite. (c) Delegation collapses both paths to the same pipeline call — replay becomes a thin wrapper over the same business logic, which is what the other approval handlers already do (create_rule_pipeline, delete_rule_pipeline, delete_csv_pipeline). Coverage rose 79% → 92% on wl_replay.py because the new body is small + fully reachable from tests. Low — git revert d267ea3, re-introduces the broken body. The dispatch entry alias ("revert" → _execute_replay_revert_csv) is a separate concern and can stay if the underlying handler is removed — REPLAY_HANDLERS aliasing was added defensively to match future routing intent. Maintenance rule: when a replay handler exists for an action_type, it MUST delegate to the same canonical pipeline the direct-handler path uses. Do NOT reimplement the business logic in the replay layer — single source of truth keeps drift impossible.
2026-05-20 Partial Node-20 deprecation bump in release.yml — bump actions/checkout@v4 → @v6 and actions/setup-python@v5 → @v6 to close §8 side-finding #1 from the 2026-05-13 Sigstore dry-run. Leave sigstore/cosign-installer@v3 deliberately unbumped despite v4.1.2 being GA. The 13 other workflows (a11y-audit.yml, appinspect.yml, appinspect-api.yml, ci.yml, codeql.yml, docs.yml, e2e-full.yml, e2e-smoke.yml, integration-tests.yml, pip-audit.yml, scorecard.yml, secret-scan.yml, semgrep.yml, validate-and-package.yml, zap-baseline.yml) ALSO pin Node-20-era versions and need a separate sweep. (a) Full project-wide sweep of all 14 workflows in one commit, including cosign-installer v3→v4; (b) bump everything except cosign-installer (signing-critical) in a single sweep; (c) bump only the two actions §8 explicitly called out + only in release.yml, defer rest to dedicated cycles. (a) bundles a signing-critical action change (cosign-installer v3→v4) with routine Node-version bumps, invalidating the 2026-05-13 §8 verification in a non-atomic commit — if a verification step regresses, the cause could be the signing-installer OR any of the other bumps. Recovery requires bisecting across 14 files. (b) is the right scope long-term but is still a 13-workflow change that needs CI verification for each; doing it in this session would mean shipping ~14 untested workflow changes. (c) keeps the change surgical: 2-line edit, no Sigstore re-verification needed (the signing path is untouched), and the 2026-05-13 §8 dry-run remains the authoritative verification artifact. The 2026-09-16 GitHub deprecation deadline is ~4 months away — there's runway to do the full sweep in a dedicated cycle with per-workflow CI verification. The two flagged actions are the ones the §8 dry-run actually warned about, so the closure has a direct provenance link to the side-finding. Low — git revert the bump commit reverts to v4/v5 pins; the 2026-09-16 deadline is the hard constraint, so revert is safe as long as the full sweep happens before then. Maintenance rule (cosign-installer bumps): bumping cosign-installer@v3 → v4 MUST be done in a commit that also re-runs docs/RELEASE_CHECKLIST.md §8 on a fresh v0.0.0-sigstore-test-N+1 tag. The §8 verification is keyed to the signing-installer version; changing it invalidates the static verification artifact. Maintain the same rule for any future signing-installer major bump. Additional checklist item (per cosign-installer v4.0.0 release notes, 2025-10-16): cosign-installer v4 supports BOTH Cosign v2.x and v3+; the v3→v4 action bump itself is safe AS LONG AS the cosign-release: pin stays on v2.x. If you ALSO bump cosign-release to v3+ at the same time (or later), the cosign sign-blob calls in release.yml MUST add --bundle because the v3+ sign-blob signature format changed and the flag became required. Without it the workflow either fails outright or (worse) emits signatures in a format cosign verify-blob rejects — silent supply-chain regression. Documented in the release.yml inline comment AND here so both source-of-truth locations carry the warning.
2026-05-20 Full Node-20 sweep — bump all remaining 14 workflows in one bundled commit (a11y-audit, appinspect, appinspect-api, ci, codeql, docs, e2e-full, e2e-smoke, integration-tests, pip-audit, scorecard, secret-scan, semgrep, validate-and-package, zap-baseline). Bumps: actions/checkout@v4→v6, actions/setup-python@v5→v6, actions/setup-node@v4→v6, actions/upload-artifact@v4→v7, plus docs.yml-only Pages bumps (configure-pages@v5→v6, upload-pages-artifact@v3→v5, deploy-pages@v4→v5). Closes the "13 other workflows need a separate sweep" caveat from the earlier 2026-05-20 release.yml row. (a) Continue with per-workflow CI verification, one commit per workflow; (b) skip workflows that are scheduled-only (codeql, secret-scan, scorecard, pip-audit, semgrep, zap-baseline) since CI failure there has lower urgency; (c) bundle all 14 in one commit, accept that CI will surface any breakage on the next run-trigger per workflow. (a) would be 14 commits with ~14 CI cycles to observe; for actions whose changelogs are pure Node-version bumps (checkout v5+v6, setup-python v6, setup-node v5+v6, upload-artifact v5+v6 all explicitly "Node-version-only" releases) this is overkill. Per-workflow CI gives near-zero new signal because the bumps share a single root cause. (b) creates a stale-pin trap: scheduled workflows that don't run for weeks would silently miss the 2026-09-16 deadline. (c) is the right tradeoff because: (i) bumps verified safe via official release-notes read for each major version transition, (ii) usage patterns are the most-stable defaults (with: name:/path: for upload-artifact; with: cache: "npm" for setup-node; default checkout; pinned python-version: for setup-python), (iii) the bundled commit makes revert trivial — single git revert undoes everything if a regression surfaces. Specific risk areas verified before bumping: setup-node v5's package.json-driven auto-cache is moot because all 4 callers already set explicit cache: "npm"; upload-artifact v7's opt-in archive: false doesn't affect default zip-upload behavior; all Pages bumps maintain stable input contracts. Low — git revert the bump commit. Each from/to version pair is independently safe per release notes; if a future GitHub-side regression breaks one of these versions, a targeted partial revert is possible by re-pinning that specific action in one workflow. Splunk-side actions (splunk/appinspect-api-action@v3.0.5) intentionally NOT bumped — that's an external action with its own cadence and isn't in scope for the Node-20 deprecation track. cosign-installer@v3 carve-out preserved per the 2026-05-20 release.yml row above (its --bundle / Sigstore-verification dependencies require a dedicated cycle).
2026-05-20 Admin-tier daily cap on row_reorder + column_reorder (default 50 per period); superadmins STAY exempt (build 666). (a) No cap anywhere — leave audit-trail pollution as a known accepted risk. (b) Cap only the admin tier (wladmin) at 50 — the chosen Option C; superadmins remain exempt. (c) Cap admin AT 50 AND superadmin at a higher number (e.g. 200) — partial superadmin defense. (d) Cap every tier including superadmin at a single number, no exemption. User on 2026-05-20 reported superadmin1 was able to reorder 16 rows while the Activity tab misleadingly showed "LIMIT" (because the analyst-tier counter passed the analyst-tier cap, which doesn't apply to admins/superadmins). The real gap: reorder ops emit one audit event per drag yet had ZERO admin-tier cap, so a rogue admin could pollute index=wl_audit indefinitely to bury malicious actions in noise. (a) leaves the gap fully open at the admin tier — rejected because wladmin is a delegated role intended for SOC analysts who shouldn't be able to fill the audit index. (c) and (d) cap superadmins, which is rejected because: (1) superadmin compromise is total compromise — they can re-raise the cap, reset cooldowns via reset_cooldowns.sh, or deactivate emergency lockdown — so a cap is not a meaningful additional defense once that role is owned; (2) two-superadmin enforcement already exists for the highest-stakes ops (lockdown deactivation requires a DIFFERENT superadmin); (3) the post-compromise attribution channel for superadmin abuse is Splunk's built-in _audit index (documented in INSTALLATION.md) which lives outside this app's control plane and can't be tampered from inside; (4) capping superadmins would also block legitimate bulk operations during incident response when superadmins are the only role with the lockdown-exempt action set. Known residual risk acknowledged here: a single rogue superadmin can still indefinitely pollute index=wl_audit via reorder noise. That risk is deferred to the post-compromise attribution path via _audit, not addressed by this fix. UI fix (paired with the cap): added usageCellHtmlForUser in appserver/static/control_panel.js so the Activity table picks the correct tier per user — superadmin rows render a muted count with "No cap configured for this tier" tooltip instead of the misleading red "LIMIT" badge. Low — DEFAULT_ADMIN_LIMITS["row_reorder"] / ["column_reorder"] set to -1 (unlimited) in bin/wl_constants.py would disable the cap without removing the code. To extend the cap to superadmins later, drop the and not is_superadmin(user_roles) guard in _save_csv (~line 4823) — that's a one-line edit. Maintenance rule: when adding a NEW admin action that emits an audit event per-op (e.g. a new reorder-style bulk action), add it to DEFAULT_ADMIN_LIMITS AND extend the if limit_action not in ("row_reorder", "column_reorder"): filter in _save_csv to include the new action. Otherwise it will silently inherit the no-cap default. Test pattern: tests/unit/test_limits.py::TestAdminReorderCaps is the template.
2026-05-20 REVERSAL of the previous 2026-05-20 row: admin-tier reorder cap now ALSO applies to superadmins, single shared cap (build 667). Why reversed: see 2026-05-20 row above. (a) Keep build-666 design (superadmins exempt; admin tier at 50); (b) cap superadmins AT a HIGHER number than admins (e.g. row_reorder=200, column_reorder=100) — preserves tier hierarchy and allows IR bulk ops; (c) single shared cap (50/50) — drop the not is_superadmin(user_roles) guard at bin/wl_handler.py:4823, keep DEFAULT_ADMIN_LIMITS values unchanged. The build-666 design framed superadmin exemption as "post-compromise attribution via _audit index, not prevention". User decided 2026-05-20 (follow-up to round-2 QA review) that defense-in-depth was worth the small ergonomic cost: a rogue superadmin must now either stay under 50 OR call set_admin_limits to raise their own cap, and set_admin_limits is itself rate-limited at 5/day AND audited AND requires a tamper-resistant HMAC-signed cooldown counter to bump (build 553 hardening). So polluting via reorder beyond 50 forces the attacker to leave a second, audit-emitting trail (action=set_admin_limits) that correlates trivially. (a) leaves the gap open. (b) is the more nuanced design — preserves the role hierarchy and matches incident-response operational ergonomics — but adds ~80 LOC for a new DEFAULT_SUPERADMIN_LIMITS dict, two-tier UI rendering, and parallel tests. (c) is one-line edit on the guard + one-line edit on the counter-mirror + UI's userTier resolver folds superadmin → admin path for adminCapped columns. User chose simplicity over hierarchy preservation: "Single shared cap (50/50)" — verbatim from the 2026-05-20 AskUserQuestion answer. Known new constraint: legitimate incident-response bulk reorganization by a superadmin can now hit the cap mid-incident; remediation is to raise the cap via Admin Settings (set_admin_limits), which is the same path a rogue user would take, but the operator's intent is visible in the audit-correlating set_admin_limits change-history entry. Low — re-add the and not is_superadmin(user_roles) guard at the same line (and mirror at the counter increment). Reversal cost is identical going both directions; the choice is purely a tunable security/UX trade-off. Maintenance rule: do NOT introduce per-tier limit dicts (DEFAULT_SUPERADMIN_LIMITS) without a new DECISION_LOG row — the build-667 design is "one cap per action, all admin tiers share it". The test TestSuperadminReorderCapBuild667::test_admin_limits_no_separate_superadmin_keys is the mechanical enforcement of this rule and will fail loudly if a future refactor breaks the assumption.
2026-05-20 log_event_emit cap at 30/period across ALL tiers (build 667). Adds a counter to _log_event (bin/wl_handler.py) for the csv_exported / csv_imported / audit_exported frontend-emitted audit events. (a) Leave uncapped — log_event is the only remaining uncapped audit-emitting REST action callable by any role, but it's gated to 3 event subtypes tied to legitimate user actions; (b) cap only the admin/superadmin tier (matching "rogue elevated role" framing); (c) cap all tiers at 30/period via a single shared analyst-tier counter (the simplest design — one counter, one cap, applies uniformly). The build-666 reorder cap closed the row/column drag-pollution vector. log_event was the remaining vector: a rogue user at ANY tier can loop {action:"log_event", event_action:"csv_exported", ...} to inflate index=wl_audit volume. Legitimate export+import even for power users is single-digit per day; 30 catches automation/abuse without false positives. (a) accepts the vector — rejected because the user's broader audit-trail-pollution concern applies regardless of tier. (b) is the framing-matched option but adds asymmetry: analysts can ALSO loop this action, so analyst-tier exemption is the strictly weaker defense. (c) is operationally simplest: single counter (the analyst counter under key log_event_emit), single default in DEFAULT_LIMITS, single rate-check call inside _log_event. Admins and superadmins both increment the same counter so the cap is uniformly enforced. Low — change DEFAULT_LIMITS["log_event_emit"] to -1 in bin/wl_constants.py to disable the cap (sentinel parity with analyst tier); the gate code stays in place. To revert fully, remove the _check_daily_limit call and the _increment_daily_limit call inside _log_event (4-line edit). Maintenance rule: if a future action emits audit events on a per-call basis (e.g., a new "log_telemetry" endpoint), add it to DEFAULT_LIMITS AND apply the same _check_daily_limit + _increment_daily_limit pattern inside the handler. The test pattern lives in tests/unit/test_limits.py::TestLogEventEmitCap.
2026-05-20 Expose row_reorder + column_reorder admin caps for runtime tuning via Admin Settings UI (build 668). Adds the two keys to the LIMIT_KEYS allow-list inside _action_set_admin_limits and adds two form rows to the Admin Settings tab. (a) Leave the caps hardcoded at 50 (DEFAULT_ADMIN_LIMITS fallback) — accept as a "known operational limitation"; (b) Backend-only fix — add keys to allow-list but skip UI form, leaving REST API as the only way to tune; (c) Backend + UI fix in one bundle (chosen). While live-testing build 667 as superadmin1 on 2026-05-20, attempted set_admin_limits {row_reorder: 2} returned "no changes". Root cause: _action_set_admin_limits has a closure-local LIMIT_KEYS tuple that gates which keys may be persisted. Build 667 added the two keys to DEFAULT_ADMIN_LIMITS in wl_constants.py but missed updating this allow-list, so the cap was frozen at 50 with no runtime tuning path. (a) leaves SOCs unable to tighten the defense (e.g. for a paranoid posture wanting 10/period) or loosen it for legitimate IR (e.g. 100/period during cleanup). (b) is incomplete — admins use the UI; REST-only tuning is invisible to them. (c) is the full fix: 1-line backend tuple expansion + ~10-line UI form addition (the form is built from a fields array; adding two entries is the same pattern as every other cap in the array). The desc text for both new fields explicitly calls out the build-667 "applies to BOTH admin AND superadmin tiers" semantics so operators don't assume the section's "Super-admins are exempt" heading text applies to these specific caps. Low — revert the LIMIT_KEYS additions (caps freeze at 50 again, but stay enforced) and remove the two fields array entries (UI form returns to prior state). Cap math + enforcement path unaffected. Maintenance rule: every numeric key added to DEFAULT_ADMIN_LIMITS MUST also be added to _action_set_admin_limits.LIMIT_KEYS, OR a desc note in DECISION_LOG must explain why it stays frozen at the default. The regression test tests/unit/test_limits.py::TestSetAdminLimitsAllowListBuild668::test_limit_keys_includes_reorder_caps reads the source-of-truth tuple from wl_handler.py and fails if either key drops out — that's the mechanical enforcement.
2026-05-22 Baseline branch protection on main (force-push + deletion + linear history + conversation resolution); full Scorecard Branch-Protection / Code-Review criteria deferred via dismiss-with-rationale. Closes Code Scanning alerts #1 (BranchProtectionID) and #74 (CodeReviewID). Re-evaluation trigger: a co-maintainer joins, OR community contribution volume justifies forced-PR-review workflow. (a) No protection at all (status quo before this turn); (b) baseline protection + dismiss the two Scorecard sub-criteria with rationale (chosen); (c) full Scorecard-criteria protection (require PR reviews + status checks + signed commits) with the maintainer on a self-bypass list; (d) full Scorecard-criteria protection with NO bypass (forces self-merge via PR every time). (a) leaves main unprotected — force-push and deletion are real risks even for a solo maintainer (typo on the wrong branch). (c) is the "looks-clean-on-Scorecard" path but the bypass list defeats the intent of the check; a future-Claude or future-contributor reading the Scorecard score would be misled into thinking PRs are reviewed. (d) is the strict version of (c) but adds friction for a single maintainer that produces no security value (self-review is not a real second pair of eyes). (b) closes the realistically-exploitable gaps (force-push, deletion, non-linear merges) while being HONEST in the dismissal rationale about why the higher-tier criteria don't apply yet. The dismissal comments on alerts #1 + #74 explicitly cite "single-maintainer pre-GA repo" and "reconsider when co-maintainer joins post-GA" — a future re-evaluation has a clean trigger. Low — to escalate to (c) or (d), reopen alerts #1 + #74 via the Code Scanning UI, then gh api PUT /repos/.../branches/main/protection with required_pull_request_reviews populated. To downgrade back to (a), gh api DELETE /repos/.../branches/main/protection. Re-evaluation rule: when a co-maintainer joins, do NOT just merge the existing baseline rules — explicitly re-open this row's discussion because the trade-off changes (review enforcement becomes a real second pair of eyes, not friction-without-value).
2026-05-22 OpenSSF Best Practices badge (bestpractices.dev) application deferred to post-GA. Closes Code Scanning alert #73 (CIIBestPracticesID) via dismiss-with-rationale. Re-evaluation trigger: community adoption volume post-v1.0.0 GA justifies the months-long external review process. (a) Apply for the badge now (open-ended weeks-to-months external review, ~15 OSS-governance criteria to demonstrate); (b) dismiss-with-rationale and defer the application to post-GA (chosen); (c) ignore the alert (leave open without explanation). The OpenSSF Best Practices badge is a meaningful credibility signal for OSS adopters, BUT the application process requires demonstrating ongoing OSS-governance practices (multi-maintainer review, established vulnerability-disclosure SLA, public roadmap process, contributor onboarding metrics) that this single-maintainer pre-public project does not yet have signal for. The project already publishes most of the underlying artifacts (SECURITY.md disclosure policy with explicit SLA table, CONTRIBUTING.md, CHANGELOG.md, Sigstore-signed releases) — what's missing is the operational track record. Applying now would likely come back "needs more activity" and burn the badge maintainer's review budget. (a) is the technically correct response but burns months for low confidence the application would pass given the project's age; (c) leaves an unexplained alert on the public Security tab, which itself looks worse than an explicit "deferred" dismissal. (b) is honest: the dismissal comment cites the deferral and the re-eval trigger; a future maintainer/contributor sees clean intent. Low — to escalate to (a), reopen alert #73 via Code Scanning UI and start the application at https://www.bestpractices.dev/. To revert to (c) [not recommended], just don't re-open. Re-evaluation rule: trigger the application when EITHER: (i) the repo accumulates 3+ external contributors with merged PRs, (ii) Splunkbase install count reaches a non-trivial number (e.g. ≥50), OR (iii) an inbound user asks for the badge as a deployment-approval gate. Until one of those, the application would be performative.
2026-05-22 Disable GitHub's Code-scanning "default setup" (code-scanning/default-setup state=not-configured); keep our advanced .github/workflows/codeql.yml as the canonical CodeQL config. Closes the "CodeQL job status was configuration error" failure that hit every push to main since 2026-05-21 (when the repo flipped public and GitHub auto-enabled default-setup). The SARIF rejection text was the giveaway: "CodeQL analyses from advanced configurations cannot be processed when the default setup is enabled." (a) Disable default-setup; keep the workflow file (chosen); (b) Delete .github/workflows/codeql.yml and rely solely on default-setup; (c) Configure default-setup's query suite to extended via the /code-scanning/default-setup PATCH API and delete the workflow file (middle ground — broader queries via default-setup, lose schedule control). The workflow's own preamble comment explicitly chose security-and-quality ("CWE-coverage + maintainability hints — not just security") over security-extended, which IS the workflow author's stated coverage preference. (b) downgrades the query pack to default (the smaller default-setup suite) AND loses our weekly cron schedule AND loses Phase 2.16 customizations (concurrency-cancel, if: github.event.repository.private == false self-gating, matrix split). (c) recovers the broader query pack via the extended knob but still loses the schedule + concurrency + private-repo guard customizations baked into the workflow. (a) is the only path that preserves all four Phase 2.16 design choices — at the cost of one PATCH call to disable default-setup. The disabling is reversible in one symmetric PATCH call. GitHub auto-enabled default-setup at repo-flip-to-public time without our involvement, so the two systems were always going to conflict the first time the workflow tried to upload; the conflict surfaces as the SARIF rejection rather than a hard fail at the GitHub UI level. Low — to revert: gh api -X PATCH repos/RelativisticJet/wl_manager/code-scanning/default-setup -f state=configured -f query_suite=default (or -f query_suite=extended for broader coverage). To switch to option (b) entirely: same enable-call PLUS delete .github/workflows/codeql.yml. Maintenance rule: do NOT add default-setup configuration to the repo via the Security UI — the workflow file is the canonical source. If you want broader query coverage than security-and-quality, edit the queries: input on the init step in codeql.yml (the comment block at the top documents the security-extended knob already), do NOT switch to default-setup. The mechanical enforcement is the SARIF error itself: any attempt to re-enable default-setup will turn CodeQL red again until one of the two systems is disabled.
2026-05-22 Accept GHSA-vfmq-68hx-4jfw / CVE-2026-41066 (lxml XXE in iterparse() + ETCompatXMLParser() with resolve_entities=True) as tolerable residual risk; defer fix until splunk-appinspect upstream relaxes its lxml<6.0.0 pin. Dependabot alert #2 dismissed (tolerable_risk); pip-audit.yml carries --ignore-vuln GHSA-vfmq-68hx-4jfw so the CI gate stays green. Re-evaluation trigger: splunk-appinspect releases a version with Requires-Dist: lxml>=6.1.0 (or removes the lxml constraint entirely). (a) Accept the residual risk and carve out (chosen); (b) override the upstream pin by force-installing lxml>=6.1.0 in requirements/appinspect.in (pip-compile would reject as unsatisfiable resolution; even if it didn't, lxml 6.x removes API surfaces appinspect 4.2.0 may call against — risk of runtime breakage during AppInspect runs); (c) downgrade or replace splunk-appinspect (no current alternative offers the same Splunkbase certification check); (d) ignore the alert with no documentation (leaves an unexplained open alert on the public Security tab — strictly worse than (a)). The CVE requires UNTRUSTED XML input to trigger local-file reads via XXE. Our pipeline invokes lxml exclusively inside .github/workflows/appinspect.yml, which runs splunk-appinspect against OUR OWN .spl artifact built by scripts/package.sh from this repo's tree. The XML files it parses (default/data/ui/views/*.xml, default/*.conf parsed for SimpleXML) are all under our source control — an attacker would need source-commit access to inject malicious XML, at which point XXE-via-lxml is far down the threat list (they could commit malicious Python in bin/ directly). The CVSS 7.5/HIGH score reflects the general-population threat model (untrusted public input), not our trusted-input CI use case. (b) is technically possible but unsafe: splunk-appinspect's <6.0.0 constraint is structural (lxml 6.x changed several etree defaults and removed deprecated APIs) — overriding it puts us in an unsupported configuration that may produce false AppInspect results without warning. (c) has no alternative — splunk-appinspect is the only tool that runs the published Splunkbase certification checks; we cannot trade it out without abandoning the Phase 3.x publishing path. (a) is the honest answer: document the residual risk, gate via the pip-audit ignore-rule, file the upstream issue, re-evaluate at the next splunk-appinspect release. Low — to revert: remove the --ignore-vuln GHSA-vfmq-68hx-4jfw line from pip-audit.yml, reopen the Dependabot alert via the Security tab UI, and force-pin lxml in requirements/appinspect.in (accepting whatever runtime breakage follows). To accelerate exit from the carve-out: file/track the upstream splunk-appinspect issue, monitor its Requires-Dist for the bump, then run scripts/regen_requirements.sh to pull the new transitively-resolved lxml. Maintenance rule: when adding a new --ignore-vuln entry to pip-audit.yml, REQUIRE a matching DECISION_LOG row that names the GHSA/CVE, the reachability analysis, AND the re-evaluation trigger. The pip-audit.yml carve-out comment must reference back to the DECISION_LOG row date so a future maintainer can correlate the two.
2026-05-24 Delete the residual .wl-dark class hook and all JS detection machinery (build 669). Continues the 2026-05-01 (build 637) decision to drop light-theme support. Removes detectDarkTheme() from wl_ui.js, ensureDarkTheme() from notifications.js, the isDark ternaries in control_panel.js info-bubble rendering, and the if (UI.detectDarkTheme()) wrapper in whitelist_manager.js. The :root block in whitelist_manager.css is now the single theme source; nothing keys off .wl-dark anywhere. Visual-regression baselines (tests/e2e/visual_baselines/*.json) updated from body_classes: ["wl-dark"] to body_classes: [] to reflect the new reality. (a) Aggressive: delete all .wl-dark machinery (chosen); (b) keep detectDarkTheme() as a documented "soft hook" for external code (Splunk plugins, debugging) but kill every internal consumer — this was the 2026-05-01 entry's framing; (c) leave the dead JS branches in place and just fix the stale comments. The 2026-05-01 row left the .wl-dark class as "harmless redundancy" — but in practice it accumulated 5 sites of dead code (1 detector + 1 ensure-helper + 3 caller sites), 5 stale visual-regression baselines locking in the class as expected, and stale comments in 2 files claiming "19 .wl-dark X selectors continue to match" when 0 actually did (the CSS-side cleanup already flattened them). (b) preserves the soft hook but doesn't justify keeping the 4 unreached consumers — and there are no known external consumers in this app's deployment, since wl_manager is the only thing using the wl- prefix in any Splunk instance. (c) doesn't fix the actual stale-comment drift the user originally surfaced as Task #5; "leave dead code" silently becomes the long-term default. (a) is consistent with the documented preference for "Truth > Speed" and "Don't add abstractions beyond what the task requires" — the soft hook IS an abstraction beyond what the task requires. Medium — re-introduce: detectDarkTheme() function in wl_ui.js (5 lines, just $("body").addClass("wl-dark"); return true;), restore the export entry, restore the if (UI.detectDarkTheme()) wrapper in whitelist_manager.js, restore the isDark ternaries in control_panel.js info-bubble (5 lines), restore ensureDarkTheme() in notifications.js (10 lines). Net ~25 lines. Light-theme proper still requires the heavier work documented in the 2026-05-01 row's "Reversal cost" column. Maintenance rule: visual-regression baseline files MUST be updated in the same commit as any change to body-class application — otherwise tests/e2e/test_visual_regression.cjs fails the next CI run with a confusing "body_classes mismatch" message rather than a useful "you intentionally removed wl-dark" one.
2026-06-01 Remove [package].check_for_updates = false from default/app.conf; cut v1.0.1. AppInspect 4.2.0 treats the setting being false as a soft warning (check_for_updates_disabled rule), which we triaged as acceptable during Phase 1.6 (see docs/APPINSPECT_FINDINGS.md F12 — "value is fine, only the stanza placement matters"). Splunkbase's own package-validation gate at upload time enforces the same condition as a hard rejection with error "The check_for_updates field found in app.conf must not be disabled." Result: wl_manager-1.0.0.spl was rejected during the v1.0.0 Splunkbase upload, requiring v1.0.1 as a packaging-only re-release. (a) Remove the check_for_updates line entirely; let Splunk's default (true) apply (chosen); (b) explicitly set check_for_updates = true; (c) leave at false and try to argue with Splunkbase certification reviewers; (d) keep at false and distribute v1.0.0 outside Splunkbase only (GitHub releases). The setting itself is not load-bearing for any feature of this app — check_for_updates controls whether Splunk's built-in launcher polls Splunkbase for new versions of the app, which is the standard customer-facing capability we have no reason to disable. The false value was inherited boilerplate, not a recorded product decision (DECISION_LOG.md has no prior row justifying it; the app.conf comment block at lines 23-33 only documented the SLIM-vs-AppInspect stanza-placement question, not the value choice). (a) is minimal — removing the line is a one-character-less change than setting it to true and is harder to silently re-introduce. (b) is functionally equivalent but slightly noisier (an explicit true invites future contributors to "I'll just flip this to false to disable update prompts" without checking DECISION_LOG). (c) is unproductive — Splunkbase's reviewers cannot waive a hard package-validation gate. (d) defeats the entire Phase 4 publishing milestone; this app exists to be installed by SOC teams via Splunkbase, not by hand-copying a .spl. (a) wins on minimality + maintainability. Low — re-add check_for_updates = false to [package] (one line in default/app.conf), bump build, cut a new release. Splunkbase upload will reject again, so the practical reversal cost is "do not re-add this line." Maintenance rule: when AppInspect API reports a check_for_updates_disabled warning during a CI run on a PR that touches app.conf, do NOT triage it as "acceptable warning" without ALSO simulating the Splunkbase upload gate (or upload an internal test app via the Splunk Dev account first). The two pipelines disagree on this rule, and the discrepancy is exactly the class of failure this row exists to record. The mechanical enforcement is a one-line grep test (! grep -E '^check_for_updates\s*=\s*false' default/app.conf) that should be added to scripts/validate.sh in a follow-up — until then, this DECISION_LOG row is the institutional memory.
2026-06-07 ESCALATION DECISION — pause v1.x Splunkbase Cloud Vetting iteration; file publisher support ticket asking for the authoritative supported-version list. v1.0.8's ">=8.1.0,<10.0.0" was rejected with the CONTENT-class wording ("no supported version of Splunk Enterprise: >=8.1.0,<10.0.0"), confirming the comma-no-space syntax PARSES correctly (no "Illegal version specification") but Cloud Classic's supported-version set is somewhere we can't reach through guesses. 9 empirical data points across F13-F19 + v1.0.0 pre-release + v1.0.8 do NOT triangulate to any defensible next single-version-string or range. The Splunkbase AI explainer's recommendations have been inconsistent (5 different forms across v1.0.4-v1.0.8 explainers; v1.0.8's ">=9.0.0" is literally the v1.0.5 value that already failed). The documented escalation path from 2026-06-02 row's reversal-cost column has been pre-trigger for 4 days; this row formalizes the decision. Support ticket draft is in docs/SPLUNKBASE_SUPPORT_TICKET.md ready to send. (a) Escalate to Splunkbase publisher support (chosen) — file a ticket asking directly which Enterprise versions are on Cloud Classic's supported list. Iteration cost (8 releases over 7 days, all publicly visible on Splunkbase listing 8800) now exceeds ticket cost. (b) Try ">=0.0.0,<10.0.0" (radical floor) — one more iteration of the same method that has produced 7 content rejections; low-confidence guess. (c) Try a specific older version like "9.2", "9.1", "9.0", "8.2", "8.0" — speculative single-string; same iteration pattern. (d) Drop Cloud Vetting target and ship Enterprise-on-prem only via Splunkbase listing 8800 standalone/distributed install path. (a) wins on EV: every guess costs another public release; the ticket costs one email and Splunkbase support's typical response time. (b) is the AI's "broaden further" trajectory but produces no new information if rejected — we'd still need to escalate. (c) has been the failed pattern from F13-F16. (d) is the documented fallback if Splunkbase support confirms Cloud Vetting is currently unreachable (e.g., supported list is internal and not declarable by third-party apps); we'd update INSTALLATION.md to clarify on-prem-only and ship without further iteration. The v1.0.8 manifest stays as-is until the ticket response arrives: the comma-no-space syntax is now established as the correct parse form even if the version set doesn't match. No new release until the support response tells us what value to declare. Low — once Splunkbase support responds with a specific value, cut v1.0.9 with that value verbatim. If support confirms on-prem-only fallback (option d), the reversal is updating INSTALLATION.md / README.md to remove Cloud Vetting language; the codebase stays as-is. Maintenance rule captured: when iterative-discovery produces N consecutive same-class failures with no triangulation path, escalate to direct query rather than continuing the iteration. This is the FIRST application of the escalation rule the prior rows kept deferring; future maintainers facing similar SLIM/AppInspect discovery problems should reference this row + docs/SPLUNKBASE_SUPPORT_TICKET.md as the playbook. Honesty rule applied: the prior 6 reversal rows kept saying "if this also fails, the next move is the support ticket" but kept iterating anyway; this row marks the actual moment we paid the deferred cost. The pattern (iteration-cost-exceeds-direct-query-cost-but-you-keep-iterating) is itself a v1.1 backlog item for the ~/.claude/qa-failure-modes-global.md library.
2026-06-06 Whitespace fix: comma WITH NO SPACE ">=8.1.0,<10.0.0" after v1.0.7's comma+space form was rejected with the SAME Illegal version specification syntax error. v1.0.7 shipped with ">=8.1.0, <10.0.0" per the previous row's AI-literal recommendation. SLIM rejected with the same syntax-class error as v1.0.6's space-conjunction. The Splunkbase AI's v1.0.7 explainer explicitly corrects its earlier recommendation: "SLIM's version parser expects a valid specifier set without whitespace around commas. The space after the comma causes the version spec to be considered invalid. Fix: remove the whitespace around the comma." The previous row's "comma form" was right; what was wrong was the SPACE after the comma. Phase 1.7's ">=9.0,<10.0" is now the third confirmation of the correct syntax: it was rejected with the CONTENT wording ("no supported version"), not the SYNTAX wording — proving the comma-with-no-space form parses correctly. The 3-position contrast (space-only / comma+space / comma+no-space) now fully isolates the whitespace sensitivity. (a) ">=8.1.0,<10.0.0" (chosen) — comma with no spaces, matching Phase 1.7's known-parsable form + AI's v1.0.7 explicit fix; (b) ">=8.1.0, <10.0.0" (space after comma) — empirically rejected today; (c) Splunkbase publisher support ticket. (a) is the AI's literal corrected recommendation in the v1.0.7 failure explainer AND matches Phase 1.7's confirmed-parsable syntax. (b) is the value we're moving away from — already empirically rejected. (c) is the documented escalation path if v1.0.8 ALSO fails (next iteration after this). Low — change Enterprise value at next re-release. One new maintenance lesson captured: SLIM version parser is WHITESPACE-SENSITIVE around commas. The correct bounded-range form is ">=X.Y.Z,<A.B.C" — comma directly between constraints, no surrounding spaces. The previous row's AI recommendation displayed ">=8.1.0, <10.0.0" with a space after the comma; that space was the parser-breaking element, not the conjunction itself. Pattern across v1.0.6→v1.0.8 iterations: the Splunkbase AI explainer corrects its own prior recommendations in successive failure reports. Each AI recommendation is one empirical-data-point hypothesis to test, not authoritative — read against the cumulative SLIM format history table in docs/SPLUNK_10_COMPATIBILITY.md before applying. Audit row honesty rule applied: the 2026-06-05 evening row's "comma form" guidance was directionally correct but specifically wrong about whitespace; that row STAYS as historical evidence; THIS row corrects it. Six consecutive corrections in 5 days continues the empirical-discovery cadence.
2026-06-05 (row 2, evening) Syntax fix: comma conjunction ">=8.1.0, <10.0.0" after v1.0.6's space form was rejected with a NEW error class — Illegal version specification. Why this row: v1.0.6 shipped with ">=8.1.0 <10.0.0" (space conjunction per AI's morning example). SLIM rejected with manifest.platformRequirements.splunk.Enterprise: Illegal version specification: >=8.1.0 <10.0.0 — fundamentally different error wording from the prior "no supported version" class. The new wording is a syntax error, not a content error: SLIM could not parse the space form as a valid range. The morning row's claim "bounded-range form uses single-space conjunction syntax per the Splunkbase AI explainer's example" was wrong; the AI's literal example USED a space because the explainer just displayed the constraints separately, but the SLIM parser requires comma conjunction. Retroactive disambiguation of Phase 1.7: Phase 1.7's ">=9.0,<10.0" (comma form) was rejected with the "no supported version" content-class wording — NOT "Illegal version specification". This proves the comma syntax PARSES correctly; Phase 1.7's rejection was content-based all along. The 2026-06-04 evening row's analytical correction holds: SLIM parses comma-separated ranges as a valid type. Cloud Classic supported-list shape narrows: Phase 1.7 proved no version in [9.0.0, 10.0.0) matched; v1.0.5 proved no version in [9.0.0, ∞) matched. Therefore Cloud Classic's list is entirely BELOW 9.0.0 — i.e., 8.x only. This matches Splunkbase AI's 8.1.0 floor hint. (a) ">=8.1.0, <10.0.0" (chosen) — comma conjunction per AI's literal recommendation in the v1.0.6 failure report; matches Phase 1.7's known-parsable comma syntax; AI's 8.1.0 floor + narrowed shape inference (8.x only) means this range should match Cloud Classic's list; (b) ">=8.1.0, <9.0.0" — narrower upper bound exactly at the empirically-confirmed boundary; matches 8.x only; (c) "8.2.0" or other single 8.x version — speculative which 8.x is supported; risks another iteration; (d) Splunkbase publisher support ticket. (a) wins because: (1) it's the AI's literal comma form from the v1.0.6 explainer; (2) the 8.x-only shape inference predicts the range matches; (3) keeping the broader upper bound (10.0.0) costs nothing if 8.x is what matches; (4) if a future Cloud Classic upgrade adds 9.x or 10.0 to the list, the broader range still passes. (b) is equivalent on coverage today but tighter — less forward-compatible. (c) is the speculative-single-version path that produced 4 failed releases (9.3, 9.4, 10.0, 9.4 again across F13-F17); the iteration cost has exceeded the publisher support ticket cost. (d) is the documented escalation if (a) ALSO fails — but the chain of evidence (Phase 1.7 comma parses + AI's literal comma form + 8.x-only shape inference) gives high confidence (a) will work. Low — change Enterprise value at next re-release. Two new maintenance lessons captured: (1) SLIM conjunction syntax: bounded semver ranges use COMMA conjunction (">=A, <B"), NOT space (">=A <B"). The AI's morning example happened to display constraints separately but the parser requires comma. This corrects the 2026-06-05 morning row's incorrect maintenance rule about space conjunction. (2) Error-class disambiguation across releases: the same SLIM check (check_that_app_passes_slim_validation_for_cloud) emits at least THREE distinct error wordings — "Expected String value" (type), "Illegal version specification" (syntax), "no supported version" (content). Always quote the EXACT error wording when documenting a failure; the wording is the empirical evidence for which rejection class applies. Audit row honesty rule applied: the morning row's incorrect "space-conjunction" claim STAYS as historical evidence; THIS row corrects it. Five consecutive corrections in 4 days demonstrates that empirical-discovery is the right method for a poorly-documented external system.
2026-06-05 (row 1, morning) Bounded-range trial ">=8.1.0 <10.0.0" after v1.0.5's ">=9.0.0" was rejected — EMPIRICALLY CONFIRMED that SLIM parses semver ranges as type (the prior row's Two outcomes possible analysis was answered). v1.0.5's SLIM rejection wording was: "no supported version of Splunk Enterprise: >=9.0.0" — the literal range string was echoed back, NOT "Expected String value, not '...'". This is the definitive disambiguation point: SLIM PARSED the range as a valid type; the rejection was content-based (no version in [9.0.0, ∞) matched Cloud Classic's supported list). Phase 1.7's "type rejection" conclusion was wrong. Semver ranges work; the question is what range matches Cloud Classic's list. Splunkbase AI explainer NEW recommendation in v1.0.5 failure report extends the floor DOWN to 8.1.0: "Recommended example: ">=8.1.0 <9.2.0". If you cannot determine the exact upper bound immediately, you can temporarily broaden while keeping a sensible cap, e.g., ">=8.1.0 <10.0.0"." The 8.1.0 floor is a meaningful clue — Cloud Classic's supported list may include 8.x versions we hadn't tested. (a) ">=8.1.0 <10.0.0" (chosen) — bounded range, broadest reasonable span that excludes 10.x; AI's "temporary broaden" recommendation; (b) ">=8.1.0 <9.2.0" — AI's narrower example; if Cloud Classic supports 8.x or early-9.x, this works; if it supports 9.3-9.5, this fails; (c) ">=8.1.0" — open-ended floor, same shape as v1.0.5's ">=9.0.0" just lower; (d) Splunkbase publisher support ticket to ask which versions are on Cloud Classic's list directly. (a) wins on coverage: 8.1.0 through 9.x is the widest plausible range. AI explicitly suggested this as the "temporary broaden" form. If 8.x OR any 9.x is supported, this matches. (b) is narrower — if Cloud Classic supports 9.3-9.5 specifically, this fails; risk-too-narrow vs (a). (c) has no upper bound — equivalent shape to v1.0.5's failed ">=9.0.0", just lower floor; if Cloud Classic's list starts at 9.3 (which it doesn't per F13-F16 evidence) this would still fail. (d) is the documented escalation path if (a) ALSO fails; doing it pre-emptively delays release. Major lesson confirmed (v1.0.5 result): SLIM parses semver ranges as a valid type. The v1.0.2-v1.0.4 docs are flagged as containing now-overconfident "ranges are type-rejected" claims; follow-up cleanup commit needed regardless of v1.0.6 outcome. Cloud Classic supported-list shape inference (from F13-F17 results): {9.3 ❌, 9.4 ❌, 10.0 ❌, [9.0.0, ∞) ❌}. The set of supported versions is NOT a contiguous semver range with a low floor. Possibilities: (i) only specific older minors like 9.0, 9.1, or 9.2 are on the list (in which case the v1.0.6 range covers them); (ii) only 8.x versions are on the list (covered); (iii) only a handful of specific 9.x patches we haven't named (covered if any is in [9.0.0, 10.0.0)); (iv) 10.x or 11.x (NOT covered by v1.0.6 — would need a different range). Low — change Enterprise value at next re-release. If v1.0.6 succeeds, future releases keep this bounded range as a permanent declared compatibility surface and stop iterating. If v1.0.6 fails, the v1.0.5 row's "Splunkbase publisher support ticket" escalation path triggers. One new maintenance lesson: bounded-range form ">=X.Y.Z <A.B.C" uses single-space conjunction syntax (NOT comma) per the Splunkbase AI explainer's example. Phase 1.7's ">=9.0,<10.0" used comma; that may have been a syntax issue layered on top of the content rejection. The space-conjunction form is what we should pin to going forward IF v1.0.6 succeeds.
2026-06-04 (row 2, evening) Trial of semver range ">=9.0.0" after "9.4" was also rejected — analytical correction acknowledged. Why this row: v1.0.4 shipped with "9.4" per the row immediately below (2026-06-04 row 1, morning); SLIM rejected "9.4" on Splunkbase upload run 8800-43365 (sequence implied) with the SAME error class as "9.3" (v1.0.1) and "10.0" (v1.0.3). We have now empirically confirmed that ALL THREE single-version strings we tried ("9.3", "9.4", "10.0") are NOT currently on Splunk Cloud Classic's supported list. The prior rows' claim "semver ranges are type-rejected by SLIM" was overconfident. Re-examining the Phase 1.7 evidence: the ">=9.0,<10.0" rejection used the EXACT SAME error wording ("no supported version of Splunk Enterprise: >=9.0,<10.0") as v1.0.3 / v1.0.4 content rejections, NOT the "Expected String value, not [...]" wording of v1.0.2's list-form type rejection. The Phase 1.7 rejection was plausibly content-based (no version in the range matched the supported list at that moment), not type-based. v1.0.5 tests this hypothesis. (a) Try the AI explainer's exact literal recommendation ">=9.0.0" (broadest possible 9.x+ range; chosen via user AskUserQuestion) — if semver ranges ARE accepted by type, this matches the largest possible set of versions; (b) ">=9.4" — narrower range, user's verbal suggestion; (c) ">=9.0.0,<10.0.0" — explicit upper bound, similar to Phase 1.7's rejected form but with patch components; (d) try untested single-version strings like "9.2", "9.1", "9.5" — speculative without empirical evidence; (e) abandon Cloud Vetting target. (a) chosen: it's the literal Splunkbase AI recommendation from the v1.0.4 failure report; it's the broadest match set so if ANY 9.x or 10.x version is on Cloud Classic's supported list this should match; the user explicitly picked it from a 3-option AskUserQuestion. (b) and (c) tested narrower variants of the same semver-range type — if SLIM rejects ranges by TYPE, all three fail identically and we learn nothing extra. (d) is exhausting one candidate at a time which is the path that led to 4 failed releases in 4 days. (e) defeats Phase 4. Two outcomes possible: (1) SLIM accepts the range → cumulative SLIM format history learns its FIRST accepted semver-range entry, all prior "ranges are type-rejected" docs need correction. (2) SLIM rejects the range with the "Expected String value" or "no supported version" error → confirms my earlier analytical conclusion was correct and we need to contact Splunk support directly for the actual supported-version list. Honesty rule applied: the v1.0.2-v1.0.4 docs (especially docs/SPLUNK_10_COMPATIBILITY.md Runtime Verification + this DECISION_LOG's prior rows) claimed semver ranges were type-rejected; that claim is now flagged as overconfident; whichever outcome v1.0.5 produces, the docs need updating. Low — change Enterprise value at next re-release; the only constraint is finding a value SLIM accepts. Three NEW maintenance lessons (in addition to those carried from earlier rows): (1) Empirical-evidence rule: when SLIM error wording matches across a "rejection class" we attribute to a specific cause (e.g., "type-rejected"), MEASURE the error wording precisely before concluding cause. The v1.0.4 → v1.0.5 transition shows that "no supported version" wording does NOT distinguish "version not on list" from "format not parsed as range" — the wording is the same. To disambiguate, try the SAME format with different versions and see if the version is echoed back literally or normalized. (2) Splunkbase AI explainer recommendations: the AI is consistent across runs (it suggested ">=9.0.0" or close variants in BOTH v1.0.3 and v1.0.4 failure explainers). Phase 1.7 was 6 months before our latest reads; SLIM may have changed in the interim. Treat AI recommendations as candidate hypotheses to TEST, not as already-rejected. (3) Cloud Classic supported-version list discovery: if v1.0.5's ">=9.0.0" ALSO fails, the next move is to file a Splunkbase publisher support ticket asking: "what Splunk Enterprise versions are currently on Splunk Cloud Classic's supported list for platformRequirements.splunk.Enterprise?" That's an explicit request to convert internal Splunk knowledge into a customer-visible answer.
2026-06-04 (row 1, morning) REVERSAL OF THE 2026-06-03 REVERSAL — declare platformRequirements.splunk.Enterprise = "9.4" (single string) in app.manifest for v1.0.4 (chosen over "10.0" which was the 2026-06-03 row's choice, and over the AI-recommended semver range which is empirically rejected). Why reversed: SLIM rejected "10.0" on Splunkbase upload 8800-43335 with Version requirement includes no supported version of Splunk Enterprise: 10.0 — the SAME error class as v1.0.1's "9.3" rejection (different version, same mechanism). The lesson: Splunk Cloud Classic's underlying Enterprise version list is INTERNAL to Splunk's managed infrastructure and does NOT match the on-prem GA timeline. The on-prem world has 10.0 GA; Splunk Cloud Classic is still on 9.x. The 2026-06-03 row's "forward-leaning" justification was based on a false assumption that "10.0 GA on-prem" implied "10.0 supported in Cloud Vetting". (a) "9.4" single string (chosen) — the literal "documented fallback" in the 2026-06-02 row + the AI explainer's upper-bound <10.0.0 clearly indicates Cloud Classic is 9.x; (b) "10.0" again — empirically rejected today, would be a 4th release attempt with the same failure; (c) AI's suggested ">=9.0.0,<10.0.0" semver range — Phase 1.7 already proved semver ranges are rejected (">=9.0,<10.0" failed identically with the same SLIM check); (d) try undocumented format "9.x" or "9" — high risk of a 5th release attempt with no empirical evidence either format works; (e) drop Cloud Vetting target and ship Enterprise on-prem only — defeats Phase 4 publishing milestone. (b), (c), (d), (e) all rejected: (b) is the empirically-failed value; (c) is the AI's recommendation but it's already rejected per our format history (Phase 1.7); (d) lacks empirical evidence; (e) defeats the public-release plan. (a) is the documented fallback AND the AI explainer corroborates "9.x is what Cloud Classic accepts" with its <10.0.0 upper bound, so going from "10.0" to "9.4" aligns with both lines of evidence. Trade-off explicitly accepted (carries over from 2026-06-03 row): 10.0-on-prem customers see a "compatibility warning" instead of a clean install path; they can override with one click in Splunkbase Cloud UI or install manually from GitHub releases. The 7-risk-area audit in docs/SPLUNK_10_COMPATIBILITY.md still applies to 10.0 customers — only the declared compatibility surface changed. Known residual gap (carries over): no runtime E2E on splunk/splunk:9.4.x container; 2026-07-18 quarterly audit per CLAUDE.md is the deadline. Low — change app.manifest.platformRequirements.splunk.Enterprise from "9.4" to a different single-version string SLIM accepts at the next forced re-release. Maintenance rules added (3 new lessons, in addition to those in 2026-06-03 row): (1) SLIM's supported-version list is INTERNAL to Splunk Cloud Classic, not visible from the AppInspect API spec or app.manifest schema docs. The only signal that a version has been added or removed is the hosted-AppInspect rejection at upload time. Both 9.3 (v1.0.1) and 10.0 (v1.0.3) became invalid AFTER the corresponding release was cut. (2) Do NOT follow Splunkbase AI-explainer recommendations verbatim when they conflict with our cumulative empirical format history. The AI today recommended ">=9.0.0,<10.0.0" semver range; that format is empirically rejected per Phase 1.7. The AI is directionally helpful (its <10.0.0 upper bound confirmed 9.x is Cloud's range) but its specific format suggestion ignored the SLIM String value constraint. Read docs/SPLUNK_10_COMPATIBILITY.md Runtime Verification section + cumulative SLIM format history table before accepting any new-format suggestion. (3) Cloud Classic version-list freshness gap: investigate Splunkbase publisher RSS/API for supported-version-list change notifications; add a quarterly check to CLAUDE.md "Splunk Version Pinning Audit" if such a feed exists. v1.1 backlog item.
2026-06-03 REVERSAL of 2026-06-02 row — declare platformRequirements.splunk.Enterprise = "10.0" (single string) in app.manifest for v1.0.3 (chosen over "9.4"). Why reversed: see 2026-06-02 row immediately below; SLIM 2.0 rejected the list form ["9.4", "10.0"] on hosted AppInspect run 8800-43334 with: Expected String value for manifest.platformRequirements.splunk.Enterprise, not ['9.4', '10.0']. The 2026-06-02 row's documented fallback path (single-string at next AppInspect re-run) is now the executed path. (a) "9.4" single string — broader near-term install base, matches Phase 1.7's working "9.3" pattern, was the literal "documented fallback" in the 2026-06-02 row; (b) "10.0" single string (chosen) — forward-leaning, aligns with Splunk's promotion cadence (10.0 GA mid-2025), reduces next-retirement risk; (c) try a comma-separated string "9.4,10.0" — undocumented format, high chance of SLIM rejection AND no recovery path documented; (d) try a semver-range string ">=9.4,<11.0" — Phase 1.7 already proved semver ranges are rejected (">=9.0,<10.0" failed identically). (c) and (d) are unproductive: SLIM's String value error message tells us nothing about whether the string itself can contain syntactic structure, and (d) is empirically already rejected. (a) is the literal documented fallback and is the safest choice for near-term Cloud Vetting because 9.x is the dominant install base today. (b) is forward-leaning: 10.0 is the latest, will fall off the supported list much later than 9.4 will (and the 2026-06-02 → 2026-06-03 cycle just demonstrated how disruptive a forced-retirement re-release is — declaring 9.4 forces another release in ~12-18 months when 9.4 retires; declaring 10.0 buys ~24-30 months). 9.4 customers can still install with the standard Splunkbase compatibility-override prompt, so the near-term install-base loss is soft, not hard. Trade-off explicitly accepted: 9.4 customers see a "compatibility warning" instead of a clean install path. Known residual risk (same as 2026-06-02): 10.0 declaration is code-audit-only, not runtime-verified against a real Splunk 10.0 container. v1.0.2's runtime verification gap carries over to v1.0.3 unchanged. The CHANGELOG + docs/SPLUNK_10_COMPATIBILITY.md Runtime Verification section both acknowledge this. Low — change app.manifest.platformRequirements.splunk.Enterprise from "10.0" to "9.4" at the next forced re-release; bump build/version; cut v1.0.4. The recovery is a 1-character change. Maintenance rules added: (1) SLIM 2.0's platformRequirements.splunk.Enterprise field accepts ONLY a single concrete-version string — list form ["A", "B"] and semver-range form ">=A,<B" are both rejected. The accepted format is "X.Y". Multi-version declaration requires a future Splunk-side SLIM schema change (no known roadmap as of 2026-06-03). (2) When AppInspect tells us "Expected String value, not [...]", the recovery is documented in the IMMEDIATELY-prior row's "Reversal cost" column — read that first, do not re-invent the recovery. (3) The 2026-07-18 quarterly Splunk Version Pinning Audit per CLAUDE.md MUST stand up a splunk/splunk:10.0.x container and run the E2E suite; if 10.0 surfaces a runtime regression, v1.0.4 falls back to "9.4" (which still leaves an unverified-runtime gap, but at the more conservative version).
2026-06-02 Declare platformRequirements.splunk.Enterprise = ["9.4", "10.0"] in app.manifest for v1.0.2 (chosen over: bump to 9.4 single-version with retest, bump to 9.4 single-version without retest, drop Cloud Vetting). AppInspect 4.2.1 SLIM validation for Splunkbase upload 8800-43285 rejected "9.3" with error "Version requirement includes no supported version of Splunk Enterprise: 9.3" — 9.3 is no longer on Splunk's supported list as of 2026-06. Cloud Vetting blocked until the manifest declares at least one currently-supported version. (a) Bump to single "9.4" AND stand up splunk/splunk:9.4.x container + run full E2E suite against it before release (safest; +1-2 days work). (b) Bump to single "9.4" and declare 9.4 compat based on API stability alone (no retest; risk that any 9.4 behavior delta hits us in prod). (c) Declare ["9.4", "10.0"] range with a 7-risk-area code audit (no runtime E2E; chosen). (d) Leave at "9.3" and accept that the app is on-prem only (no Splunkbase Cloud listing). The Cloud-listing goal is a Phase 4 release milestone; (d) defeats it. (b) is fast but ships untested compat; if a 9.4 customer reports a regression we own the incident. (a) is the most rigorous but adds 1-2 days to v1.0.2 for one minor-version bump that Splunk's API-stability commitment makes very unlikely to break. (c) is the chosen middle: declare BOTH 9.4 and 10.0 in one release so the next supported-version retirement doesn't force another patch release, AND do the 7-risk-area audit from CLAUDE.md "Splunk Version Pinning Audit" so the declaration has provenance. The audit's findings are recorded in docs/SPLUNK_10_COMPATIBILITY.md (new file). Key audit results: 4 of 7 risk areas are N/A (BaseRestHandler not used, dispatch_session not used, MAX_MULTIVAL_COUNT not relied on, panel-depends quirk not depended on); 3 are present but handled correctly today (mvc.Components has explicit dual-instance reads, INDEXED_EXTRACTIONS+KV_MODE matches documented Splunk pairing, simpleRequest 404 wrapped in try/except per feedback_splunk_simplerequest_404.md). Python syntax sweep confirms 3.7-compat (no match/case/walrus/PEP-604) so 10.0's Python 3.9 baseline is safe. Known residual risk: no runtime E2E on 9.4 or 10.0 — declaration is code-audit-only. v1.0.2 release notes + CHANGELOG must acknowledge this; v1.1 backlog item is to stand up parallel containers for runtime confirmation. Low — revert app.manifest platformRequirements.splunk.Enterprise to the single supported version SLIM accepts at next AppInspect re-run, bump build/version, cut v1.0.3. If runtime testing on 9.4 or 10.0 surfaces a bug, the partial revert is to remove only the failing version from the list (e.g. ["9.4"] if 10.0 is the problem). Maintenance rule: the next quarterly Splunk Version Pinning Audit (scheduled 2026-07-18 per CLAUDE.md) MUST stand up splunk/splunk:9.4.x and splunk/splunk:10.0.x parallel containers and run the integration + E2E suites against both; any failure logs a row in docs/SPLUNK_10_COMPATIBILITY.md under a new "Runtime verification" section. If 10.0 hits a runtime bug, the recovery is to revert the manifest to ["9.4"] only and document the gap. Until then, customer-facing docs (CHANGELOG, INSTALLATION) state that 9.4 / 10.0 support is "declared via API-stability + code audit, full E2E deferred to v1.1".
2026-06-02 Coherent project hook system at scripts/hooks/ (tracked) + tracked .claude/settings.example.json template + scripts/install-hooks.sh bootstrap. Adds 4 new project hooks (preflight-tag-guard.js, validate-runner.js, urlargs-sync.js, lib/code-quality-checks.js) alongside the pre-existing block-synthetic-fixtures.js, plus 3 new global hooks (force-push-guard.js, banned-phrase-trigger.js + companion, additional-thoughts-trigger.sh + companion) under ~/.claude/hooks/. All 6 project-side artifacts + the §3.5 preflight script (scripts/preflight-tag.sh) covered by a bash unit-test framework at tests/hooks/ (105 assertions across 9 suites, runnable via make hook-tests). (a) Leave hook scripts under .claude/hooks/ (per-developer, gitignored) like post-edit-check.js + stop-check.js; (b) Ship a one-line install script that drops absolute paths into .claude/settings.json (chosen); (c) Investigate ${workspaceFolder} template expansion in Claude Code hook commands; (d) Wire hooks via the project's scripts/pre-commit script only (no Claude Code integration). (a) was the path for post-edit-check.js/stop-check.js and proved that per-developer hook scripts grow stale or diverge — the regex set drifted between the two hooks and nobody noticed until the 2026-06-01 consolidation. Shared logic MUST live in a tracked location. (c) is the cleanest fix but Claude Code does NOT expand ${workspaceFolder} in hook commands today (verified empirically). Until that lands, hardcoded paths are unavoidable. (d) loses the in-IDE feedback loop — Claude Code hooks fire during the workflow that introduces the problem; pre-commit hooks fire ~30 min later when the developer remembers to commit. (b) is the pragmatic middle ground: hook scripts tracked under scripts/hooks/ (so collaborators get them automatically), settings template tracked under .claude/settings.example.json (so collaborators see the recommended wiring), scripts/install-hooks.sh rewrites the hardcoded c:/Users/PC/wl_manager prefix to the actual checkout (so the template is one-command useful). The .gitignore exempt rule (!.claude/settings.example.json) is the load-bearing piece — without it the template can't be tracked at all. Per-developer customizations stay in .claude/settings.json (still gitignored). Low to retire individual hooks (delete the entry from .claude/settings.json); the hook scripts themselves are passive — removing them from settings.json takes them out of the firing chain. The block-synthetic-fixtures.js hook in particular has the 2026-04-23 incident behind it and should never be retired without a replacement. Maintenance rules: (1) any NEW hook lives in scripts/hooks/ (tracked) with a matching tests/hooks/test_<name>.sh suite — never in .claude/hooks/ directly, except for personal-preference hooks like post-edit-check.js whose threshold logic is per-developer; (2) when ${workspaceFolder} expansion ships in Claude Code, simplify .claude/settings.example.json to use it and retire the path-rewrite in install-hooks.sh; (3) hook log files (~/.claude/<name>.log) grow unbounded today — log rotation is a known gap, deferred until any file crosses ~50MB. Documented here so future-me does not re-discover it; (4) the force-push-guard.js regex deliberately only inspects the FIRST shell sub-command to avoid false-positives on git commit -m "...--force..." and python -c "...git push --force..." patterns — extending to chained ls && git push --force requires a real shell parser, not a regex tweak. Trade-off documented in tests/hooks/test_global_force_push_guard.sh (4 dedicated regression cases).
2026-05-11 E2E tests CI-gated via TWO workflows. e2e-smoke.yml runs ~3 non-destructive E2E flow tests on every PR (~5 min). e2e-full.yml runs the entire 20-test E2E suite (destructive + visual regression + concurrency + stress) on a nightly UTC schedule and via manual workflow_dispatch. Smoke selection: test_trash_traversal.cjs + test_rate_limit_burst.cjs + test_control_panel_long_content.cjs — all UI-driven, no destructive helpers, exercise the REST → frontend stack end-to-end. The destructive WL_TEST_HARNESS=1 env var is set only inline per nightly step, never globally. (a) Full CI on every PR (run all 22 on each PR); (b) tag-gated (PR label e2e to run any E2E); (c) manual workflow_dispatch only with no nightly; (d) status quo (E2E tests never run in CI, only ad-hoc on developer machines). (a) is rejected because 20 tests × ~30-60s + Playwright + container setup = 30+ min added to every PR, multiplied across every contributor push. Rate-limit collisions in the integration suite (a documented Ring 3 finding) would be worse for E2E since the destructive E2E tests deplete shared counters faster than the integration suite. (b) shifts the gating decision to contributors who may forget the label — over time the signal degrades to "nobody runs E2E in CI". (c) is what (d) effectively becomes: opt-in tools get used heavily for a month then forgotten. (d) is the prior state — 20 tests with real bug-finding signal lived entirely outside CI, so PR-introduced regressions had to wait for a developer's manual run to surface. The chosen two-workflow shape preserves PR feedback latency (smoke <5 min) while ensuring the full suite runs at least daily; failures in the nightly run upload artifacts and surface in the GitHub Actions summary the next morning. Mirrors the standard test-pyramid principle: heavier tests run less frequently but never skip. Low — to revert to "full E2E on every PR": change schedule and workflow_dispatch to pull_request in e2e-full.yml and delete e2e-smoke.yml. To revert to status quo: delete both workflow files. The two-workflow shape is intentional separation; merging them would re-introduce the latency cost (a) was rejected for. Maintenance rule (adding a new E2E test): by default, NEW tests go to the nightly full workflow. Only promote to smoke after the test has been stable in nightly for ≥2 weeks AND a smoke slot opens up (we cap smoke at ~5 tests to keep PR latency <5 min). If you promote a test to smoke, document the choice in the workflow file's comments and remove an existing one if the cap is hit.

When a past decision is reversed: add a NEW row with the new choice and a "Why reversed: see row" note. Do NOT delete or edit the old row — it is evidence for the next person who proposes the same losing option.


Followups & acknowledgements

Out-of-band closures and acknowledgements that reference decisions in the main table but aren't themselves new tech choices. Format: | Date | Reference | Body |. Use this for "the action implied by decision N has been taken" entries — e.g., credential storage acks, account creations, recovery-code splits.

Date Reference Body
2026-05-16 D15 followup (Phase 1.1.1 closure) Splunk Developer account credentials stored in maintainer's personal password manager. TOTP 2FA enabled on the account; recovery codes stored separately from the password. Linked Gmail (communicate.oleh@gmail.com) has its own 2FA + recovery. Storage method intentionally not documented further in this public log.
2026-05-17 D18 followup (Phase 1.4 closure) Splunk Developer credentials registered as GitHub Actions secrets on RelativisticJet/wl_manager: SPLUNK_DEV_USERNAME (registered 2026-05-17T17:42:17Z) and SPLUNK_DEV_PASSWORD (registered 2026-05-17T17:42:48Z). Verified via gh secret list --repo RelativisticJet/wl_manager immediately after registration. Phase 1.4 ⚠️ PARTIAL → ✅ flipped at the same time. End-to-end auth validation deferred to Phase 1.5's first workflow run (splunk/appinspect-api-action does the splunk.com JWT exchange internally; a wrong secret surfaces as a clean 401 there). Secret values are NOT in the repo; only the registration acknowledgement.