Created
May 28, 2026 12:23
-
-
Save rlewkowicz/2deb1ec8c59225dcd297a1812473ecad to your computer and use it in GitHub Desktop.
actionplan example
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Summary | |
| Replace the fixed-ratio rationale-MIL/span decoder path with a dynamic hard-mask rationalizer. The Python model owns the behavior: a generator emits per-token, per-label Bernoulli mask logits; a predictor must classify each label from selected evidence; a complement predictor pass verifies that removed evidence loses positive signal; and RELAX supplies the generator gradient for discrete masks after a straight-through Binary Concrete warmup. The selected span length is not a configured ratio, min token count, max token count, POS class, punctuation rule, label rule, or sentence-template rule. Span length emerges from task sufficiency, complement pressure, L0 sparsity, continuity, and validation-calibrated thresholds. | |
| The browser/plugin stays final-span-only. It consumes `final_span_logits`, applies learned/calibrated mask thresholds from the manifest, and emits contiguous selected token runs as final spans. | |
| # Scope | |
| In scope: | |
| - Loss config and phase loss cutover in `model/spltk/engine/fused/config.py:32-75`, `model/spltk/engine/fused/config.py:317-342`, `model/spltk/engine/fused/config.py:1974-2025`, `model/spltk/engine/fused/trainer.py:156-215`, and `model/spltk/engine/fused/trainer.py:297-305`. | |
| - Profile defaults in `model/spltk/cfg/model2vec-training-profiles.json:32-66` and `model/spltk/cfg/model2vec-training-profiles.json:192`. | |
| - Generator, mask sampling, evidence pooling, and surrogate wiring in `model/spltk/nn/modules/fused.py:145-188`, `model/spltk/nn/modules/fused.py:430-477`, `model/spltk/nn/modules/fused.py:720-768`, `model/spltk/nn/modules/fused.py:876-960`, and `model/spltk/nn/modules/fused.py:1048-1058`. | |
| - New reusable tensor utilities in `model/spltk/nn/modules/dynamic_rationale.py`. | |
| - Dynamic rationale objective replacement in `model/spltk/engine/fused/losses.py:626-928`. | |
| - Trainer output-key planning and loss routing in `model/spltk/engine/fused/trainer.py:1568-1655`, `model/spltk/engine/fused/trainer.py:2418-2505`, `model/spltk/engine/fused/trainer.py:3056-3108`, and `model/spltk/engine/fused/trainer.py:6778-6850`. | |
| - Runtime row-store output-key activation in `model/spltk/engine/fused/rows/stores.py:91-169`. | |
| - Validation/report arrays that read final span logits in `model/spltk/engine/fused/validation.py:3887`, `model/spltk/engine/fused/validation.py:4762`, and `model/spltk/engine/fused/validation.py:5290`. | |
| - Export and manifest contract in `model/spltk/export/export.py:340-347`, `model/spltk/engine/validation/validators.py:533-541`, `scripts/plugin-pipeline/assets.mjs:385-392`, `scripts/plugin-pipeline/handoff.mjs:140-160`, and `src/embedded/model2vecProvider.ts:1268-1296`. | |
| - Browser final-span decoding in `src/embedded/model2vecProvider.ts:1876-1969` and `src/embedded/model2vecProvider.ts:2084-2118`. | |
| - Benchmark/report paths in `scripts/plugin-pipeline/browser-ort-runner.mjs:1021-1065`, `scripts/plugin-pipeline/baseline-evaluator.mjs:85-137`, and `scripts/plugin-pipeline/final-report.mjs`. | |
| Out of scope: | |
| - Fixed `keepRatio`, top-k ratio selection, min/max token clamps, or per-label token-count rules. | |
| - POS, punctuation, pronoun, article, stopword, sentence-length, or cadence filters. | |
| - Label-name-specific span rules. | |
| - Route, candidate-span, sentence-embedding, or classifier-probability tensors in the production browser ONNX output. | |
| - Regression test authoring. | |
| # Architecture | |
| ## Algorithm Choice | |
| - Use RELAX as the primary discrete-mask estimator because the learned surrogate `c_phi` can adapt when fractional text masks are not semantically equivalent to hard chunks. REBAR remains a config fallback because it is the special case that uses `f(relaxed_mask)` as the control variate. | |
| - Use straight-through Binary Concrete only as warmup and emergency fallback. It is lower-variance and simple, but biased. RELAX is the target training mode for the generator once the predictor can score selected and complement evidence. | |
| - Keep the generator output independent of label text, POS tags, punctuation classes, and sentence structure. The only masking constraint is the existing sequence validity mask. | |
| ## Dynamic Rationale Objective | |
| - The generator is the existing `rationale_token_head` over final-span features. It outputs `mask_logits` shaped `[batch, pos_tokens, labels]`. | |
| - Training samples a hard Bernoulli mask per token/label: | |
| `z = logit(p) + log(u / (1 - u))` | |
| `b = H(z)` | |
| `y = sigmoid(z / temperature)` | |
| `b_st = b.detach() - y.detach() + y` | |
| - RELAX also samples conditional relaxed logits: | |
| `u_tilde = where(b == 1, 1 - p + p * v, (1 - p) * v)` | |
| `z_tilde = logit(p) + log(u_tilde / (1 - u_tilde))` | |
| `y_tilde = sigmoid(z_tilde / temperature)` | |
| - The predictor receives selected-mask evidence, complement-mask evidence, relaxed evidence, and conditional-relaxed evidence through one vectorized path. Stack mask variants on the batch axis before pooling so the text encoder output is reused and the packed classifier heads run on a single concatenated tensor. | |
| - The hard selected predictor loss updates predictor parameters directly. Generator gradients for hard mask decisions come from RELAX: | |
| `g_relax = (f(b) - c_phi(y_tilde)).detach() * log_prob(b | p) - c_phi(y_tilde) + c_phi(y)` | |
| - The objective `f(b)` is label-agnostic and dynamic: | |
| `task = BCE(selected_logits, targets)` | |
| `complement = positive_mask * softplus(complement_logits - selected_logits + margin)` | |
| `sparsity = mean(b over valid tokens)` | |
| `continuity = mean(abs(b[t] - b[t - 1]) over adjacent valid tokens)` | |
| `f(b) = taskWeight * task + complementWeight * complement + sparsityWeight * sparsity + continuityWeight * continuity` | |
| - There is no target selected ratio. Diagnostics record selected ratios and token counts, but they do not drive top-k or min/max selection. | |
| - Final span emission is deterministic: calibrated thresholding over `final_span_logits`, then contiguous selected token runs. Adjacent spans are not bridged across unselected tokens. | |
| # Phases | |
| ## Phase 1: Replace Fixed-Ratio Config With Dynamic Rationale Config | |
| - In `model/spltk/engine/fused/config.py:32-75`, remove active `rationaleMil` and `rationaleBottleneck` defaults and add `dynamicRationale`. | |
| - Add `DEFAULT_DYNAMIC_RATIONALE_LOSS_CONFIG` with: | |
| - `estimator`: `"relax"` | |
| - `warmupEstimator`: `"straightThrough"` | |
| - `warmupEpochs` | |
| - `temperatureStart` | |
| - `temperatureEnd` | |
| - `taskWeight` | |
| - `complementWeight` | |
| - `complementMargin` | |
| - `sparsityWeight` | |
| - `continuityWeight` | |
| - `surrogateWeight` | |
| - `surrogateVarianceWeight` | |
| - `positiveWeight` | |
| - `negativeWeight` | |
| - `thresholdCalibrationMetric` | |
| - Do not include `keepRatio`, `selectionKeepRatio`, `effectiveTokenKeepRatio`, `minTokens`, `maxTokens`, or top-k fields. | |
| - In `model/spltk/engine/fused/config.py:317-342` and `model/spltk/engine/fused/config.py:1974-2025`, allow `dynamicRationale` in `spanFineTune` loss weights and reject the retired fixed-ratio rationale losses from active profiles. | |
| - In `model/spltk/engine/fused/trainer.py:156-215`, replace `_rationale_mil_loss_config` and `_rationale_bottleneck_loss_config` with `_dynamic_rationale_loss_config`. | |
| - In `model/spltk/engine/fused/trainer.py:297-305`, replace `_SPAN_FINETUNE_PHASE_LOSSES` entries with `dynamicRationale`, `routerProposal`, and any existing classifier feedback loss that still applies. | |
| - In `model/spltk/cfg/model2vec-training-profiles.json:32-66`, set span fine-tune active losses to `dynamicRationale` and a low-weight `routerProposal`; remove `rationaleMil`, `rationaleBottleneck`, `candidateSpan`, and `finalSpan` from active loss weights. | |
| - Confirm `rg -n "rationaleMil|rationaleBottleneck|selectionKeepRatio|effectiveTokenKeepRatio|keepRatio|minTokens|maxTokens|maxTopK|minSelectedTokens|effectiveTokenTarget" model/spltk/engine/fused model/spltk/cfg/model2vec-training-profiles.json` has no active training objective hits. | |
| ## Phase 2: Add Vectorized Dynamic Rationale Utilities | |
| - Add `model/spltk/nn/modules/dynamic_rationale.py` with: | |
| - `sample_logistic_noise_like(logits, generator=None)` | |
| - `binary_concrete_relaxation(mask_logits, valid_mask, temperature, training)` | |
| - `straight_through_binary_mask(mask_logits, valid_mask, temperature, training)` | |
| - `conditional_binary_concrete(mask_logits, hard_mask, valid_mask, temperature, generator=None)` | |
| - `bernoulli_log_prob(mask_logits, hard_mask, valid_mask)` | |
| - `masked_mean_by_pair(values, pair_mask, denominator_floor=1.0)` | |
| - `mask_continuity(mask, valid_mask)` | |
| - `mask_sparsity(mask, valid_mask)` | |
| - Implement all functions with broadcasted tensor operations over `[batch, tokens, labels]`. | |
| - Clamp uniform samples with dtype-safe eps before `log` or `logit`. | |
| - Ensure invalid sequence positions produce zero hard mask, zero soft mask, and zero log-prob contribution. | |
| - Keep the module free of label names, POS classes, punctuation checks, token text checks, and sentence-boundary rules. | |
| ## Phase 3: Wire Generator, Predictor, Complement, And Surrogate Paths | |
| - In `model/spltk/nn/modules/fused.py:145-188`, keep `rationale_token_head` as the generator and add a compact RELAX surrogate head. The surrogate should consume label-aligned evidence features plus mask statistics, not token strings or label names. | |
| - In `model/spltk/nn/modules/fused.py:430-477`, split the current `rationale_classifier_outputs` into reusable helpers: | |
| - `rationale_evidence_inputs(...)` returns label-aligned pooled evidence embeddings and mask statistics. | |
| - `rationale_classifier_outputs(...)` calls the helper and packed classifier heads. | |
| - `rationale_surrogate_outputs(...)` calls the helper and surrogate head. | |
| - In `model/spltk/nn/modules/fused.py:720-768`, make `final_span_logits` equal masked generator logits directly. Do not reconstruct logits from sparse normalized weights. | |
| - Add output support for: | |
| - `rationale_token_logits` | |
| - `rationale_token_hard_mask` | |
| - `rationale_token_soft_mask` | |
| - `rationale_token_conditional_soft_mask` | |
| - `rationale_classifier_logits` | |
| - `rationale_complement_classifier_logits` | |
| - `rationale_surrogate_logits` | |
| - `rationale_conditional_surrogate_logits` | |
| - In `model/spltk/nn/modules/fused.py:876-960`, request text features once, stack selected/complement/relaxed/conditional-relaxed masks on the batch axis, and run pooled evidence through packed heads in one vectorized call. | |
| - In `model/spltk/nn/modules/fused.py:1048-1058`, expose diagnostic outputs only for Python training/validation; production export still exposes only `final_span_logits`. | |
| - Confirm dense `classifier_logits` remains available for diagnostics but is not the span fine-tune task source when `dynamicRationale` is active. | |
| ## Phase 4: Replace MIL/Bottleneck Losses With Dynamic RELAX Loss | |
| - In `model/spltk/engine/fused/losses.py:626-928`, remove `_rationale_mil_loss` and `_rationale_bottleneck_loss` and add `_dynamic_rationale_loss`. | |
| - Inputs must include selected logits, complement logits, hard mask, soft mask, conditional soft mask, mask logits, surrogate outputs, targets, label mask, row loss mask, valid token mask, and sample weights. | |
| - Compute active pair weights from existing classifier target masks and row masks only. | |
| - Compute selected task BCE for positives and negatives with existing classifier positive weights. | |
| - Compute complement pressure only where the label is positive and active. | |
| - Compute sparsity and continuity over valid token positions only. | |
| - Build `f_hard` per row/label and reduce with pair weights. Do not reduce `f_hard` before the RELAX policy term. | |
| - For `estimator == "relax"`, add the policy-gradient loss: | |
| - `((f_hard - c_tilde).detach() * log_prob_hard * pair_weights).sum() / pair_weights.sum()` | |
| - `-(c_tilde * pair_weights).sum() / pair_weights.sum()` | |
| - `+(c_y * pair_weights).sum() / pair_weights.sum()` | |
| - For `estimator == "rebar"`, set `c_tilde = f(y_tilde)` and `c_y = f(y)` using the same selected/complement/sparsity/continuity formulas on relaxed masks. | |
| - For `estimator == "straightThrough"`, backprop through `b_st` and skip score-function terms. | |
| - Train the surrogate with `surrogateVarianceWeight` using the RELAX gradient-variance objective. Use `surrogateWeight` MSE-to-`f_hard.detach()` only during warmup or when variance-gradient construction is disabled by config. | |
| - Return diagnostics: | |
| - `activePairs` | |
| - `positiveActivePairs` | |
| - `taskLoss` | |
| - `complementLoss` | |
| - `sparsityLoss` | |
| - `continuityLoss` | |
| - `relaxPolicyLoss` | |
| - `surrogateLoss` | |
| - `meanSelectedRatio` | |
| - `p10SelectedRatio` | |
| - `p50SelectedRatio` | |
| - `p90SelectedRatio` | |
| - `meanPositiveSelectedRatio` | |
| - `meanContinuityFlips` | |
| - `meanLogProbHard` | |
| - Ensure diagnostics never become selection constraints. | |
| ## Phase 5: Cut Over Trainer, Row Stores, And Prediction Aliases | |
| - In `model/spltk/engine/fused/trainer.py:1568-1655`, request all dynamic rationale outputs when `dynamicRationale` is active. | |
| - In `model/spltk/engine/fused/trainer.py:2418-2505`, replace the `rationaleMil` and `rationaleBottleneck` branches with one `dynamicRationale` branch that calls `_dynamic_rationale_loss`. | |
| - In `model/spltk/engine/fused/trainer.py:3056-3108`, ensure `candidateSpan` and `finalSpan` losses are inactive during dynamic rationale span fine-tune. | |
| - In `model/spltk/engine/fused/trainer.py:6778-6850`, alias prediction classifier heads to `rationale_classifier_logits` when `dynamicRationale` is the active runtime loss. | |
| - In `model/spltk/engine/fused/rows/stores.py:91-169`, update runtime output-key selection so cached training views include the dynamic rationale outputs only when the loss is active. | |
| - Confirm output-key planning does not request route, candidate, sentence embedding, or dense classifier outputs for production final-span export. | |
| ## Phase 6: Calibrate Dynamic Thresholds And Export Final-Span-Only Runtime | |
| - In `model/spltk/engine/fused/validation.py:3887`, replace hard `logits >= 0.0` span prediction metrics with thresholded predictions from calibrated per-label final-span thresholds. | |
| - In `model/spltk/engine/fused/validation.py:4762` and `model/spltk/engine/fused/validation.py:5290`, report selected-ratio distributions as diagnostics only. | |
| - In `model/spltk/export/export.py:340-347`, replace `finalSpanSelection.keepRatio` with: | |
| - `scoreMode: "calibrated_binary_mask"` | |
| - `thresholdSource: "span_thresholds"` | |
| - `mergeAdjacent: false` | |
| - Remove manifest `keepRatio` from final-span selection. | |
| - In `model/spltk/engine/validation/validators.py:533-541`, validate that the manifest requires final-span-only ONNX output and dynamic calibrated mask thresholding. | |
| - In `scripts/plugin-pipeline/assets.mjs:385-392` and `scripts/plugin-pipeline/handoff.mjs:140-160`, require the new manifest policy and reject `keepRatio`. | |
| - In `src/embedded/model2vecProvider.ts:1268-1296`, update manifest validation for `calibrated_binary_mask` and no `keepRatio`. | |
| - In `src/embedded/model2vecProvider.ts:1876-1969`, decode final spans by applying the label threshold to `final_span_logits`; group contiguous selected tokens into spans; do not top-k, ratio-clamp, min/max-clamp, POS-filter, punctuation-filter, or bridge unselected tokens. | |
| - In `src/embedded/model2vecProvider.ts:2084-2118`, remove merge-adjacent behavior if contiguous selected runs are the only supported final shape. | |
| ## Phase 7: Run Full Validation, Benchmarks, Packaging, And Final Report | |
| - Run `python -m compileall model/spltk`. | |
| - Run `spltk pipeline train`. | |
| - Run `spltk pipeline export`. | |
| - Run `node scripts/bsdetector-plugin-pipeline.mjs evaluate-baseline`. | |
| - Run `npm run plugin:evaluate`. | |
| - Run `npm run plugin:benchmark`. | |
| - Run `npm run build:chrome`. | |
| - Run `node scripts/plugin-pipeline/assets.mjs source`. | |
| - Show the `abstract_noun_density` subset from `model/artifacts/spltk-model2vec/baseline-evaluation-report.json` as a sanity slice only, and verify spans are hard final spans from `final_span_logits`. | |
| - Update `model/artifacts/spltk-model2vec/final.md` through the existing final-report pipeline and break down status by training, export shape, browser evaluation, benchmark, and packaging. | |
| # Post-Completion Verification | |
| - `public/models/spltk-model2vec/manifest.json` has `runtime.onnx.outputNames` exactly `["final_span_logits"]`. | |
| - `public/models/spltk-model2vec/manifest.json` has `fused.finalSpanSelection.scoreMode` exactly `"calibrated_binary_mask"`. | |
| - `public/models/spltk-model2vec/manifest.json` has no `fused.finalSpanSelection.keepRatio`. | |
| - `rg -n "FINAL_SPAN_NON_CONTENT|FINAL_SPAN_BRIDGE|keepRatio|minTokens|maxTokens|maxTopK|minSelectedTokens|effectiveTokenTarget|selectionKeepRatio|effectiveTokenKeepRatio" src model/spltk/engine/fused model/spltk/export scripts/plugin-pipeline -g '*.ts' -g '*.py' -g '*.mjs' -g '*.json'` has no active final-span selection hits. | |
| - Dynamic rationale diagnostics include nonzero `dynamicRationale.activePairs`, finite `taskLoss`, finite `complementLoss`, finite `sparsityLoss`, finite `continuityLoss`, and selected-ratio percentiles that vary across rows and labels. | |
| - Browser/plugin emitted spans are derived only from `final_span_logits` and manifest thresholds. | |
| - Punctuation, articles, pronouns, and short cadence fragments can appear in selected spans when the model assigns them mask logits above threshold; there is no code path that excludes them by token class. |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
actionplan.md
An
actionplan.mdis an executable phased plan for another agent.Do not write user-facing notes, answers, commentary, or reminders. Do not repeat constraints already stated in
AGENTS.md.Plans must contain concrete phases, important files, and specific actions. Do not include vague directives such as “explore". Confirm the changes, files and line numbers in advance, things to look out for and possible post cleanup. Phases can overlap if needed.
When writing an action plan:
When executing an action plan:
actionplan.mdatomically after each major completed change.actionplan.md.Treat interface changes as complete cutovers. Do not preserve backward compatibility unless explicitly required. Do not make incremental compatibility layers.
Do not build or test until all phases are complete.
When executing an action plan, commit changes after each phase is removed
Code Churn
Do not avoid code churn when it improves the system.
Prioritize performance, clear ownership, proper class/template structure, healthy architecture, modularity, reuse, and limited blast radius. Reduce legacy code, fold them into new shared helpers, classes, functions, templates, factories, or other reusable abstractions where appropriate.