Fix :flag async adapters (route through ClassifyJob) + out-of-the-box Active Storage attachment filtering - #3

Merged
rameerez merged 1 commit into
mainfrom
fix/flag-async-adapters-via-classify-job
Jul 10, 2026
Merged

Fix :flag async adapters (route through ClassifyJob) + out-of-the-box Active Storage attachment filtering#3
rameerez merged 1 commit into
mainfrom
fix/flag-async-adapters-via-classify-job

Conversation

@rameerez

Copy link
Copy Markdown
Owner

Found while wiring CarHey's avatar AI classifier — the first real-world async image adapter against the released 1.0.0.beta1.

1. Async adapters never actually ran in Moderate::ClassifyJob

beta1's :flag after_commit called Moderate.classifyinline for every adapter, including ones declaring synchronous? == false. Consequences:

  • a remote moderation API's network call ran inside the request that saved the content (1–5s stall per save, exactly what the docs say must never happen);
  • Moderate::ClassifyJob — whose header, and filters/base.rb's comments, promise this routing — was dead code nothing enqueued.

Fix: the concern probes the new public Configuration#adapter_async? (identical predicate to the :block validator's: async only when the adapter explicitly answers synchronous? == false) and enqueues ClassifyJob instead of classifying inline. The job re-reads the persisted value and files the Flag through the same Flag.flag! builder. Hosts that worked around the gap (enqueue-it-yourself + short-circuit moderation_field_changed_for_commit?) can delete both workarounds — CarHey will, on the next release bump.

2. moderates <attachment> works out of the box now

Previously every host had to hand-roll the three-method seam for a has_one_attached field (AR dirty tracking can't see attachment writes, and Active Storage clears attachment_changes before after_commit). Now the concern:

  • snapshots filtered attachment writes in a before_save (one-shot, consumed at commit, cleared through the existing moderation_field_committed ensure-hook);
  • treats an unattached ActiveStorage::Attached proxy as blank in both the concern and ClassifyJob (covers the purge-between-enqueue-and-run race).

The overridable seam remains for richer cases. The dummy Comment now carries both shapes: :image (custom seam, unchanged) and a new :photo (native, zero overrides).

Tests

205 runs, 0 failures. Existing image tests updated to the enqueue+perform shape; new coverage: async-never-inline, native attachment tracking + re-save-no-spam, vanished-attachment no-op, adapter_async?. Rubocop: no new offenses beyond the repo's pre-existing Metrics baseline.

Release note

This is a behavior change for any beta1 host silently relying on inline classification of an async adapter (there shouldn't be any — that path blocked requests). Suggest shipping as 1.0.0.beta2; CHANGELOG entries added under the unreleased 1.0.0 section.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UMa8bEvYfi9Dr8RYhhfKze

…nt support
Two host-facing fixes found while wiring CarHey's avatar AI classifier
(the first real-world async image adapter):
1. ASYNC ADAPTERS NEVER RAN IN THE JOB. The :flag after_commit called
Moderate.classify INLINE for every adapter - including ones declaring
synchronous? == false - so a remote moderation API's network call ran
inside the request that saved the content, and Moderate::ClassifyJob
(whose own docs, and filters/base.rb's, promised the routing) was dead
code nothing enqueued. The concern now probes the new
Configuration#adapter_async? (same predicate the :block validator uses)
and enqueues ClassifyJob instead; the job re-reads the persisted value
and files the Flag itself. Hosts that worked around this by enqueuing
the job themselves + short-circuiting
moderation_field_changed_for_commit? can delete both workarounds.
2. ACTIVE STORAGE ATTACHMENTS NOW WORK OUT OF THE BOX. `moderates :avatar,
with: :img, mode: :flag` on a has_one_attached model previously
required every host to hand-roll the 3-method seam (AR dirty tracking
can't see attachment writes, and Active Storage clears
attachment_changes before after_commit). The concern now snapshots
filtered attachment writes in a before_save, consumes the one-shot
snapshot at commit (cleared via moderation_field_committed, which the
ensure already guaranteed), and both the concern and ClassifyJob treat
an unattached ActiveStorage::Attached proxy as blank - covering the
purge-between-enqueue-and-run race. The overridable seam stays for
richer cases; the dummy Comment now carries BOTH shapes (:image =
custom seam, :photo = native).
Tests: existing image tests updated to the enqueue+perform shape; new
coverage for never-inline, native tracking + re-save no-spam, vanished-
attachment no-op, and adapter_async?. 205 runs green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UMa8bEvYfi9Dr8RYhhfKze
@rameerez
rameerez merged commit 2c348b8 into mainJul 10, 2026
14 of 15 checks passed
@rameerez
rameerez deleted the fix/flag-async-adapters-via-classify-job branch July 10, 2026 18:41
rameerez added a commit that referenced this pull request Jul 10, 2026
CHANGELOG: fold the since-beta1 deltas (#3 async-adapter routing fix + Active
Storage attachment filtering, #4 Flag close sugar) into a proper
[1.0.0.beta2] section; bump Moderate::VERSION.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UMa8bEvYfi9Dr8RYhhfKze
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@rameerez
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Fix :flag async adapters (route through ClassifyJob) + out-of-the-box Active Storage attachment filtering - #3

Merged
rameerez merged 1 commit into
mainfrom
fix/flag-async-adapters-via-classify-job
Jul 10, 2026
Merged

Fix :flag async adapters (route through ClassifyJob) + out-of-the-box Active Storage attachment filtering#3
rameerez merged 1 commit into
mainfrom
fix/flag-async-adapters-via-classify-job

Conversation

@rameerez

Copy link
Copy Markdown
Owner

Found while wiring CarHey's avatar AI classifier — the first real-world async image adapter against the released 1.0.0.beta1.

1. Async adapters never actually ran in Moderate::ClassifyJob

beta1's :flag after_commit called Moderate.classifyinline for every adapter, including ones declaring synchronous? == false. Consequences:

  • a remote moderation API's network call ran inside the request that saved the content (1–5s stall per save, exactly what the docs say must never happen);
  • Moderate::ClassifyJob — whose header, and filters/base.rb's comments, promise this routing — was dead code nothing enqueued.

Fix: the concern probes the new public Configuration#adapter_async? (identical predicate to the :block validator's: async only when the adapter explicitly answers synchronous? == false) and enqueues ClassifyJob instead of classifying inline. The job re-reads the persisted value and files the Flag through the same Flag.flag! builder. Hosts that worked around the gap (enqueue-it-yourself + short-circuit moderation_field_changed_for_commit?) can delete both workarounds — CarHey will, on the next release bump.

2. moderates <attachment> works out of the box now

Previously every host had to hand-roll the three-method seam for a has_one_attached field (AR dirty tracking can't see attachment writes, and Active Storage clears attachment_changes before after_commit). Now the concern:

  • snapshots filtered attachment writes in a before_save (one-shot, consumed at commit, cleared through the existing moderation_field_committed ensure-hook);
  • treats an unattached ActiveStorage::Attached proxy as blank in both the concern and ClassifyJob (covers the purge-between-enqueue-and-run race).

The overridable seam remains for richer cases. The dummy Comment now carries both shapes: :image (custom seam, unchanged) and a new :photo (native, zero overrides).

Tests

205 runs, 0 failures. Existing image tests updated to the enqueue+perform shape; new coverage: async-never-inline, native attachment tracking + re-save-no-spam, vanished-attachment no-op, adapter_async?. Rubocop: no new offenses beyond the repo's pre-existing Metrics baseline.

Release note

This is a behavior change for any beta1 host silently relying on inline classification of an async adapter (there shouldn't be any — that path blocked requests). Suggest shipping as 1.0.0.beta2; CHANGELOG entries added under the unreleased 1.0.0 section.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UMa8bEvYfi9Dr8RYhhfKze

…nt support
Two host-facing fixes found while wiring CarHey's avatar AI classifier
(the first real-world async image adapter):
1. ASYNC ADAPTERS NEVER RAN IN THE JOB. The :flag after_commit called
Moderate.classify INLINE for every adapter - including ones declaring
synchronous? == false - so a remote moderation API's network call ran
inside the request that saved the content, and Moderate::ClassifyJob
(whose own docs, and filters/base.rb's, promised the routing) was dead
code nothing enqueued. The concern now probes the new
Configuration#adapter_async? (same predicate the :block validator uses)
and enqueues ClassifyJob instead; the job re-reads the persisted value
and files the Flag itself. Hosts that worked around this by enqueuing
the job themselves + short-circuiting
moderation_field_changed_for_commit? can delete both workarounds.
2. ACTIVE STORAGE ATTACHMENTS NOW WORK OUT OF THE BOX. `moderates :avatar,
with: :img, mode: :flag` on a has_one_attached model previously
required every host to hand-roll the 3-method seam (AR dirty tracking
can't see attachment writes, and Active Storage clears
attachment_changes before after_commit). The concern now snapshots
filtered attachment writes in a before_save, consumes the one-shot
snapshot at commit (cleared via moderation_field_committed, which the
ensure already guaranteed), and both the concern and ClassifyJob treat
an unattached ActiveStorage::Attached proxy as blank - covering the
purge-between-enqueue-and-run race. The overridable seam stays for
richer cases; the dummy Comment now carries BOTH shapes (:image =
custom seam, :photo = native).
Tests: existing image tests updated to the enqueue+perform shape; new
coverage for never-inline, native tracking + re-save no-spam, vanished-
attachment no-op, and adapter_async?. 205 runs green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UMa8bEvYfi9Dr8RYhhfKze
@rameerez
rameerez merged commit 2c348b8 into mainJul 10, 2026
14 of 15 checks passed
@rameerez
rameerez deleted the fix/flag-async-adapters-via-classify-job branch July 10, 2026 18:41
rameerez added a commit that referenced this pull request Jul 10, 2026
CHANGELOG: fold the since-beta1 deltas (#3 async-adapter routing fix + Active
Storage attachment filtering, #4 Flag close sugar) into a proper
[1.0.0.beta2] section; bump Moderate::VERSION.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UMa8bEvYfi9Dr8RYhhfKze
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@rameerez
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix :flag async adapters (route through ClassifyJob) + out-of-the-box Active Storage attachment filtering - #3

Merged
rameerez merged 1 commit into
mainfrom
fix/flag-async-adapters-via-classify-job
Jul 10, 2026
Merged

Fix :flag async adapters (route through ClassifyJob) + out-of-the-box Active Storage attachment filtering#3
rameerez merged 1 commit into
mainfrom
fix/flag-async-adapters-via-classify-job

Conversation

@rameerez

Copy link
Copy Markdown
Owner

Found while wiring CarHey's avatar AI classifier — the first real-world async image adapter against the released 1.0.0.beta1.

1. Async adapters never actually ran in Moderate::ClassifyJob

beta1's :flag after_commit called Moderate.classifyinline for every adapter, including ones declaring synchronous? == false. Consequences:

  • a remote moderation API's network call ran inside the request that saved the content (1–5s stall per save, exactly what the docs say must never happen);
  • Moderate::ClassifyJob — whose header, and filters/base.rb's comments, promise this routing — was dead code nothing enqueued.

Fix: the concern probes the new public Configuration#adapter_async? (identical predicate to the :block validator's: async only when the adapter explicitly answers synchronous? == false) and enqueues ClassifyJob instead of classifying inline. The job re-reads the persisted value and files the Flag through the same Flag.flag! builder. Hosts that worked around the gap (enqueue-it-yourself + short-circuit moderation_field_changed_for_commit?) can delete both workarounds — CarHey will, on the next release bump.

2. moderates <attachment> works out of the box now

Previously every host had to hand-roll the three-method seam for a has_one_attached field (AR dirty tracking can't see attachment writes, and Active Storage clears attachment_changes before after_commit). Now the concern:

  • snapshots filtered attachment writes in a before_save (one-shot, consumed at commit, cleared through the existing moderation_field_committed ensure-hook);
  • treats an unattached ActiveStorage::Attached proxy as blank in both the concern and ClassifyJob (covers the purge-between-enqueue-and-run race).

The overridable seam remains for richer cases. The dummy Comment now carries both shapes: :image (custom seam, unchanged) and a new :photo (native, zero overrides).

Tests

205 runs, 0 failures. Existing image tests updated to the enqueue+perform shape; new coverage: async-never-inline, native attachment tracking + re-save-no-spam, vanished-attachment no-op, adapter_async?. Rubocop: no new offenses beyond the repo's pre-existing Metrics baseline.

Release note

This is a behavior change for any beta1 host silently relying on inline classification of an async adapter (there shouldn't be any — that path blocked requests). Suggest shipping as 1.0.0.beta2; CHANGELOG entries added under the unreleased 1.0.0 section.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UMa8bEvYfi9Dr8RYhhfKze

…nt support
Two host-facing fixes found while wiring CarHey's avatar AI classifier
(the first real-world async image adapter):
1. ASYNC ADAPTERS NEVER RAN IN THE JOB. The :flag after_commit called
Moderate.classify INLINE for every adapter - including ones declaring
synchronous? == false - so a remote moderation API's network call ran
inside the request that saved the content, and Moderate::ClassifyJob
(whose own docs, and filters/base.rb's, promised the routing) was dead
code nothing enqueued. The concern now probes the new
Configuration#adapter_async? (same predicate the :block validator uses)
and enqueues ClassifyJob instead; the job re-reads the persisted value
and files the Flag itself. Hosts that worked around this by enqueuing
the job themselves + short-circuiting
moderation_field_changed_for_commit? can delete both workarounds.
2. ACTIVE STORAGE ATTACHMENTS NOW WORK OUT OF THE BOX. `moderates :avatar,
with: :img, mode: :flag` on a has_one_attached model previously
required every host to hand-roll the 3-method seam (AR dirty tracking
can't see attachment writes, and Active Storage clears
attachment_changes before after_commit). The concern now snapshots
filtered attachment writes in a before_save, consumes the one-shot
snapshot at commit (cleared via moderation_field_committed, which the
ensure already guaranteed), and both the concern and ClassifyJob treat
an unattached ActiveStorage::Attached proxy as blank - covering the
purge-between-enqueue-and-run race. The overridable seam stays for
richer cases; the dummy Comment now carries BOTH shapes (:image =
custom seam, :photo = native).
Tests: existing image tests updated to the enqueue+perform shape; new
coverage for never-inline, native tracking + re-save no-spam, vanished-
attachment no-op, and adapter_async?. 205 runs green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UMa8bEvYfi9Dr8RYhhfKze
@rameerez
rameerez merged commit 2c348b8 into mainJul 10, 2026
14 of 15 checks passed
@rameerez
rameerez deleted the fix/flag-async-adapters-via-classify-job branch July 10, 2026 18:41
rameerez added a commit that referenced this pull request Jul 10, 2026
CHANGELOG: fold the since-beta1 deltas (#3 async-adapter routing fix + Active
Storage attachment filtering, #4 Flag close sugar) into a proper
[1.0.0.beta2] section; bump Moderate::VERSION.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UMa8bEvYfi9Dr8RYhhfKze
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@rameerez
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix :flag async adapters (route through ClassifyJob) + out-of-the-box Active Storage attachment filtering - #3

Merged
rameerez merged 1 commit into
mainfrom
fix/flag-async-adapters-via-classify-job
Jul 10, 2026
Merged

Fix :flag async adapters (route through ClassifyJob) + out-of-the-box Active Storage attachment filtering#3
rameerez merged 1 commit into
mainfrom
fix/flag-async-adapters-via-classify-job

Conversation

@rameerez

Copy link
Copy Markdown
Owner

Found while wiring CarHey's avatar AI classifier — the first real-world async image adapter against the released 1.0.0.beta1.

1. Async adapters never actually ran in Moderate::ClassifyJob

beta1's :flag after_commit called Moderate.classifyinline for every adapter, including ones declaring synchronous? == false. Consequences:

  • a remote moderation API's network call ran inside the request that saved the content (1–5s stall per save, exactly what the docs say must never happen);
  • Moderate::ClassifyJob — whose header, and filters/base.rb's comments, promise this routing — was dead code nothing enqueued.

Fix: the concern probes the new public Configuration#adapter_async? (identical predicate to the :block validator's: async only when the adapter explicitly answers synchronous? == false) and enqueues ClassifyJob instead of classifying inline. The job re-reads the persisted value and files the Flag through the same Flag.flag! builder. Hosts that worked around the gap (enqueue-it-yourself + short-circuit moderation_field_changed_for_commit?) can delete both workarounds — CarHey will, on the next release bump.

2. moderates <attachment> works out of the box now

Previously every host had to hand-roll the three-method seam for a has_one_attached field (AR dirty tracking can't see attachment writes, and Active Storage clears attachment_changes before after_commit). Now the concern:

  • snapshots filtered attachment writes in a before_save (one-shot, consumed at commit, cleared through the existing moderation_field_committed ensure-hook);
  • treats an unattached ActiveStorage::Attached proxy as blank in both the concern and ClassifyJob (covers the purge-between-enqueue-and-run race).

The overridable seam remains for richer cases. The dummy Comment now carries both shapes: :image (custom seam, unchanged) and a new :photo (native, zero overrides).

Tests

205 runs, 0 failures. Existing image tests updated to the enqueue+perform shape; new coverage: async-never-inline, native attachment tracking + re-save-no-spam, vanished-attachment no-op, adapter_async?. Rubocop: no new offenses beyond the repo's pre-existing Metrics baseline.

Release note

This is a behavior change for any beta1 host silently relying on inline classification of an async adapter (there shouldn't be any — that path blocked requests). Suggest shipping as 1.0.0.beta2; CHANGELOG entries added under the unreleased 1.0.0 section.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UMa8bEvYfi9Dr8RYhhfKze

…nt support
Two host-facing fixes found while wiring CarHey's avatar AI classifier
(the first real-world async image adapter):
1. ASYNC ADAPTERS NEVER RAN IN THE JOB. The :flag after_commit called
Moderate.classify INLINE for every adapter - including ones declaring
synchronous? == false - so a remote moderation API's network call ran
inside the request that saved the content, and Moderate::ClassifyJob
(whose own docs, and filters/base.rb's, promised the routing) was dead
code nothing enqueued. The concern now probes the new
Configuration#adapter_async? (same predicate the :block validator uses)
and enqueues ClassifyJob instead; the job re-reads the persisted value
and files the Flag itself. Hosts that worked around this by enqueuing
the job themselves + short-circuiting
moderation_field_changed_for_commit? can delete both workarounds.
2. ACTIVE STORAGE ATTACHMENTS NOW WORK OUT OF THE BOX. `moderates :avatar,
with: :img, mode: :flag` on a has_one_attached model previously
required every host to hand-roll the 3-method seam (AR dirty tracking
can't see attachment writes, and Active Storage clears
attachment_changes before after_commit). The concern now snapshots
filtered attachment writes in a before_save, consumes the one-shot
snapshot at commit (cleared via moderation_field_committed, which the
ensure already guaranteed), and both the concern and ClassifyJob treat
an unattached ActiveStorage::Attached proxy as blank - covering the
purge-between-enqueue-and-run race. The overridable seam stays for
richer cases; the dummy Comment now carries BOTH shapes (:image =
custom seam, :photo = native).
Tests: existing image tests updated to the enqueue+perform shape; new
coverage for never-inline, native tracking + re-save no-spam, vanished-
attachment no-op, and adapter_async?. 205 runs green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UMa8bEvYfi9Dr8RYhhfKze
@rameerez
rameerez merged commit 2c348b8 into mainJul 10, 2026
14 of 15 checks passed
@rameerez
rameerez deleted the fix/flag-async-adapters-via-classify-job branch July 10, 2026 18:41
rameerez added a commit that referenced this pull request Jul 10, 2026
CHANGELOG: fold the since-beta1 deltas (#3 async-adapter routing fix + Active
Storage attachment filtering, #4 Flag close sugar) into a proper
[1.0.0.beta2] section; bump Moderate::VERSION.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UMa8bEvYfi9Dr8RYhhfKze
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@rameerez
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Fix :flag async adapters (route through ClassifyJob) + out-of-the-box Active Storage attachment filtering - #3

Merged
rameerez merged 1 commit into
mainfrom
fix/flag-async-adapters-via-classify-job
Jul 10, 2026
Merged

Fix :flag async adapters (route through ClassifyJob) + out-of-the-box Active Storage attachment filtering#3
rameerez merged 1 commit into
mainfrom
fix/flag-async-adapters-via-classify-job

Conversation

@rameerez

Copy link
Copy Markdown
Owner

Found while wiring CarHey's avatar AI classifier — the first real-world async image adapter against the released 1.0.0.beta1.

1. Async adapters never actually ran in Moderate::ClassifyJob

beta1's :flag after_commit called Moderate.classifyinline for every adapter, including ones declaring synchronous? == false. Consequences:

  • a remote moderation API's network call ran inside the request that saved the content (1–5s stall per save, exactly what the docs say must never happen);
  • Moderate::ClassifyJob — whose header, and filters/base.rb's comments, promise this routing — was dead code nothing enqueued.

Fix: the concern probes the new public Configuration#adapter_async? (identical predicate to the :block validator's: async only when the adapter explicitly answers synchronous? == false) and enqueues ClassifyJob instead of classifying inline. The job re-reads the persisted value and files the Flag through the same Flag.flag! builder. Hosts that worked around the gap (enqueue-it-yourself + short-circuit moderation_field_changed_for_commit?) can delete both workarounds — CarHey will, on the next release bump.

2. moderates <attachment> works out of the box now

Previously every host had to hand-roll the three-method seam for a has_one_attached field (AR dirty tracking can't see attachment writes, and Active Storage clears attachment_changes before after_commit). Now the concern:

  • snapshots filtered attachment writes in a before_save (one-shot, consumed at commit, cleared through the existing moderation_field_committed ensure-hook);
  • treats an unattached ActiveStorage::Attached proxy as blank in both the concern and ClassifyJob (covers the purge-between-enqueue-and-run race).

The overridable seam remains for richer cases. The dummy Comment now carries both shapes: :image (custom seam, unchanged) and a new :photo (native, zero overrides).

Tests

205 runs, 0 failures. Existing image tests updated to the enqueue+perform shape; new coverage: async-never-inline, native attachment tracking + re-save-no-spam, vanished-attachment no-op, adapter_async?. Rubocop: no new offenses beyond the repo's pre-existing Metrics baseline.

Release note

This is a behavior change for any beta1 host silently relying on inline classification of an async adapter (there shouldn't be any — that path blocked requests). Suggest shipping as 1.0.0.beta2; CHANGELOG entries added under the unreleased 1.0.0 section.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UMa8bEvYfi9Dr8RYhhfKze

…nt support
Two host-facing fixes found while wiring CarHey's avatar AI classifier
(the first real-world async image adapter):
1. ASYNC ADAPTERS NEVER RAN IN THE JOB. The :flag after_commit called
Moderate.classify INLINE for every adapter - including ones declaring
synchronous? == false - so a remote moderation API's network call ran
inside the request that saved the content, and Moderate::ClassifyJob
(whose own docs, and filters/base.rb's, promised the routing) was dead
code nothing enqueued. The concern now probes the new
Configuration#adapter_async? (same predicate the :block validator uses)
and enqueues ClassifyJob instead; the job re-reads the persisted value
and files the Flag itself. Hosts that worked around this by enqueuing
the job themselves + short-circuiting
moderation_field_changed_for_commit? can delete both workarounds.
2. ACTIVE STORAGE ATTACHMENTS NOW WORK OUT OF THE BOX. `moderates :avatar,
with: :img, mode: :flag` on a has_one_attached model previously
required every host to hand-roll the 3-method seam (AR dirty tracking
can't see attachment writes, and Active Storage clears
attachment_changes before after_commit). The concern now snapshots
filtered attachment writes in a before_save, consumes the one-shot
snapshot at commit (cleared via moderation_field_committed, which the
ensure already guaranteed), and both the concern and ClassifyJob treat
an unattached ActiveStorage::Attached proxy as blank - covering the
purge-between-enqueue-and-run race. The overridable seam stays for
richer cases; the dummy Comment now carries BOTH shapes (:image =
custom seam, :photo = native).
Tests: existing image tests updated to the enqueue+perform shape; new
coverage for never-inline, native tracking + re-save no-spam, vanished-
attachment no-op, and adapter_async?. 205 runs green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UMa8bEvYfi9Dr8RYhhfKze
@rameerez
rameerez merged commit 2c348b8 into mainJul 10, 2026
14 of 15 checks passed
@rameerez
rameerez deleted the fix/flag-async-adapters-via-classify-job branch July 10, 2026 18:41
rameerez added a commit that referenced this pull request Jul 10, 2026
CHANGELOG: fold the since-beta1 deltas (#3 async-adapter routing fix + Active
Storage attachment filtering, #4 Flag close sugar) into a proper
[1.0.0.beta2] section; bump Moderate::VERSION.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UMa8bEvYfi9Dr8RYhhfKze
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@rameerez
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix :flag async adapters (route through ClassifyJob) + out-of-the-box Active Storage attachment filtering - #3

Merged
rameerez merged 1 commit into
mainfrom
fix/flag-async-adapters-via-classify-job
Jul 10, 2026
Merged

Fix :flag async adapters (route through ClassifyJob) + out-of-the-box Active Storage attachment filtering#3
rameerez merged 1 commit into
mainfrom
fix/flag-async-adapters-via-classify-job

Conversation

@rameerez

Copy link
Copy Markdown
Owner

Found while wiring CarHey's avatar AI classifier — the first real-world async image adapter against the released 1.0.0.beta1.

1. Async adapters never actually ran in Moderate::ClassifyJob

beta1's :flag after_commit called Moderate.classifyinline for every adapter, including ones declaring synchronous? == false. Consequences:

  • a remote moderation API's network call ran inside the request that saved the content (1–5s stall per save, exactly what the docs say must never happen);
  • Moderate::ClassifyJob — whose header, and filters/base.rb's comments, promise this routing — was dead code nothing enqueued.

Fix: the concern probes the new public Configuration#adapter_async? (identical predicate to the :block validator's: async only when the adapter explicitly answers synchronous? == false) and enqueues ClassifyJob instead of classifying inline. The job re-reads the persisted value and files the Flag through the same Flag.flag! builder. Hosts that worked around the gap (enqueue-it-yourself + short-circuit moderation_field_changed_for_commit?) can delete both workarounds — CarHey will, on the next release bump.

2. moderates <attachment> works out of the box now

Previously every host had to hand-roll the three-method seam for a has_one_attached field (AR dirty tracking can't see attachment writes, and Active Storage clears attachment_changes before after_commit). Now the concern:

  • snapshots filtered attachment writes in a before_save (one-shot, consumed at commit, cleared through the existing moderation_field_committed ensure-hook);
  • treats an unattached ActiveStorage::Attached proxy as blank in both the concern and ClassifyJob (covers the purge-between-enqueue-and-run race).

The overridable seam remains for richer cases. The dummy Comment now carries both shapes: :image (custom seam, unchanged) and a new :photo (native, zero overrides).

Tests

205 runs, 0 failures. Existing image tests updated to the enqueue+perform shape; new coverage: async-never-inline, native attachment tracking + re-save-no-spam, vanished-attachment no-op, adapter_async?. Rubocop: no new offenses beyond the repo's pre-existing Metrics baseline.

Release note

This is a behavior change for any beta1 host silently relying on inline classification of an async adapter (there shouldn't be any — that path blocked requests). Suggest shipping as 1.0.0.beta2; CHANGELOG entries added under the unreleased 1.0.0 section.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UMa8bEvYfi9Dr8RYhhfKze

…nt support
Two host-facing fixes found while wiring CarHey's avatar AI classifier
(the first real-world async image adapter):
1. ASYNC ADAPTERS NEVER RAN IN THE JOB. The :flag after_commit called
Moderate.classify INLINE for every adapter - including ones declaring
synchronous? == false - so a remote moderation API's network call ran
inside the request that saved the content, and Moderate::ClassifyJob
(whose own docs, and filters/base.rb's, promised the routing) was dead
code nothing enqueued. The concern now probes the new
Configuration#adapter_async? (same predicate the :block validator uses)
and enqueues ClassifyJob instead; the job re-reads the persisted value
and files the Flag itself. Hosts that worked around this by enqueuing
the job themselves + short-circuiting
moderation_field_changed_for_commit? can delete both workarounds.
2. ACTIVE STORAGE ATTACHMENTS NOW WORK OUT OF THE BOX. `moderates :avatar,
with: :img, mode: :flag` on a has_one_attached model previously
required every host to hand-roll the 3-method seam (AR dirty tracking
can't see attachment writes, and Active Storage clears
attachment_changes before after_commit). The concern now snapshots
filtered attachment writes in a before_save, consumes the one-shot
snapshot at commit (cleared via moderation_field_committed, which the
ensure already guaranteed), and both the concern and ClassifyJob treat
an unattached ActiveStorage::Attached proxy as blank - covering the
purge-between-enqueue-and-run race. The overridable seam stays for
richer cases; the dummy Comment now carries BOTH shapes (:image =
custom seam, :photo = native).
Tests: existing image tests updated to the enqueue+perform shape; new
coverage for never-inline, native tracking + re-save no-spam, vanished-
attachment no-op, and adapter_async?. 205 runs green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UMa8bEvYfi9Dr8RYhhfKze
@rameerez
rameerez merged commit 2c348b8 into mainJul 10, 2026
14 of 15 checks passed
@rameerez
rameerez deleted the fix/flag-async-adapters-via-classify-job branch July 10, 2026 18:41
rameerez added a commit that referenced this pull request Jul 10, 2026
CHANGELOG: fold the since-beta1 deltas (#3 async-adapter routing fix + Active
Storage attachment filtering, #4 Flag close sugar) into a proper
[1.0.0.beta2] section; bump Moderate::VERSION.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UMa8bEvYfi9Dr8RYhhfKze
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@rameerez
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix :flag async adapters (route through ClassifyJob) + out-of-the-box Active Storage attachment filtering - #3

Merged
rameerez merged 1 commit into
mainfrom
fix/flag-async-adapters-via-classify-job
Jul 10, 2026
Merged

Fix :flag async adapters (route through ClassifyJob) + out-of-the-box Active Storage attachment filtering#3
rameerez merged 1 commit into
mainfrom
fix/flag-async-adapters-via-classify-job

Conversation

@rameerez

Copy link
Copy Markdown
Owner

Found while wiring CarHey's avatar AI classifier — the first real-world async image adapter against the released 1.0.0.beta1.

1. Async adapters never actually ran in Moderate::ClassifyJob

beta1's :flag after_commit called Moderate.classifyinline for every adapter, including ones declaring synchronous? == false. Consequences:

  • a remote moderation API's network call ran inside the request that saved the content (1–5s stall per save, exactly what the docs say must never happen);
  • Moderate::ClassifyJob — whose header, and filters/base.rb's comments, promise this routing — was dead code nothing enqueued.

Fix: the concern probes the new public Configuration#adapter_async? (identical predicate to the :block validator's: async only when the adapter explicitly answers synchronous? == false) and enqueues ClassifyJob instead of classifying inline. The job re-reads the persisted value and files the Flag through the same Flag.flag! builder. Hosts that worked around the gap (enqueue-it-yourself + short-circuit moderation_field_changed_for_commit?) can delete both workarounds — CarHey will, on the next release bump.

2. moderates <attachment> works out of the box now

Previously every host had to hand-roll the three-method seam for a has_one_attached field (AR dirty tracking can't see attachment writes, and Active Storage clears attachment_changes before after_commit). Now the concern:

  • snapshots filtered attachment writes in a before_save (one-shot, consumed at commit, cleared through the existing moderation_field_committed ensure-hook);
  • treats an unattached ActiveStorage::Attached proxy as blank in both the concern and ClassifyJob (covers the purge-between-enqueue-and-run race).

The overridable seam remains for richer cases. The dummy Comment now carries both shapes: :image (custom seam, unchanged) and a new :photo (native, zero overrides).

Tests

205 runs, 0 failures. Existing image tests updated to the enqueue+perform shape; new coverage: async-never-inline, native attachment tracking + re-save-no-spam, vanished-attachment no-op, adapter_async?. Rubocop: no new offenses beyond the repo's pre-existing Metrics baseline.

Release note

This is a behavior change for any beta1 host silently relying on inline classification of an async adapter (there shouldn't be any — that path blocked requests). Suggest shipping as 1.0.0.beta2; CHANGELOG entries added under the unreleased 1.0.0 section.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UMa8bEvYfi9Dr8RYhhfKze

…nt support
Two host-facing fixes found while wiring CarHey's avatar AI classifier
(the first real-world async image adapter):
1. ASYNC ADAPTERS NEVER RAN IN THE JOB. The :flag after_commit called
Moderate.classify INLINE for every adapter - including ones declaring
synchronous? == false - so a remote moderation API's network call ran
inside the request that saved the content, and Moderate::ClassifyJob
(whose own docs, and filters/base.rb's, promised the routing) was dead
code nothing enqueued. The concern now probes the new
Configuration#adapter_async? (same predicate the :block validator uses)
and enqueues ClassifyJob instead; the job re-reads the persisted value
and files the Flag itself. Hosts that worked around this by enqueuing
the job themselves + short-circuiting
moderation_field_changed_for_commit? can delete both workarounds.
2. ACTIVE STORAGE ATTACHMENTS NOW WORK OUT OF THE BOX. `moderates :avatar,
with: :img, mode: :flag` on a has_one_attached model previously
required every host to hand-roll the 3-method seam (AR dirty tracking
can't see attachment writes, and Active Storage clears
attachment_changes before after_commit). The concern now snapshots
filtered attachment writes in a before_save, consumes the one-shot
snapshot at commit (cleared via moderation_field_committed, which the
ensure already guaranteed), and both the concern and ClassifyJob treat
an unattached ActiveStorage::Attached proxy as blank - covering the
purge-between-enqueue-and-run race. The overridable seam stays for
richer cases; the dummy Comment now carries BOTH shapes (:image =
custom seam, :photo = native).
Tests: existing image tests updated to the enqueue+perform shape; new
coverage for never-inline, native tracking + re-save no-spam, vanished-
attachment no-op, and adapter_async?. 205 runs green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UMa8bEvYfi9Dr8RYhhfKze
@rameerez
rameerez merged commit 2c348b8 into mainJul 10, 2026
14 of 15 checks passed
@rameerez
rameerez deleted the fix/flag-async-adapters-via-classify-job branch July 10, 2026 18:41
rameerez added a commit that referenced this pull request Jul 10, 2026
CHANGELOG: fold the since-beta1 deltas (#3 async-adapter routing fix + Active
Storage attachment filtering, #4 Flag close sugar) into a proper
[1.0.0.beta2] section; bump Moderate::VERSION.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UMa8bEvYfi9Dr8RYhhfKze
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@rameerez
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Fix :flag async adapters (route through ClassifyJob) + out-of-the-box Active Storage attachment filtering - #3

Merged
rameerez merged 1 commit into
mainfrom
fix/flag-async-adapters-via-classify-job
Jul 10, 2026
Merged

Fix :flag async adapters (route through ClassifyJob) + out-of-the-box Active Storage attachment filtering#3
rameerez merged 1 commit into
mainfrom
fix/flag-async-adapters-via-classify-job

Conversation

@rameerez

Copy link
Copy Markdown
Owner

Found while wiring CarHey's avatar AI classifier — the first real-world async image adapter against the released 1.0.0.beta1.

1. Async adapters never actually ran in Moderate::ClassifyJob

beta1's :flag after_commit called Moderate.classifyinline for every adapter, including ones declaring synchronous? == false. Consequences:

  • a remote moderation API's network call ran inside the request that saved the content (1–5s stall per save, exactly what the docs say must never happen);
  • Moderate::ClassifyJob — whose header, and filters/base.rb's comments, promise this routing — was dead code nothing enqueued.

Fix: the concern probes the new public Configuration#adapter_async? (identical predicate to the :block validator's: async only when the adapter explicitly answers synchronous? == false) and enqueues ClassifyJob instead of classifying inline. The job re-reads the persisted value and files the Flag through the same Flag.flag! builder. Hosts that worked around the gap (enqueue-it-yourself + short-circuit moderation_field_changed_for_commit?) can delete both workarounds — CarHey will, on the next release bump.

2. moderates <attachment> works out of the box now

Previously every host had to hand-roll the three-method seam for a has_one_attached field (AR dirty tracking can't see attachment writes, and Active Storage clears attachment_changes before after_commit). Now the concern:

  • snapshots filtered attachment writes in a before_save (one-shot, consumed at commit, cleared through the existing moderation_field_committed ensure-hook);
  • treats an unattached ActiveStorage::Attached proxy as blank in both the concern and ClassifyJob (covers the purge-between-enqueue-and-run race).

The overridable seam remains for richer cases. The dummy Comment now carries both shapes: :image (custom seam, unchanged) and a new :photo (native, zero overrides).

Tests

205 runs, 0 failures. Existing image tests updated to the enqueue+perform shape; new coverage: async-never-inline, native attachment tracking + re-save-no-spam, vanished-attachment no-op, adapter_async?. Rubocop: no new offenses beyond the repo's pre-existing Metrics baseline.

Release note

This is a behavior change for any beta1 host silently relying on inline classification of an async adapter (there shouldn't be any — that path blocked requests). Suggest shipping as 1.0.0.beta2; CHANGELOG entries added under the unreleased 1.0.0 section.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UMa8bEvYfi9Dr8RYhhfKze

…nt support
Two host-facing fixes found while wiring CarHey's avatar AI classifier
(the first real-world async image adapter):
1. ASYNC ADAPTERS NEVER RAN IN THE JOB. The :flag after_commit called
Moderate.classify INLINE for every adapter - including ones declaring
synchronous? == false - so a remote moderation API's network call ran
inside the request that saved the content, and Moderate::ClassifyJob
(whose own docs, and filters/base.rb's, promised the routing) was dead
code nothing enqueued. The concern now probes the new
Configuration#adapter_async? (same predicate the :block validator uses)
and enqueues ClassifyJob instead; the job re-reads the persisted value
and files the Flag itself. Hosts that worked around this by enqueuing
the job themselves + short-circuiting
moderation_field_changed_for_commit? can delete both workarounds.
2. ACTIVE STORAGE ATTACHMENTS NOW WORK OUT OF THE BOX. `moderates :avatar,
with: :img, mode: :flag` on a has_one_attached model previously
required every host to hand-roll the 3-method seam (AR dirty tracking
can't see attachment writes, and Active Storage clears
attachment_changes before after_commit). The concern now snapshots
filtered attachment writes in a before_save, consumes the one-shot
snapshot at commit (cleared via moderation_field_committed, which the
ensure already guaranteed), and both the concern and ClassifyJob treat
an unattached ActiveStorage::Attached proxy as blank - covering the
purge-between-enqueue-and-run race. The overridable seam stays for
richer cases; the dummy Comment now carries BOTH shapes (:image =
custom seam, :photo = native).
Tests: existing image tests updated to the enqueue+perform shape; new
coverage for never-inline, native tracking + re-save no-spam, vanished-
attachment no-op, and adapter_async?. 205 runs green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UMa8bEvYfi9Dr8RYhhfKze
@rameerez
rameerez merged commit 2c348b8 into mainJul 10, 2026
14 of 15 checks passed
@rameerez
rameerez deleted the fix/flag-async-adapters-via-classify-job branch July 10, 2026 18:41
rameerez added a commit that referenced this pull request Jul 10, 2026
CHANGELOG: fold the since-beta1 deltas (#3 async-adapter routing fix + Active
Storage attachment filtering, #4 Flag close sugar) into a proper
[1.0.0.beta2] section; bump Moderate::VERSION.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UMa8bEvYfi9Dr8RYhhfKze
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@rameerez