Skip to content

Kei Merge + 1.6 + NF Update to 1.6 - #146

Draft
Rojikku wants to merge 45 commits into
NovelSourcery:mainfrom
Rojikku:KeiMess
Draft

Kei Merge + 1.6 + NF Update to 1.6#146
Rojikku wants to merge 45 commits into
NovelSourcery:mainfrom
Rojikku:KeiMess

Conversation

@Rojikku

@Rojikku Rojikku commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Perhaps a bit of a mess, needs review, but appears to work maybe.

Checklist:

  • Updated extVersionCode value in build.gradle for individual extensions
  • Updated overrideVersionCode or baseVersionCode as needed for all multisrc extensions
  • Referenced all related issues in the PR body (e.g. "Closes #xyz")
  • Added the isNsfw = true flag in build.gradle when appropriate
  • Have not changed source names
  • Have explicitly kept the id if a source's name or language were changed
  • Have tested the modifications by compiling and running the extension through Android Studio
  • Have removed web_hi_res_512.png when adding a new extension
  • This PR is AI-assisted, I have reviewed the changes manually and confirmed they are not slop

Rojikku added 30 commits July 13, 2026 02:04
The modern PluginExtension plugin was already registered against
ns.plugins.extension in build-logic, but ns.versions.toml never
declared it - only extension-legacy existed. Gradle configuration
failed the moment anything touched the modern plugin id.
ns.plugins.extension needs .asProvider() to disambiguate from the
extension-legacy group accessor Gradle generates (extension-legacy
is treated as a nested extension.legacy path, colliding with the
top-level extension alias).

PluginExtensionLegacy also had no gradlePlugin registration at all,
despite ns.versions.toml already declaring its id and real extensions
(e.g. calibre) applying it - every legacy extension currently fails
to resolve its own plugin.
…lassName

PluginExtensionLegacy.kt imported keiyoushi.gradle.extensions.baseVersionCode
for theme.baseVersionCode, but no such property was ever defined in this
repo's history - it now bridges to the theme's KeiyoushiMultisrcExtension,
matching how themes are configured today (e.g. lib-multisrc/bakkin).

GenerateKeepRulesTask's property was renamed from extClass to className
at some point, but PluginExtensionLegacy's call site was never updated.
libVersion is a new concept - existing extensions and themes (e.g.
lib-multisrc/bakkin) predate it and never set it, so keiyoushi.libVersion
had no value and configuration failed for every one of them. Defaulting
to "1.4" keeps everything that doesn't care building exactly as before;
only extensions that actually need 1.6 have to opt in explicitly.
resolve_ext() only matched the modern DSL's theme= property in
build.gradle.kts, and only looked at build.gradle.kts files at all.
141 of 142 real extensions are still on legacy Groovy build.gradle
using themePkg=, so a themed extension's rebuild would silently stop
triggering when its shared multisrc theme changed.
create-repo.py was rewritten to read tachiyomix.name/contentWarning/
extensionLib from every extension's manifest, but :core's shared
AndroidManifest.xml (used by all 141+ legacy Groovy extensions) only
ever emitted the old tachiyomi.novelextension.* keys - the new script
would crash on the first real APK it processed.

Followed upstream kei's own migration commit (7fb92dc88) as a reference:
they added the new tachiyomix.* keys alongside the old ones rather than
replacing them, then separately did a mass conversion of every extension
to the modern DSL. We're keeping PluginExtensionLegacy (141 extensions
haven't been converted), so the additive keys plus new placeholders on
the legacy plugin's manifestPlaceholders is the equivalent bridge.

extensionLib is hardcoded to "1.4" for legacy extensions since they're
not allowed to declare libVersion at all (see the assertWithoutFlag
guard in PluginExtensionLegacy). Verified end-to-end via calibre's
merged manifest.
…rge-repo.py

Reads the new manifest metadata (tachiyomix.name/contentWarning/extensionLib)
via aapt badging instead of the old flat index fields, and writes both the
new protobuf-backed index.json/index.pb and a reconstructed index.min.json
for backwards compatibility with existing installed clients (whose repo URL
still points at index.min.json, per commit-repo.sh's jsdelivr purge).

Fixed two bugs found while validating this against the real repo:
- APK_BASE_URL/ICON_BASE_URL in create-repo.py pointed at upstream
  keiyoushi/extensions instead of this repo's own novelsourcery/extensions.
  Since the new merge-repo.py no longer reconstructs these URLs itself
  (it now merges already-built Extension messages directly), whatever
  create-repo.py writes flows straight through to the published index.
- merge-repo.py's final index_pb2.Index(...) construction still referenced
  legacy_index and extract_extension_lib, both removed earlier in the same
  rewrite - guaranteed NameError on first run. Replaced with the
  already-merged, already-typed all_extensions list.
:core needs Call.awaitSuccess(), only available in extensions-lib 1.6+
(tagged v1.6.0-1, which now also has RateLimited - see extensions-lib
commit c81a5f8). But the 141 existing legacy extensions and the
lib-multisrc themes were built against extensions-lib 1.4.4-r60-3,
which has RefreshContext support that the 1.6 line doesn't carry yet -
bumping the single shared `tachiyomi-lib` pin globally would have kept
:core compiling while silently breaking every RefreshContext-using
extension (e.g. NovelFire).

Added a second `tachiyomi-lib-16`/`common16` bundle pinned at v1.6.0-1,
alongside the original `tachiyomi-lib`/`common` (still v1.4.4-r60-3).
:core now compiles specifically against common16 (it doesn't touch
RefreshContext, so this is safe and independent of what individual
extensions compile against). PluginExtension/PluginMultiSrc now pick
common vs common16 based on the declared keiyoushi.libVersion, resolved
in afterEvaluate since Gradle's dependency-notation parser needs the
bundle passed as its own directly-typed accessor (a .flatMap'd Provider
loses whatever internal type tag makes bundle expansion work - confirmed
via UnsupportedNotationException when that was tried first). Extended
VALID_LIB_VERSIONS to allow "1.6".

Verified end-to-end: :core, calibre (legacy, 1.4), and bakkin (theme,
1.4) all compile; calibre's full debug APK builds and its aapt badging
output shows tachiyomix.extensionLib as 0x3fb33333 - the correct IEEE-754
hex encoding of 1.4, confirming create-repo.py's hex_to_float() will
decode it correctly.
Preparing to flip the default in PluginMultiSrc from 1.4 to 1.6 (we
plan to phase 1.4 out entirely) - every existing theme needs to keep
its current, unverified-for-1.6 behavior explicitly rather than
silently inheriting whatever the new default becomes. Two of these
(madaranovel, readnovelfull) genuinely use RefreshContext, which only
exists in the 1.4 line - confirmed via compile that this preserves
their behavior exactly.
Same reasoning as the multisrc theme commit - every one of the 141
existing legacy Groovy extensions gets an explicit libVersion so
flipping the modern DSL's default away from 1.4 doesn't silently
change what any of them compile against. Also prepares each file for
PluginExtensionLegacy actually requiring and using this value.
…onLegacy

Flips keiyoushi.libVersion's convention from "1.4" to "1.6" in both
PluginExtension and PluginMultiSrc, now that every existing consumer
has an explicit "1.4" stamp preserving its current behavior. New
extensions/themes that don't declare libVersion now default to 1.6.

PluginExtensionLegacy previously forbade declaring libVersion at all
(assertWithoutFlag(!extra.has("libVersion"))) and hardcoded everything
to 1.4. Since every legacy extension now declares it explicitly, this
plugin requires it instead, and actually uses the declared value:
- versionName is "$libVersion.$versionCode" instead of a hardcoded "1.4.*"
- tachiyomix.extensionLib manifest metadata uses the real value
- compileOnly picks common vs common16 based on the declared value
  (in afterEvaluate, same reason as PluginExtension/PluginMultiSrc -
  a bundle Provider needs to stay its own directly-typed accessor for
  Gradle's dependency-notation parser to expand it)
- a themed extension's libVersion must match its theme's, mirroring
  the existing baseVersionCode/theme consistency check

Added a bare Project.libVersion accessor (mirroring baseVersionCode)
for themes, which use the flat property style rather than the nested
keiyoushi{} DSL block.

Verified end-to-end: full ./gradlew help configuration succeeds across
all 220 restamped files; :core (1.6), calibre (legacy, no theme),
madaranovel/readnovelfull (themes using RefreshContext, 1.4), and
azora/freewebnovel (real extensions consuming those themes) all
compile. Built azora's full debug APK - its manifest shows
versionName=1.4.4 and tachiyomix.extensionLib=0x3fb33333 (1.4 in hex),
confirming the whole legacy+theme+libVersion chain works correctly
end-to-end.
paginatedChapterList only ever read context.existingChapters - never
mangaId, lastFetchTime, or forceRefresh - so there's no reason for it
to depend on RefreshContext at all. Takes existingChapters: List<SChapter>
directly, matching getMangaUpdate's own chapters parameter, which is
what every caller now has on hand post-migration.
RefreshContext (and the getChapterList(manga, context) override it
required) is fork-only and deprecated (tsundoku PR #325) in favor of
upstream's getMangaUpdate, which takes existing chapters directly as
a parameter - and that method doesn't exist at all on the extensions-lib
1.4 line, so this requires the libVersion bump. The manga-details half
of getMangaUpdate calls mangaDetailsRequest/mangaDetailsParse directly
(both deprecated-but-present on 1.6) since the plain getMangaDetails(manga)
suspend shortcut was removed entirely in favor of the combined method.

Verified via a real debug APK build: manifest shows versionName=1.6.8
and tachiyomix.extensionLib=0x3fcccccd (1.6 in hex).
Same migration as NovelFire - RefreshContext-based getChapterList
override replaced with a getMangaUpdate override, using the passed-in
chapters parameter and mangaDetailsRequest/mangaDetailsParse directly.
…ibVersion 1.6

Same RefreshContext -> getMangaUpdate migration as NovelFire/LightNovelWorld,
applied to the shared readnovelfull theme. Since it's a shared base class,
bumping its libVersion to 1.6 requires every extension using it to match
(per the theme/extension libVersion consistency check) even though most of
them (allnovel, allnovelfull, freewebnovel, libread, lightnovelplus,
novelbin, novelfull, readnovelfull) never override getChapterList
themselves and needed no code changes - only the version bump.

NovelLive extends the theme but had its own separate getChapterList
override (a different chapter-list strategy), migrated the same way.
….Builder

Adds rateLimit(minimumDelayMillis, permits) on top of the existing
rule-based rateLimit(permits, period, interval, shouldLimit), mapping
minimumDelayMillis onto period (not interval) - an interval floor would
force a minimum gap between every dispatch regardless of the permits
budget, silently defeating any permits value above 1.

Bundled in :core, so it's dexed into each extension's own APK and keeps
working even if the host app's own rate limiting is missing, disabled,
or stripped by a hostile fork - the whole point of RateLimited sources
self-enforcing rather than relying solely on the app.
Defaults chosen from evidence already in this file: NovelFire actively
detects and surfaces the site's own "You are being rate limited"
response (NovelFireThrottlingError), so these are a real floor, not a
guess.

- minimumDelayMillis = 1000 (respects the site's own detected throttling)
- recommendedDelayMillis = 1500
- recommendedPermits = 2 (lets the common case - detail fetch + one
  chapter-list page - through without an artificial stall)

Wires the :core rateLimit() self-throttle into the client directly, so
this keeps working even if the host app's rate limiting is missing or
disabled. Verified the relevant keiyoushi.network.RateLimit* classes
are present in NovelFire's own compiled dex (not just the app's),
confirming the self-throttle is genuinely bundled into the extension's
own APK.
extensions-lib's index.proto now carries isNovel on Extension
(reserved 8000+ fork-field block, not an upstream field). Without
this, create-repo.py's isNovel key made merge-repo.py's strict
json_format.Parse reject every extension outright.
…tructions

git apply --index --3way exits non-zero even when it successfully applies
with conflict markers, so the old classification lumped genuine failures
together with resolvable conflicts, dumping a redundant raw patch for the
latter while also leaving it dirty in the tree. Split into three buckets
(clean / conflicted / failed) so conflicted files are left with their
markers for direct resolution.

Also: the printed "undo" instructions said `git reset --hard kei-last`,
but kei-last points into kei's disjoint commit history, not ours -- doing
that would replace the whole tree with kei's raw upstream. Since all work
happens on a throwaway branch, fixed the instructions to just abandon it.

.kei-sync is now committed automatically once every file at least applied
(clean or with conflict markers), rather than left staged for manual
commit -- a bad sync is cheap to revert by discarding the workbranch.

Also: workbranch naming keyed off target SHA instead of date (so re-runs
on the same day resume instead of colliding), added binary-file handling
(take upstream wholesale if undiverged locally, else flag for manual
pick), and a remote-existence check.
Answers two questions that came up while planning the big pending kei
refactor (89b3b42a4..7d508b834): "what commit did I actually run this
against" and "what's left to deal with, without re-running the merge and
re-touching things I've already dealt with."

- Every invocation (apply or status) appends a timestamped block to
  .git/kei-sync.log with the full sync range, workbranch, and per-file
  buckets -- a durable local audit trail independent of branch names or
  the reject dir (which gets wiped and regenerated each run).

- New `--status` mode: read-only, makes no changes. For each file in the
  pending range, checks whether the working tree already matches
  upstream (resolved), still matches the old sync point exactly
  (untouched), or differs from both (diverged). Diverged entries print a
  diff-size hint against upstream, since a small diff there is often a
  fork customization layered on an already-incorporated change (e.g. the
  isNovel field we hand-added to index_pb2.py/.pyi will never byte-match
  kei's version) rather than something still unresolved. Safe to re-run
  any time; exits non-zero while anything's pending so it doubles as a
  readiness check.

- New .kei-sync-ignore file (repo-relative paths, one per line): files
  permanently excluded from both modes, for cases where we've decided a
  path should never be auto-synced from upstream again. This is distinct
  from a file merely being resolved for the current range -- ignored
  paths drop out of consideration entirely, forever, until removed from
  the list.

Verified all three flows (clean auto-commit through conflicts, hard
failures correctly blocking the .kei-sync bump, and ignore-list
suppression working in both modes) against a synthetic repo, and ran
--status and a dry apply against the real pending kei range in a scratch
clone to confirm the bucketing matches reality.
…cision annotations)

Toward feeling like a native git subcommand rather than a one-off script:
- -h/--help prints usage.
- Reject unexpected extra arguments (only $1 was ever checked before).
- Preserve git apply's stderr for genuinely-unappliable files into
  $REJECT_DIR/$f.patch.err instead of discarding it via 2>/dev/null --
  the raw patch alone didn't say *why* it failed.

Also wires --status up to .git/kei-sync-decisions.jsonl (written by the
kei-sync-vscode helper, added next commit, at the moment a conflict is
actually resolved in the Merge Editor): when present and jq is on PATH,
Diverged entries get annotated with the real recorded decision instead of
only the diff-size guess. Falls back silently to the existing heuristic
if jq is missing or no decision was recorded for a path -- no new hard
dependency on the core script.

Verified against the real pending kei range (89b3b42a4..7d508b834) in a
scratch clone: stderr capture produces real "patch does not apply"
diagnostics for the 11 hard-failures, and a synthetic decisions.jsonl
correctly annotates matching Diverged entries while leaving unmarked ones
on the plain diff-size hint.
Rojikku added 14 commits July 13, 2026 02:04
sync-kei.sh --status's Diverged bucket can't tell a genuinely unresolved
conflict apart from a file where the fork permanently carries its own
content on top of an already-correctly-merged upstream change (e.g.
index_pb2.py's hand-added isNovel field) -- its diff-size hint is only a
guess. This records the actual decision made at the moment a conflict is
resolved in VSCode (Merge Editor or manual edits -- indistinguishable via
the Git extension API, and it doesn't matter which), via
.git/kei-sync-decisions.jsonl, which the previous commit already wired
--status up to read.

Not published to the marketplace -- personal, local-only tooling,
installed via `npx @vscode/vsce package` + `code --install-extension`.
Checked in (with a README) so it isn't lost and isn't unusable if we or
anyone else wants it later, per the plan's addendum.

Verified with a mock-vscode Node harness driving the real extension.js
through: initial-snapshot (no false prompt), resolve-with-decision
(correct jsonl shape, status bar count), a dismissed prompt showing up as
untagged, finalize (summary content, clipboard/doc, backup-then-clear),
and the gate closing when the branch no longer matches kei-sync-*. Also
packaged and installed the real .vsix locally via `code
--install-extension` to confirm it loads without error.
Root cause of the isNovel/branding/libVersion regressions found in this
sync's review: when a file lands in the FAILED bucket because it's a
rename (old path deleted upstream, new path added elsewhere) AND our copy
had local fork customizations, the only artifact sync-kei.sh gave you was
a bare "delete N lines" patch for the old path -- nothing connecting what
the fork actually changed to the new upstream file. Reconciliation was
100% manual and memory-dependent, which is exactly the kind of thing that
silently drops content at scale (11 failed files) or under fatigue.

Fixes, verified against the real pending range and the actual current
kei-sync-7d508b834809 working tree:

- Both modes now print detected renames up front, using a 10% similarity
  threshold -- git's own default (50%) misses heavily-rewritten renames
  entirely, which are exactly the ones most likely to carry fork
  customizations. Confirmed empirically: PluginExtension.kt->
  ExtensionPlugin.kt (49% similar) and merge-repo.py->publish-repo.py (38%)
  were BOTH invisible at the default threshold.

- For a detected rename where the old path has local divergence, the
  script now saves just the fork's own edit to that file (old kei baseline
  vs. our current content) to
  .git/kei-sync-pending/<oldpath>.fork-customizations.diff -- a small,
  surgical checklist of what needs porting into the new file, generated
  automatically instead of requiring anyone to reconstruct it from memory.
  Also lists pure deletions (no matching replacement) separately.

- --status now additionally compares the current working tree (not just
  the pending range) against kei's target tree, catching files we're
  still carrying that don't exist upstream at all -- almost always the old
  half of a rename that never got deleted. Split into "orphaned" (existed
  in kei as of our last sync, so very likely a leftover) vs. "fork-original"
  (never existed upstream, probably intentional but flagged so that's a
  conscious call).

- New .kei-sync-ignore populated with the fork's actual permanent-only
  content (lib/, scripts/, sync bookkeeping files, a handful of
  novel-specific core utilities and repo policy files), discovered by
  running the new fork-original report and confirming each entry is
  legitimate, permanent fork content rather than something needing review.

Running --status against the real in-progress merge immediately surfaced
all 10 genuinely orphaned files (the exact ones this session's manual
review found by hand) with zero false positives, and the fork-customizations
diff for PluginExtension.kt was a clean ~15-line artifact showing exactly
the libVersion/common16 wiring that needs to be re-applied to ExtensionPlugin.kt.
report_renames_and_deletions() used "git diff --quiet LAST_SYNC -- old" to
decide whether a rename's old side had local customizations worth saving.
That check can't tell "modified" from "gone" -- for old files that had
already been cleanly auto-applied (deleted) earlier in the same run, it
produced a full "delete the whole file" diff mislabeled as "fork
customizations," which is actively misleading (nothing to port, but reads
like there is).

Added an explicit existence check first: gone means either auto-applied or
already handled by hand, nothing to diff, just confirm the replacement
file exists. Caught by re-running --status against the real in-progress
merge and noticing 8 of the 15 rename entries had suspiciously large
"fork-customizations" diffs (up to 108 lines) for files known to have
applied cleanly -- turned out to be exactly this.
…ation

Two real bugs found while using the script to actually resolve the pending
kei refactor tonight:

- sync-kei.sh always resolved TARGET from kei/main, the remote-tracking
  ref -- which moves every time anyone fetches, including this script's
  own `git fetch kei` step. Mid-session, kei/main advanced 5 commits
  under us with no way to have prevented it, silently changing what a
  --status run was even comparing against. Fixed: pull up to a local
  branch (default: kei, overridable with --branch <ref>) that's advanced
  explicitly and only on request (git branch -f kei kei/main), never as a
  side effect of fetching. Also fixes a genuine ref ambiguity: naming the
  pin branch the same as the remote ("kei") makes bare `git rev-parse kei`
  ambiguous between refs/heads/kei and the remote's own ref namespace;
  now resolved via refs/heads/<ref> explicitly.

- --status's RESOLVED/UNTOUCHED/DIVERGED classification used `git diff
  --quiet <rev> -- <path>`, which only compares a commit against the
  INDEX, not the raw working tree -- an untracked file (never `git add`ed,
  exactly what a hand-recreated rename target looks like right after a
  failed apply) reads as "doesn't exist" even when it's sitting on disk
  with the right content, silently misclassifying it as untouched instead
  of resolved. Replaced with a helper that compares the commit's blob
  directly against the working-tree file, correct regardless of tracking
  state. This alone moved ~18 files from the wrong bucket into "Resolved"
  once the real build-logic fixes landed.

Also: fixed a `set -e` footgun where the Diverged-bucket diff-size hint
used `grep -c`, which exits 1 (not an error) on a zero count -- inside a
variable-assignment command substitution that silently aborted the whole
script the moment a file had zero pure additions or zero pure removals,
with no error message. Fixed a regex-escaping bug generating harmless but
noisy "stray \ before /" warnings (escaping `/`, which isn't a regex
metacharacter). Added gradle/kei.versions.toml to .kei-sync-ignore (a file
kei has that this fork deliberately never carries) and __pycache__/ to
.gitignore.
kei restructured build-logic wholesale (package rename, PluginExtension.kt
-> ExtensionPlugin.kt split across io/github/keiyoushi/gradle/{api,internal,
tasks}/, new signing/jar pipeline). sync-kei.sh correctly flagged every
touched file as a rename with local customizations that needed manual
porting (see its "Renames detected" report) -- this is that porting,
verified file-by-file against each old file's actual fork-diff rather than
guessed from memory.

Restored:
- Dual libVersion support (Phase F): `keiyoushi.libVersion.convention("1.6")`
  and the compileOnly(libs.bundles.common vs common16) switch, now on
  ExtensionPlugin.kt and ThemePlugin.kt (the old PluginExtension.kt and
  PluginMultiSrc.kt both had it -- kei's own new files had neither).
  Constant.kt's VALID_LIB_VERSIONS extended to ["1.4", "1.6"] to match.
- The kei->ns catalog aliasing this fork relies on for Android SDK/Java
  version pins, since we don't carry a real kei.versions.toml (settings.gradle.kts
  registers "kei" against gradle/ns.versions.toml instead). Restored the
  `Project.ns` accessor and `Project.kei = ns` alias, and the explicit
  kei.versions.* -> ns.versions.* call-site swaps in AndroidBasePlugin.kt
  and configurations/Kotlin.kt matching the old files' own precedent.
- baseVersionCode/libVersion extension properties on Project, now backed
  by kei's renamed KeiyoushiThemeExtension (was KeiyoushiMultisrcExtension).
- isNovel: true on ExtensionMetadata, the JSON payload GenerateSourceInfoTask
  writes for publish-repo.py to read -- every extension in this repo is a
  novel by construction, and this is the field the whole isNovel pipeline
  (extensions-lib index.proto, this repo's publish script, the app) depends
  on further downstream.
- PluginExtensionLegacy.kt's imports, which still pointed at the old
  namespace entirely (it's fork-original, never touched by kei syncs, so
  nothing here would have caught this until the build broke): repointed to
  the new namespace, KeiyoushiThemeExtension, and a new
  GenerateLegacyKeepRulesTask. That last one is NOT the same as kei's own
  GenerateKeepRulesTask -- kei's version hardcodes the keep rule to the
  KSP-generated "ExtensionGenerated" class name, which is correct for the
  modern DSL but wrong for legacy extensions with hand-written, arbitrarily-
  named entry classes (the old GenerateKeepRulesTask took an explicit
  className for exactly this reason). Restored as a separate, legacy-only
  task rather than modifying kei's shared one.

Deleted the 10 old-namespace files these changes make dead: PluginAndroidBase.kt,
PluginExtension.kt, PluginMultiSrc.kt, and the old keiyoushi.gradle.{configurations,
extensions,tasks,utils} tree, once nothing referenced them anymore (verified via
`grep -rn '^import keiyoushi\.gradle\.'` returning empty outside these files).

Verified: no remaining old-namespace imports anywhere in build-logic, the proto/JSON
pipeline round-trips isNovel correctly end to end, and `scripts/sync-kei.sh --status`
reports every one of these files as either Resolved or a small, expected diff against
upstream -- no unexplained divergence remains.
kei folded create-repo.py + merge-repo.py into a single publish-repo.py and
switched its metadata source from parsing `aapt dump badging` output to
reading the JSON GenerateSourceInfoTask now emits at build time. The new
script was a clean copy of kei's own upstream version -- Keiyoushi branding,
their signing key, their CDN URLs, generic `tachiyomi-` naming, and (this is
the one that actually mattered) never sets isNovel on the extensions it
publishes, since the manifest-meta-data-tag mechanism that used to derive it
doesn't exist in the new pipeline at all.

- index.proto: added the missing `isNovel = 8000` field declaration (the fork's
  reserved 8000+ field block) -- the compiled index_pb2.py/.pyi already had it
  from an earlier session, hand-patched without updating the .proto source they're
  supposed to be generated from, so the two had drifted apart. Regenerated both
  via `protoc --python_out=. --pyi_out=.` per the file's own header instructions,
  confirmed byte-identical in the only intentional aspect (offsets) and verified
  with a live strict-JSON parse round-trip.
- publish-repo.py: name/badgeLabel/signingKey/contact -> NovelSourcery's (from
  the old merge-repo.py), LANGUAGE_REGEX and the apk/jar/icon cleanup globs
  tachiyomi- -> tsundoku- and eu.kanade.tachiyomi.extension. ->
  eu.kanade.tachiyomi.novelextension. (matching the fork's actual APK naming
  convention), and `isNovel=info.get("isNovel", True)` threaded through to
  index_pb2.Extension -- paired with the ExtensionPlugin.kt commit's
  `isNovel: true` on the JSON payload this reads.
  APK/JAR/ICON_BASE_URL kept on the fork's existing raw.githubusercontent.com
  pattern rather than adopting kei's new jsdelivr CDN switch -- a deliberate,
  conservative call (only found evidence jsdelivr is intentionally used for
  index.min.json specifically, via commit-repo.sh's purge call, not for
  APK/icon downloads), flagging in case that's wrong and jsdelivr should
  apply here too.

Deleted create-repo.py, merge-repo.py, commit-repo.sh, and always_build.json
now that publish-repo.py fully replaces them and nothing else references them.
The mechanically clean/already-resolved part of the kei build-logic
refactor sync -- fork branding on the CI workflows and issue templates
(novelsourcery/extensions-source, NS org references), CONTRIBUTING.md
brought up to date with kei's rewritten version, the KSP SourceProcessor
mirrors/concrete-source-class support and MirrorPreferences/strings.json
updates (pure upstream, no fork logic needed), the new
ext-bootstrap.py, and gradle/libs.versions.toml's apksig catalog entry
(needed by the new build-logic signing pipeline) plus the fork's existing
tachiyomi-lib-16/common16 entries.

No fork-specific customization needed in any of these -- verified each
against kei's target commit directly where it mattered (SourceProcessor.kt
and MirrorPreferences.kt are byte-identical to upstream).
Matches kei's own pattern: apk/jar/icon URLs (versioned filenames, so
no cache-purge needed) move to jsdelivr's free GitHub CDN; the
top-level index.min.json URL users paste into the app stays on
raw.githubusercontent.com, same as kei. The purge curls already
present in build_push.yml were still pointed at keiyoushi/extensions
from the merge -- fixed to novelsourcery/extensions.
…conflicts

Two independent sources of needless manual conflict resolution, found while
re-running the script on an already-fully-resolved sync:

- Permanent local overrides (repo name, product branding, signing key, CDN
  URLs) sit on lines upstream routinely touches for its own reasons (e.g. its
  own version-bump commits), so every such commit re-triggered a conflict
  whose resolution is always "keep ours." .kei-sync-rewrite normalizes known
  permanent-override regions to a stable placeholder on both the base and
  target blobs before diffing, so a routine upstream touch-up near one of
  these never generates a hunk in the first place.

- git apply --3way's diff3 algorithm can lose alignment around repeated
  boilerplate (e.g. CONTRIBUTING.md's near-duplicate examples) and flag a
  block as conflicting even when both sides are byte-identical.
  collapse_identical_conflicts() resolves those silently before they ever
  reach a human.

Building the rewrite table required writing the rewritten blobs as real git
objects (git hash-object -w) so --3way has something to look up by SHA --
a bare unified diff with no `index` line falls back to strict apply, which
either succeeds outright or hard-fails with no usable markers at all. Also
fixed two bugs hit along the way: a pipefail trap where `diff -u | tail`
killed the whole script the moment two rewritten blobs actually differed,
and files that should resolve as a no-op (rewrite fully accounted for the
only difference, or the file was already deleted locally same as upstream)
landing in hard-failure instead of being recognized as already resolved.

Verified in a disposable scratch clone against the real pending range:
9 conflicted + 37 hard-failed dropped to 0 hard-failed + 13 genuine
conflicts (all matching the already-known substantive fork customizations).
Two branding gaps missed in the earlier publish-repo.py pass: the Discord
invite still pointed at kei's server (3FbCpdKbdY instead of our own
JG2K2jTjd6), and one repository-JSON link used lowercase "novelsourcery"
while every other github.com/... link in the file uses the real org casing
"NovelSourcery".

Found while testing sync-kei.sh's new rewrite table -- both showed up as
conflicts that looked spurious (case-only, or Discord IDs that are easy to
eyeball as "probably the same") but were actually flagging real, if minor,
content gaps.
…r context

The org name needs different casing in different contexts -- "NovelSourcery"
(the real casing) in plain github.com/... links, but lowercase
"novelsourcery" in raw.githubusercontent.com/jsdelivr.net/github.io URLs and
the github.repository == '...' YAML comparison (all confirmed intentional,
not casing bugs). An unscoped rule can only pick one output casing, so it
was guaranteed to be wrong in whichever context it didn't target -- which is
exactly what happened: the previous all-lowercase version was wrong for
plain links (and produced a spurious-looking case-only conflict against
CONTRIBUTING.md's correct capitalized content), and the fix-in-place
attempt at capitalizing it broke the CDN/YAML contexts instead. Rules are
now scoped by domain prefix or exact surrounding text, same technique
already used for the CDN-specific rules.

Matching is now case-insensitive (sed's I flag) so kei's own text doesn't
need to match one specific casing to be caught by these rules in the first
place.

Verified via a full case-insensitive-identical sweep across every
conflicted file in a scratch clone of the real pending range: zero ghost
conflicts remain anywhere, not just in the specific file this was found in.
… failure)

CI failed on :lib-multisrc:readwn (and every other lib-multisrc module --
all 75 build.gradle.kts files use the identical two-line import) with
"Unresolved reference 'keiyoushi'": they still imported baseVersionCode/
libVersion from keiyoushi.gradle.extensions, a package deleted when the
orphaned old-namespace build-logic files were removed earlier this branch.

lib-multisrc/ is deliberately outside sync-kei.sh's scope (it's extension
code, not build-logic), so it was never touched by any of that cleanup --
but it still references symbols that moved. baseVersionCode/libVersion are
fork-only properties (Phase F work, never existed in kei's upstream), now
declared in io.github.keiyoushi.gradle.internal.extensions.Project.kt;
updated all 75 imports to match, same fix already applied to
PluginExtensionLegacy.kt for the same reason.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant