MIB Platform · Engineering
Sprint plan
Where the platform stands, who does what in the ten days from 13 October, what each task waits for, and the gate each stage has to pass. Day 1 is done and checked against GitHub.
The sprint, 13 to 22 October
Ten calendar days, two people. Every line on a card links to its issue or PR; every chip is a link (a task ID jumps to that card).
mib-regulations, mib-identity, mib-ai, mib-rag, mib-retrieval, mib-infra, mib-shared, .github
mib-web (portal and admin), mib-lms, mib-ingestion
How the front end and mib-regulations work together
The front end goes first and writes the API contract; the backend adapts to it. The mock in mib-web already has the pages and their data shapes (src/lib/types.ts, src/lib/mock-data.ts), so each contract starts from those. The rules every contract follows (naming, errors, pagination, access, examples), the shared components, the template and the lint are in docs/contracts/README.md (mib-regulations#35, merged 13 Oct).
Write the contract
One OpenAPI file per feature, as a PR to mib-regulations under docs/contracts/, copied from _template.yaml.
Review the same day
CI lints it against the conventions. Disagreements are settled in the PR. Merged means frozen; a later change is a new PR both sides approve.
Implement to match
Existing endpoints are adapted to the contract. Done only when the contract test (mib-regulations#44) is green.
Sync
Raditya swaps the mock for the live API on dev; both check every page state. A mismatch goes back as a contract PR, never a quiet change on one side.
Each contract issue (mib-regulations#36 to #39) tracks all three steps (contract, backend, sync) as a checklist and closes at sync, not when the YAML merges; contract PRs say "Part of", not "Closes". The AI chat's shape (C5) is backend-defined: Afif publishes it on mib-ai#19 on Day 6.
Contracts
| ID | Issue | Covers | 1 · Contract | 2 · Backend | 3 · Sync | Status |
|---|---|---|---|---|---|---|
| C1 | mib-regulations#36 | Peraturan and putusan: list, search, detail with connections, visitor preview vs signed-in text, download | R1 · Day 1 | A3, A4, A9 | R4, R5, R10 | frozen 1.0.1 (mib-regulations#51, 13 Oct) |
| C2 | mib-regulations#37 | Kurs KMK and Tarif Bunga KMK, Excel export | R2 · Day 2 | A5 | R6 | not started |
| C3 | mib-regulations#38 | Tax treaties: list, search, preview, download | R2 · Day 2 | A5 | R6 | not started |
| C4 | mib-regulations#39 | Visitor search teaser, chat quota states | R2 · Day 2 | S1 | R9 | not started |
| C5 | mib-ai#19 | AI chat request and response (backend-defined) | A6 · Day 6 | A7, A8 | R8, R9 | not started |
The board
Critical path
The AI chat is the UAC gate (the contract's acceptance test), so its chain decides the sprint. The regulation pages are the second chain. Everything else fits around them.
Not in these ten days
Left out: capacity or a client decision
- Payments:
mib-identity#6,#7,#8,#36,#37,mib-web#23,#24. The gateway is decided (Midtrans, 13 Oct), so nothing technical blocks them now. They are left out for capacity; the Premium price is still a mock (mib-identity#14). - Compile & Compare:
mib-web#22,mib-regulations#33. The gate is decided (Premium, unlimited); what it does is not. - Analytics:
mib-web#26. Where the events go is undecided. - LMS:
mib-lms#1is phase two. - Ingestion questions:
mib-ingestion#41(Peninjauan Kembali),#58(periodic full re-check).
Backend not scheduled yet
- Admin screens
mib-web#9to#15needmib-identity#10,#11,#29,mib-regulations#7andmib-rag#19.mib-web#8(admin login) is in R7 because its backend exists. - Daily quota chain is stretch S1; first to start if a day frees up.
- Production infra:
mib-infra#1,#3,#6,#7,#9and the alerting work (#43,#44). - Corrected text
mib-ingestion#21(ingestion part merged) and the deferred RAG design (mib-rag#34).
Day 1, 13 October: done and verified
Checked against GitHub on 13 Oct: each issue's acceptance criteria against the PR that closed it, the tests in the diff, and CI on main.
| Item | Result | Evidence |
|---|---|---|
A1 · mib-regulations#41 CI install | done | PR #45 (also carried in #35). sqlalchemy[asyncio] installs greenlet; main CI green. |
mib-regulations#24 JSON-LD injection | done | PR #47 escapes <, > and &. Test: a </script> payload cannot close the block and round-trips. |
mib-regulations#25 page → 500 | done, one test skipped | PR #48 caps page at 100,000 on both routes; the schema test runs in CI. The end-to-end "huge page → 422" test needs a database, and regs CI has none, so it is skipped there. |
mib-identity#34 deleted user | done | PR #38: 404 for an unknown user, with tests. |
mib-identity#35 login burst OOM | 3 of 4 | PR #39: concurrency test, one shared limiter at every call site, config plus "an old hash still verifies and is upgraded". Fourth criterion: the load test records a login burst and peak memory. That is mib-infra#7, which now carries it. |
Reviews of mib-regulations#29, mib-rag#37, mib-regulations#30 | done | mib-regulations#29 and mib-rag#37 merged. mib-regulations#30 was closed unmerged and replaced by #49 (merged). mib-regulations#28, #42, #43 and mib-rag#36 are closed. |
R1 · mib-regulations#36 step 1, contract C1 | done | PR #46 merged: frozen at 1.0.0, "Part of #36", contracts CI green, same-day review with fixes pushed. #36 correctly stays open until the sync. |
Open after Day 1
- C1 1.0.1 (
mib-regulations#51, merged 13 Oct). Two vocabularies in the frozen contract did not match what the tables store, so every real row would have failed the contract test:legal_status(nowin_force | revoked | unknown, amended is theis_amendedflag, andrevokedwins overis_amended) andconnections[].kind(nowrevokes | revoked_by | amends | amended_by | partly_revokes | partly_revoked_by). It also adds the 422 on the two detail endpoints. A response enum value that is added, removed or renamed is now a breaking change. Raditya's approval was not recorded on the PR before the merge: tell him, because R4's types must follow these codes. - What a visitor with no token gets from the download endpoints. The contract defines a valid token (stamped copy) and an invalid token (401) but not an absent one. Recommended:
x-access: signed_in, so a visitor gets 401 and the page asks them to log in, until the client answers Q8 (.github#7). Needs a decision before A9. - regs CI has no database, so every
requires_dbtest is skipped there, and the contract test needs real rows.mib-regulations#44now starts by adding a PostgreSQL service and the migrate steps, as in mib-identity's CI. - Routes outside C1:
/search,/search/semanticand/regulations/{id}/htmlare in no contract, so the route check in#44would flag them. The contract'sqreplaces/search;/html(the SEO page) needs an exemption or its own entry. - What C1 costs on the backend: the list gains
q,number_display,is_amended,is_partly_revoked, a year range, repeatabletypeandsort=relevance; detail returns JSON withpreview_html,body_html,teaser,connectionsandneeds_ocr(today it is metadata only and the text is on/html); download (#5) and the four/decisionsroutes (#26) are new.
Merged on Day 1, and what is still open
| PR | State | What it did |
|---|---|---|
mib-regulations#35, #45 | merged | Contract conventions, shared components, template and lint; the CI install fix. |
mib-regulations#29, #49; mib-rag#37 | merged | Rate lookup, exclude_from_rag, corrected text and served_content_hash; change_amount; RAG skips the rate decrees and re-embeds a corrected document once. |
mib-regulations#47, #48; mib-identity#38, #39 | merged | The four bug fixes above. |
mib-regulations#46 | merged | Contract C1, frozen at 1.0.0. |
mib-regulations#51 | merged | C1 1.0.1 (above). |
mib-infra#73 | open | Traefik gets 256 MiB on the dev host. |
.github#25 | open | PRD putusan columns for SETPP. |
mib-infra#65 | closed unmerged | Maintenance windows restart only what they stopped. |
Stages
Each stage names its deliverables and the gate that closes it. A stage is not finished because the work was done; it is finished because the gate passes. Phases end on gates, not on dates.
| Stage | Name | State | Where it stands |
|---|---|---|---|
| 0.1 | Foundation hardening | closed | All five gaps closed and asserted in CI. |
| 0.2 | Infrastructure and survivability | backup left | Backup automation and a rehearsed, timed restore (mib-infra#6) are the last Phase 0 gate. Blocks nothing, gates release. |
| 0.3 | Identity and single sign-on | closed | All four gate clauses. Hardened 13 Oct (login burst, unknown user). |
| D | Design | complete | Figma finished 13 Oct; client feedback is minor and tracked in mib-web#37. Screens are sliced as a client mock (mib-web#27). The published library (mib-web#6) and per-screen sign-off are open. |
| 1 | Data | critical path | Every stage merged and the cycle wired; first dev full run done 11 Oct. Needs real OSS keys and egress for a first real corpus. Not deployable alone. |
| 2 | Regulations Portal | contract-first | C1 frozen; the backend adapts to it, with the contract test as its gate. The pages are a mock until the sync. |
| 3 | AI layer | chat not built | The pipeline is built; the chat, quota and shared summaries are not. Sprint chain A2 to A10. |
| 4 | Monetisation | not started | Gateway decided 13 Oct: Midtrans. Adapter, checkout and cancel not built; price still a mock. Out of this sprint. |
| 6 | Admin dashboard | auth only | Authentication, roles and audit log exist; the surface does not. |
| 5 | LMS | phase two | Scaffold only; deliberately unspecified. |
How the work is ordered
Five rules decide the sequence. Where they conflict, the earlier one wins.
- Fix the foundation first. Each early gap was load-bearing for everything after it.
- Identity before anything that authenticates. Deferring it would mean rebuilding authentication in phase two, the outcome the single-account requirement exists to prevent.
- Data before the portal. A search interface needs a corpus to search.
- Design runs in parallel. It gates the front end only, never infrastructure or ingestion.
- Phases end on gates, not on dates.
Stage cards
Foundation hardening
closedFive gaps found by running the system on 20 Aug, not by reading it. All closed and merged, each with a CI assertion that exercises the real thing, because two of them (the gateway and the boundary) had broken while every static check passed.
/readyhung ~130 s against an unreachable database; probes now run off the event loop under a deadline and return 503 in 2 s while/healthkeeps answering (mib-shared#3).- Nothing routed through the gateway: Traefik could not read the Docker socket, and CLI flags in the dev override discarded the static config, so the entrypoint every label routes on did not exist. CI now routes a real request (
mib-infra#12). - The boundary was not enforced: all six services connected as the database owner. Each now uses its own least-privilege role with passwords from the environment; CI asserts the refusal against a real cluster (
mib-infra#4). mib-sharedwas consumed by nothing: now adopted everywhere, duplicatedlogging.pydeleted; adoption surfaced a swallowed traceback and a missingtrace_id(mib-shared#4).- No tracing: W3C propagation, the error envelope with a quotable reference, and a client with a mandatory timeout, bounded retry and defined fallback (
mib-shared#2).
- Gate
- A request through Traefik returns 200 from a service.
/readyreturns 503 in about two seconds with the database down while/healthkeeps answering. Traefik initiates the trace and every service carriestrace_idon every log line. A service attempting a cross-schema read is refused with a permission error. Every clause is asserted in CI on each change.
Infrastructure and survivability
backup and restore leftOn a single self-managed host the database holds the corpus, the vector index and every user and payment record, with nothing managed behind it, so two of this stage's gates are release gates.
- Build
- Egress for Qwen, OSS, the payment gateway (Midtrans) and the crawl sources on the host firewall. Inbound restricted to Cloudflare ranges plus administrative SSH. Host provisioned from version-controlled configuration. A decision on secret injection. The memory budget written down, including the resident vector index. Continuous WAL archiving to OSS with failure alerting. Development, staging and production, with production never sharing a host.
- Gate
- A full restore rehearsed on a clean host, with the elapsed time recorded against the four-hour target (
NFR-38,NFR-39): measured, not asserted. Backup and archive failures both raise an alert. The load-test harness (NFR-35,mib-infra#7) sustains the target rate against the real topology, including a login burst and identity's peak memory.
- Built Compose projects for five environments, now one directory per service with env overlays (
mib-infra#74, 13 Oct); Traefik routing; Postgres with pgvector; least-privilege roles; observability; CI hygiene checks that fail the build if anything but Traefik publishes a host port. - 25 Aug Performance targets adopted by assumption, since the client has no figures: ≤1 s lexical search and filter (
NFR-01), ≤2 s detail (NFR-02), ≤15 s AI summary (NFR-03), inmib-infra/docs/performance-targets.md(mib-infra#10). Validating them waits on the load harness and a real corpus. - 26 Aug Rate limits adopted by assumption (
mib-infra#11, PR #35): API per IP at the edge, 20 req/s average, burst 40, through a sharedapi-ratelimitmiddleware; PDF downloads per account 50/day, burst 5/minute, handed tomib-regulationsas config (mib-regulations#6). Per-user limiting is unimplemented anywhere. - 26 Aug Maintenance mode (
mib-infra#21, PR #33): a Traefik file-provider catch-all that outranks every label router,scripts/maintenance.sh on/off, amib_maintenance_activeflag and alert suppression. The orchestratorpipeline-cycle.sh(mib-infra#36, PR #37) ends the window when ingestion finishes and refuses to start while embedding is still draining. - 29 Aug Observability's topology decided (
mib-infra#20, PR #40): its own dedicated instance, never co-located with the application, in dev and production alike, because a monitor sharing the host it watches cannot report that host dying (NFR-18). Dev's instance (2 vCPU, 2 GiB, limits scaled to ~1.78 GB) is not provisioned yet and the cross-host metrics wiring is not built. - 7 Oct Ingestion's deployment wiring (
mib-infra#52, PR #57): OSS and Qwen settings, optional, with the LLM flags off;pipeline-cycle.shrunscleanupthencorrect, reports exit 75 as retry later and 78 as misconfigured. Peak memory measured: 394 MiB on PMK 60/2026 (#53, PR #59). JDIH egress recorded (production/egress-rules.tsv,scripts/check-egress.sh, PR #58), not yet in the live security group. - 10 Oct No service holds DDL (
mib-infra#16, PR #67): a migrator role per schema owner owns the tables and alone runs migrations (scripts/migrate.sh); runtime roles keep data access. Existing hosts are converted once by provisioning again;smoke-db-roles.shasserts the split.deploy.shmigrates with the new image first and never starts the ingestion one-shot (PR #66). Dev ingestion limit 768 MiB (#62). - 12 Oct Region and models (PRD v0.32): production is most likely a Contabo VPS in Singapore; OSS and Model Studio are in
ap-southeast-1. This reverses AD-4's Jakarta: personal data leaves Indonesia (a UU PDP point for the client,NFR-15), egress control moves to the host firewall, and the Alibaba runbooks need rewriting. The existing dev host stays until moved.
#6), the load harness (#7), Telegram alerts (#43, blocked by #44, which creates the bot), mounting MAINTENANCE_DIR into mib-rag (#51, p1), the real visitor address behind Cloudflare (#46, p1), egress to fiskal.kemenkeu.go.id (#55) and SETPP_SKIP_DOWNLOAD_IDS (#56). Production sizing is unresolved and is the client's call: the application alone requests 8,192 MB of limits before the OS or a vector index nobody can size until Phase 3, and the observability instance needs its own budget line.Identity and single sign-on
closedThe largest Phase 0 item and the one everything else waited on. One account per person, one authority, verified locally by every other service. Closed on every gate clause on 23 Aug.
- Build
- Registration and email verification, password hashing, asymmetric token signing with a published key, local verification with no network hop per request. Opaque refresh tokens held server-side with immediate revocation, single logout, a session list a user can revoke from. Capabilities as a token claim, resolved from plan version plus institutional membership. A separate token audience for administrators. Authenticated service-to-service calls. 13 Oct: password hashing shares one concurrency limiter so a login burst cannot OOM identity (
mib-identity#35, PR #39), and the entitlements read answers 404 for an unknown user (#34, PR #38). - Gate
- All four clauses pass. (1) A token issued by identity verifies in
mib-regulationsandmib-aiwithout calling identity, including refusal of another key, anHS256token using the public key as its secret, a wrong audience and a wrong issuer. (2) Revoking a session ends it at the next refresh; the access-token TTL is the worst-case lag, 15 minutes plus 30 seconds of leeway. (3) A capability change takes effect within one token lifetime, with no restart and no re-login. (4) An administrator token is refused by public endpoints and the reverse. - Found
- Four defects that building it surfaced and reading would not: a
RecursionErroron the only signing-key path production uses (every test ran on the ephemeral dev key); a 500 on login from a client whose address is not an IP (last_seen_ipisinet);mib-aishipping an Alembic history it could not run (alembicwas never a dependency); andscript.py.makomissing its upgrade placeholder in all four migrating repos, so autogenerate wrotepasswhile reporting success. - Property
- An administrator is a row in
identity.userswith a role, so "not obtainable by escalating a public token" is not structural. It holds because the audience is recorded on the session at login rather than derived from the role at refresh. Writing the role column on an account already logged in as a customer cannot promote that session. Two privilege levels, neither able to alter its own role; the last active super-administrator cannot be removed by anyone; super-administrators come only from a seeding script run on the host; every administrator write records actor, before and after in the same transaction. - Not built
- No Redis layer in front of
sso_sessions: revocation must be immediate, so PostgreSQL stays authoritative. PPN rate and manual quota grants were split tomib-identity#29; both depend on plans and subscriptions.
Design
completeBlocks only the front end. Design system first, then screens composed from it, including the states that generate the most rework when left unspecified.
- Build
- Design system and brand identity as the single source of truth, tokens exported to code rather than transcribed. The phase-one journeys end to end at both breakpoints. Empty, loading, error-with-reference, quota-exhausted, unentitled and no-readable-text states designed. A print-suitable logo asset for the download watermark.
- Gate
- Client sign-off recorded per screen, with approved screens versioned so a later change is visibly a change. After sign-off, changes are change requests against Phase 2, not free revisions.
- 9 Oct Every approved screen sliced as a client mock on mock data (
mib-web#27, closes#1and#5, taggedv0.2.0); colour, type and page width from tokens; tier states per the client's entitlement matrix, previewed with a runtimeDEMO_MODEswitcher; registration and login working against identity. - 10 Oct Cleaned for the client's review (
mib-web#31,v0.2.2): no developer text on screen, JPY per 100 yen on both calculators, one shared loader on every user-started API call. - 13 Oct Figma complete. Client feedback is minor after earlier rounds and does not block the build; each item goes into
mib-web#37, and one that changes an API goes through its contract PR.
mib-web#6), and client sign-off per screen. Watch: a Figma-first process with an engaged client can iterate indefinitely, because moving a rectangle costs nothing and moving a shipped component costs a sprint; the sign-off gate is the only control on this.Data
stages merged, real run pendingFill the corpus. Re-specified 30 Sep 2026 (PRD v0.12, extended in v0.13): regulations come from JDIH Kemenkeu as full PDFs from 2015 (the older 54% is HTML fulltext plus companion files), and tax-court rulings from SETPP, the listed risalah only (about 34,000, 2011 to mid-2019). The previous source is retired. Text extracted from the file is the only input to search and chunking, with one decided exception: LLM-corrected text for scans whose built-in text layer is faulty (mib-ingestion#21).
- Build
- One adapter per source, so a source change cannot break the others. The source file stored unmodified (the per-user watermark is applied at download), raw text and display HTML to OSS under deterministic keys, the text addressed by its own content hash. Text extraction that survives page headers, footers, page numbers and hyphenation so a citation such as Pasal 1 angka 5 stays intact. Metadata and typed relations from JDIH's detail pages. A PDF with no text layer is flagged
needs_ocrand alerted; OCR itself is out of scope. Optional LLM HTML cleanup as its own resumable one-shot, and LLM correction of faulty scan text behind a typo-only gate. Kurs KMK and Tarif Sanksi Bunga KMK as plain lookups. A weekly sweep, with change detection a non-blocker. - Gate
- The JDIH corpus (about 8,700 regulations) and the SETPP risalah (about 34,000) ingested and searchable. Re-running creates no duplicates and no re-embeds, and text extracted twice from the same file is identical. Extracted text chunks with zero crashes on a JDIH and a SETPP sample. A revoked regulation's status changes without its own listing moving. A document whose structure is not recognised is counted and alerted, never processed silently. Metadata and relationship accuracy sampled manually and recorded.
Timeline
- 5 Oct
#18(PR #26):full_textdropped; GIN trigram indexes (pg_trgm) on title and number; nosearch_vector. The owner role must installpg_trgmbefore the next deploy.#1(PR #27): the JDIH adapter discovers through search, feed and JDIHN, reads detail pages, downloads PDFs and pre-2015 companions, resumes after a restart.#2(PR #28): English schema and metadata mapping.#3(PR #29): text extraction, artifacts,needs_ocrper page with one grouped alert per run. - 6 Oct
#4(PR #30): upsert bysource_ref, which for JDIH isproduk_hukum_id(OQ-38; the slug is its own column); natural key (type, number, year, issuing unit); display HTML sanitised by annh3allow-list.#6(PR #31) change detection;#5(PR #32) the sweep refuses with exit 75 while embedding drains, scheduled Saturday 00:00 WIB. PRs #33 to #38: the changed-provisions parser (#25; a partial list isunreadable, neverparsed), a liverobots.txtguard (#10), Kurs/Tarif rows (#22), the HTML cleanup one-shot (#19,#20) and the text-correction one-shot (#21); both exit 0, 75 or 78. - 7 Oct
#39(PR #43):run_onceruns the whole cycle, then re-reads the detail page of any stored document a new one revokes or amends (R-5).#16(PR #44): the SETPP adapter, one request per second intoputusan; an id that always fails (37402) is skipped by setting,#60(PR #61).#7(PR #45): a parse-volume alert against the median of the previous 8 runs.#49(PR #53):regulation_relationsis written at last, with reciprocal rows and owner tracking.#48(PR #52): issuing unit read from current KMK numbers.#54(PR #57): Kurschange_amountfrom fiskal.#55(PR #56) and#46(PR #62): an image-stamped promulgation date, and compound change-and-delete items. - 8–9 Oct
#66(PR #67,provisions/5): annex, Penjelasan, structure and diganti leads; PMK 71/2026 and 63/2026 parse.#70(PR #71,provisions/6): each run re-parses stored documents left by an older parser. Lampiran kept in the text (#68, PRs #69 and #72,jdih-extract/6, PRD v0.29): blank forms and pages past 200 per segment are left out, each with a note; PMK 7/2026 (2,003 pages) builds in about 200 MiB.mib-ragembeds annex pieces taggedsection = lampiran(decided 9 Oct,mib-rag#1). - 9–10 Oct Two dev OOMs.
#73(PR #74,v0.3.1): an annex header that is an image; memory returned between documents; a source over 100 MB is stored unextracted.#75(PR #76,v0.3.2,mib-shared v0.10.0): a 451 MB download held in memory twice over; downloads and uploads now go through disk and that file peaks at 220 MiB. - 10 Oct
#77(PR #78,v0.4.0): parserprovisions/7reads leadsprovisions/6left unreadable (2,456 of 8,505, 65% from HTML layout tables);jdih-extract/8reads layout tables as text; aCONVERTER_VERSIONbump now re-extracts stored documents from the source kept in OSS (reextract_batch, 200 a run, [TBD]).#80,#81(v0.4.1,mib-shared v0.11.0): a re-extracted putusan keeps one text row, and superseded OSS objects are deleted once the replacing write commits; objects left earlier needpython -m mib_ingestion.jobs.pruneonce. - 11 Oct First dev full run (v0.4.0 then v0.4.1, about 8 h): 8,402 regulations and 33,997 putusan re-extracted; unreadable provision lists down from 2,456 to 1,277; 0 re-extraction or OSS-delete errors; 322 item failures.
#83fixed three causes (TAP MPR out of scope, shared slug0rebuilt from OSS, lone surrogates);mib-infra#69skips 115 SETPP ids that always answer 500;#70sets both catch-up batches to 50000. Left: 148 JDIH files missing upstream, 31 natural-key collisions to review, 5 records without title or number. TC-10 (memory) still unmeasured. - 12 Oct
#84(PR #85,jdih-extract/9): no table row starts with|; a heading cell gets its own line, a list marker joins its text, data tables keep their columns. Every HTML document gets a newcontent_hashon the rebuild (~8 h on dev), so the first full embedding run (mib-rag#6) goes after it.#86(PR #87,provisions/8): a coarse article or annex reading that returns a complete set of units or refuses, marked inprovision_parse_reason; the next dev run re-parses every stored list (TC-33).
.env; apply the role split on dev first (mib-infra#67) then deploy with scripts/deploy.sh; pg_trgm installed by the owner role; egress for fiskal.kemenkeu.go.id and SETPP_SKIP_DOWNLOAD_IDS; run mib-ingestion#42 TC-10 at the 768 MiB limit and time it to size reextract_batch and provision_reparse_batch. A first full SETPP run is about 34,000 downloads at one per second, roughly 13 hours. Re-extraction gives every changed document a new content_hash, so let it finish before the first embedding run.content_hash; a status change to an older document only from the relations of a newer one. The sweep is not a full re-read; a periodic full re-check is a client question (mib-ingestion#58). Say "one week" to the client.mib-ingestion#10, closed 6 Oct) found that JDIH's terms require Biro Hukum Kemenkeu's permission for commercial use; decided 6 Oct (afifhrz) that as openly published public data MIB uses it without seeking permission. SETPP has no terms page and no robots.txt; JDIH's is read live every run. SETPP's unlisted full putusan are not taken.Regulations Portal
contract-firstThe first thing a user sees. Two items carry disproportionate risk: the visitor preview, planned as the primary organic discovery channel (whether search engines may list it at all has been with the client since 2 Oct, OQ-35), and watermarked PDF delivery, which puts file-sized work on the request path.
- Build
- Authentication and profile screens, keyword search with filters, the list view, and the HTML detail page with preserved structure and a view watermark. The connections graph, with unresolved references shown as text rather than hidden. Visitor tiering, server-rendered and (unless OQ-35 decides otherwise) indexable, with structured metadata. Watermarked PDF streaming under a concurrency cap, with a download audit record and a per-account rate limit.
- Gate
- Search and detail latency targets met at the 95th percentile. The withheld portion of a visitor preview is genuinely not delivered to the client, not merely hidden. Every download is recorded. Stamping throughput and peak memory per document measured under load, and the threshold at which the design reverts written down before launch.
- 27 Aug Started ahead of ingestion, querying whatever rows exist: list, detail and keyword search (
#1, PR #19); semantic search gated byregulations.search.semantic(#2, PR #20, withmib-retrieval#10); the HTML view withschema.org/Legislationdata, a visitor text truncated by an HTML-aware truncator topreview_char_limitcharacters before any markup is built (#3, PR #21). That PR forced the search and internal text endpoints offfull_text. - 2 Oct (PRD v0.14) The semantic-search mode on the list is switched off (no client agreement; no plan grants the capability); search by meaning is part of the AI chat. Every fallback PDF is rendered before go-live (
#5). - 5 Oct Keyword search narrowed to title and number (trigram indexes);
#27re-scoped. - 8 Oct Client's feature list: tax treaties are public, watermarked, searched by country and title, not in the AI chat (
#31,mib-web#19); Kurs and Tarif get public pages with Excel export (#32,mib-web#21); Premium gates Compile & Compare (#33,mib-web#22), which reverses AD-8. - 13 Oct
#28(PR #29): the read model is the English schema, search matches title, number andnumber_display(a%or_is literal), so live search works; ranking and the exact-number lookup stay in#27andrelevancestill sorts newest first. The same PR serves the corrected text with its own hash, the Kurs/Tarif lookup andexclude_from_rag.#24(PR #47) and#25(PR #48) fixed. - 13 Oct Contract-first. The public API is OpenAPI written by
mib-webfrom its pages, indocs/contracts, and the backend adapts. C1 is frozen at 1.0.0 (PR #46) and corrected in 1.0.1 (#51). It records partial revocation, a barenumberbesidenumber_display, optional access on downloads (the watermark and the audit row need the caller), and only the putusan fields the table stores (case number, abstract, year, tax-type code, risalah date; no title, outcome or stage; no peninjauan-kembali list).
FR-REG-20's preview, decided with C4 (#39); what a visitor with no token gets from a download; caching the regulation pages waits on OQ-35.AI layer
chat not builtThe whole paywall rests here: with the corpus free to any registered account, AI quality and unit cost are the commercial proposition. And the AI part has its own contractual acceptance test.
mib-ai#19). Pass mark: an average of at least 3.8 of 5, no factual-accuracy score of 1 or 2, at most 15 seconds on average; five test cases, each run three times. Our own repeatable run is mib-ai#20. NG-2 is reversed.- Build
- Chunking that respects document structure before falling back to length. The embedding state machine, driven by one staleness predicate covering new, changed and pipeline-migrated regulations, with rows claimed so concurrent workers cannot collide. The backfill, costed and timed before it runs; it advances only during maintenance windows, so its finish depends on how many windows the client accepts as much as on throughput. Retrieval, two-stage ranking and caching. The chat: one sorting call then fixed steps, exact lookup for a named document, facts (status, dates, relations, changed provisions) filled in by code from the database, every number checked before the AI's text is shown, the whole answer at once with no streaming. Summaries stored once per document version and shared. Quota per answered message: 2 a day for visitors and Gratis, 50 for Premium (the client's feature list, 8 Oct; replaces the rolling window and the lifetime grant,
mib-ai#2). - Gate
- The vector index type chosen by benchmark, recording resident index size and not only recall and latency. Quota correct at the boundaries, including the daily reset (00:00 WIB) and a guest's count carrying to a new account for the rest of the day. A Qwen outage degrades to lexical search and consumes no quota. Our own run of the UAC passes before the client's UAT (
mib-ai#20).
- Built Started ahead of Stage 1 on purpose: chunking needs text, not a database, and was measured on 54 scraped documents; the 296-document sample then found a crash (
mib-rag#23). Structure-aware chunking and the Qwen embedding client; the claiming machinery; the worker loop (mib-rag#27); retrieval (mib-retrieval#1);POST /api/v1/ai/summary(mib-ai#1); superseded-regulation gating (mib-ai#9). None of it has run against a real corpus. - 24–26 Aug
diubahrefused new summaries anddicabutwarned instead; revised 26 Aug so a revoked regulation generates with a warning (the text itself is not in dispute). Reversed 30 Sep: an amended regulation also gets a prominent warning, not a refusal (mib-ai#17, not built, so the code still refuses). A regulation's relations are whole-document only (FR-IN-03), so the refusal was regulation-wide, not scoped to a section. - 2 Oct Summaries stored once per document version and shared (
mib-ai#3); putusan summaries in scope (mib-ai#18); TC-RAG-01's changed-provisions list is read by code at ingestion (mib-ingestion#25), never written by the AI; putusan vectors at half precision (mib-rag#35); a retrieval request names exactly one corpus (FR-RS-18). - 13 Oct
mib-rag#36(PR #37):exclude_from_ragis carried likeneeds_ocr; the sync drops an excluded regulation's chunks;served_content_hashmakes a correction landing after the first embedding re-embed that document once.
#19), shared summary storage and free re-view (#3), quota (#2), putusan summaries (#18), timeout and quota-safe failure (#4), the executed backfill (mib-rag#6), retrieval caching and tuning, corpus-scoped retrieval and rerank (mib-retrieval#14, #15). Open bugs: mib-ai#14, #15, #16; mib-rag#32, #33.Monetisation
not startedPlans as configurable, versioned data rather than code, so pricing and packaging change without a release. Invoicing carries tax obligations from day one.
- Build
- The plan record with its capability set, versioned rather than edited in place. The payment integration behind a provider-agnostic adapter, with signature verification and idempotent webhook handling. Entitlement resolution from plan version and membership. Invoices bearing the configured tax rate, with buyer details snapshotted at issue and a gapless number sequence. Institutional seats on one subscription. Checkout, and cancel with a reminder and a questionnaire.
- Gate
- A price change leaves existing subscriptions on what they paid, and applies at renewal with notice. An issued invoice is never mutated; a correction is a new document. Replaying a webhook is a no-op. Seat exhaustion refuses the next member with a defined error rather than silently granting access.
mib-identity#7); open there: Midtrans' hosted page versus our own screens, since card entry on our page widens the PCI scope. The plan shape is now Gratis / Premium / Korporasi (client's feature list, 8 Oct), replacing Plan A and Plan B: Premium shown at Rp 75.000 a month (a mock; annual price open), Korporasi through sales. Checkout is mib-identity#36 and mib-web#23; cancel is #37 and mib-web#24. The PRD still describes the old model and needs an amendment.Admin dashboard
auth onlyPartly needed in Phase 1, since reviewing ingestion output cannot wait for the full build. The authentication under it already exists (audience, both roles, seeding script, audit log, from Stage 0.3), so what is left is the surface and the editors rather than the security model. The surface is part of mib-web on admin.DOMAIN rather than its own application (decided 23 Aug): one fewer container on a 2 GiB host, at the cost of a credential that can suspend accounts living in the process that serves public pages (.github#7).
- Build
- Regulation CRUD with a manual-lock marker so corrections survive the next crawl. Ingestion history and retry. User and subscription management. Content editing bounded to text, images and banners, not a page builder. Plan, capability and tax-rate editing behind the super-administrator role, every change audited before and after. Poller and sweep configuration.
- Gate
- A day-to-day administrator cannot alter prices, capabilities or their own role. This is already true and tested as of Stage 0.3; what this gate adds is that it stays true once there are prices and capabilities to alter. Capability names come from a fixed list, never free text, and the impact of a change is shown before it is saved.
Learning Management System
phase twoDeliberately unspecified. The requirements captured so far are an outline from the architecture diagram, not an agreed specification, and must be re-specified before anything here is estimated or scheduled (mib-lms#1). It benefits from identity already being built: the LMS verifies existing tokens rather than building authentication. mib-lms is a scaffold with no routes.
What is actually built
Per repository, as merged. Test counts and what the code does were last run on 29 Aug (435+ tests across 8 Python repos, mib-rag alone 143 including suites against a real PostgreSQL); they have not been re-run since. The ingestion history is in the Stage 1 timeline above.
| Repo | Built | Not built / open |
|---|---|---|
| mib-shared | Auth middleware, the ErrorEnvelope every service returns (error_code, message, request_id, trace_id), tracing, readiness, the traced HTTP client (timeout, bounded retry, fallback, including a retryable status left standing). OSSClient (signing v0.8.0, write v0.9.0, downloads to disk v0.10.0, delete v0.11.0); the shared embedding caller (v0.7.0). Adopted by all five services. A reusable workflow publishes each service's image to GHCR on a tag, verbatim (v0.1.0 → ghcr.io/mib-devs/<service>:v0.1.0). |
#31 (p1): run_migrations' CREATE SCHEMA IF NOT EXISTS fails for a role without CREATE on the database. The migrator roles hold it as a workaround (mib-infra#67); the fix belongs here. #28 shared Qwen chat client; #15 service credentials; #12 JWKS cache cadence. |
| mib-identity | Registration, verification, password reset, login. RS256 tokens with a published JWKS, opaque revocable refresh, single logout, session list, capability resolution. Admin audience, two roles, seeding script, audit log. /ready also fails on a migration-version mismatch in either direction (#22). 23 endpoints, 150 tests (29 Aug). 13 Oct: one password limiter (PASSWORD_HASH_CONCURRENCY, default 4; argon2 cost in configuration; #35, PR #39); 404 for an unknown user on the entitlements read (#34, PR #38). |
Plans (#5), subscriptions (#6), the Midtrans adapter and webhook (#7), invoicing (#8), PPN and quota grants (#29), checkout (#36), cancel (#37), organisations (#4), user admin (#10), site content (#11), maintenance windows (#18). Open decisions: #12, #14, #16. |
| mib-regulations | Internal (service credential, unrouted by Traefik): /internal/regulations/hashes (with exclude_from_rag and served_content_hash), /{id}/text (the corrected object when one is recorded, with the sha256 of the bytes served), /{id}/status (mib-ai's berlaku/diubah/dicabut values mapped from legal_status and is_amended, revoked winning), /rates?on= (Kurs/Tarif with change_amount). Public /api/v1: list, detail, /{id}/html (visitor text truncated before markup), keyword /search, and /search/semantic (switched off). English-schema read model; test_schema_drift.py checks declared columns against a migrated database. 13 Oct: #28, #43, #24, #25 fixed; contract C1 frozen. |
The C1 backend (#27 ranking and exact lookup, #4, #26, #5, #6) with the contract test (#44); public rates (#32); treaties (#31); search teaser (#34); chat lookup and batch facts (#40); admin CRUD (#7); Compile & Compare (#33). CI runs no database, so the DB tests (96 locally) are skipped there. |
| mib-ai | Local token verification. POST /api/v1/ai/summary (#1): grounding chunks from mib-retrieval, the summary with disclaimer and per-chunk source references, gated on ai.summary, bounded by SUMMARY_TIMEOUT_SECONDS, a distinct non-200 for every failure mode. Superseded-regulation gating (#9): amended refuses with a 409 naming diubah_oleh, revoked generates with a warning. app.clients.regulations, built to a contract mib-regulations#16 shipped under a different shape until mib-regulations#18 renamed it (26 Aug). |
The chat (#19), quota (#2), shared summaries (#3), timeout and quota-safe failure (#4), putusan summary (#18), the amended warning (#17), our own UAC run (#20). Bugs: #14 a 404 treated as an outage, #15 unknown status treated as in force, #16 chunks not cross-checked. |
| mib-rag | Structure-aware chunking (pasal, ayat, huruf; length fallback; article and path metadata on every chunk; titling clamped to the stack's depth, #23). Qwen embedding client, batched with bounded concurrency and typed failures (20 texts per request is a hard cap), atomic chunk replace. Staleness predicate and SKIP LOCKED claiming. Corpus sync (#15). Circuit breaker and alerting (#3). Chunk-level failure tracking (#5, FR-RAG-19). The worker loop, gated on window.flag, publishing embedding.flag while draining (#27). exclude_from_rag and served_content_hash (#36, 13 Oct). Chunk size 2,000 with 200 overlap are working values, revisable until the first full embedding run (FR-RAG-27). |
The backfill (#6, p0), annex pieces tagged section = lampiran (#1), needs_ocr exclusion (#5 open again), the SETPP setup (#35), dashboard counts (#19), parent-child chunks (#34). Decisions: #8 index type (IVFFlat, provisional), #9 tuning. Bugs: #32 chunks deleted on a transiently null hash, #33 the sweep stops early. |
| mib-retrieval | POST /internal/v1/retrieve (#1): prompt normalization and cache key, single-prompt embedding, pgvector cosine top-K with an optional regulation_id filter applied before ranking, abbreviation table bundled with the image (30 entries). Callers: mib-ai and mib-regulations (#10). |
Corpus-scoped retrieval and several phrases per request, exact for one regulation (#14), two-stage rerank (#15), response caching (#2), lexical degraded mode (#3), hybrid (#12), reference lookup (#13), slang (#9). |
| mib-ingestion | The whole cycle: JDIH and SETPP adapters, extraction and artifacts, mapping, upsert, relations, provisions, Kurs/Tarif rows, HTML cleanup, text correction, re-extraction, alerts. Parser provisions/8, extractor jdih-extract/9; v0.4.1 tagged. See the Stage 1 timeline. |
Treaty storage and upload tool (#64, #65), stale-poller alert and settings (#59), a periodic full re-check (#58), Peninjauan Kembali (#41), diagrams (#50, draft PR #51). The dev run on the latest parser and TC-10 (memory) are to do. |
| mib-infra | See Stage 0.2: Compose per service with env overlays, Traefik, Postgres and pgvector, role split, observability, maintenance mode, the pipeline cycle, deployment wiring, egress record, smoke tests. | Backup and restore (#6), load harness (#7), security hardening (#9), real visitor IP (#46), the mib-rag mount (#51), Telegram (#43, #44), egress (#55), Traefik 256 MiB (#73, open PR). |
| mib-web | The client mock (#27, v0.2.0; cleaned in #31, v0.2.2): landing, regulation and putusan lists and detail, the AI page, Kurs KMK and Tarif Bunga, pricing, checkout, profile, LMS entry. Tier screens follow the entitlement matrix through one table (accessFor in src/lib/access.ts); DEMO_MODE=1 previews each tier. Registration, verification, login, logout and password reset work against mib-identity with a cookie session (CLAUDE.md, In mib-web). Nothing reads mib-regulations or mib-ai yet. Runs on the dev host at dev.alhilaby.cloud. |
Wiring to the contracts (#20, #36, #21, #19), the chat page (#18), teaser (#17), checkout (#23), cancel (#24), static pages (#25), analytics (#26), the admin screens (#8 to #15). Findings to fix before wiring: #35 (full text sent to visitors, unsanitised HTML, repeated q crash, npm run lint crashes under FlatCompat with eslint-config-next 16 and CI runs only the build, tiers should come from the token's capabilities). |
| mib-lms | Scaffold only: builds, starts, answers its health check, owns no tables. | Phase two (#1). |
Next, in order
The sprint board above is the order for the next ten days. These are the items that sit outside it or behind it.
mib-infra#6, backup automation and a rehearsed timed restore (NFR-38,NFR-39): the last Phase 0 gate. Blocks nothing, gates release.- A first real
mib-ingestionrun on dev: the steps are in Stage 1, "Before a first real corpus". Everything downstream waits on a corpus; the first full embedding run (mib-rag#6) goes after thejdih-extract/9rebuild. mib-web#6, the Figma library. The real constraint on the front end now that screens are sliced.- The embedding worker's flag mount (
mib-infra#51, p1): neither the orchestrator nor the worker mountedMAINTENANCE_DIRintomib-rag, so the two sides cannot see each other's flag file in a real deployment. The same mount exists formib-ingestion(mib-infra#49). - The AI chat (
mib-ai#19): the design is PRD §7.3; the sprint chain builds it. The contract's UAC is the AI handover gate, and our own run (mib-ai#20) must pass before the client's UAT. - Payments (Stage 4): behind the sprint; start with the Midtrans adapter and the plan record.
Decisions on record
Newest first. Each is written into the issue that ships it; the PRD version that records it is named where there is one. The PRD does not yet record the 8 Oct client feature list or 13 Oct contract-first work.
- 13 Oct 2026 Payment gateway: Midtrans (client; PRD v0.35, OQ-9 resolved,
mib-identity#13closed). VA, QRIS and credit card behind the FR-PAY-01 adapter. Egress:api.midtrans.com(andapi.sandbox.midtrans.comoutside production); its payment notifications arrive inbound onmib-identity's webhook route through Cloudflare, so that route must not sit behind the admin allowlist or a login. - 13 Oct 2026 The public API is contract-first (
mib-regulations#35,docs/contracts). The front end writes an OpenAPI contract per feature, the backend adapts, and a contract issue closes at the sync. Contract field names are snake_case English as the existing endpoints are, not the mock's camelCase Indonesian; money and rates are decimal strings; enum values are the stored codes, and labels belong to the web; keys are never omitted, unknown isnull. A correction to a frozen contract nobody has consumed is a patch version. - 8 Oct 2026 The client's feature list is the source of truth (
.github#7). Tiers guest / registered free / premium / corporate; plans Gratis / Premium (Rp 75.000 a month, a mock) / Korporasi (through sales); 2 AI answers a day for visitors and Gratis, 50 for Premium; registering does not raise the limit; a visitor past the limit gets a teaser; a logged-in free user reads in full but the chat is locked until they subscribe; Compile & Compare is Premium and unlimited (AD-8 reversed); payment by VA, QRIS or credit card; cancel is a reminder and a questionnaire. Its open items (Q1 to Q12) are in.github#7with a default each. - 8 Oct 2026 Tax treaties are public (list, search, preview and download without a login), watermarked (a visitor's copy carries the MIB Group logo and time; a signed-in copy also the name and email), found by country and title only, replaced by an administrator (one current file per treaty), and not in the AI chat.
- 12 Oct 2026 Region and models (PRD v0.32, senior architect). Host most likely a Contabo VPS in Singapore; OSS and Model Studio in
ap-southeast-1(AD-4 reversed). Models per call site (PRD §5.2): embeddingqwen3.7-text-embedding(1024 dimensions, unchanged); rerankqwen3-rerank; the chat's sorting callqwen3.8-flash; chat answers and summariesqwen3.7-plus(replacingqwen3.7-flash;qwen3.8-maxonly if plus fails M-02 inmib-ai#20); HTML cleanupqwen3.7-flash; text correctionqwen3.8-flash. Not yet in any.env. - 2 Oct 2026 PRD v0.14: the AI feature is a chat, and an architecture review.
- The UAC RAG criteria annexed to the contract (SPK 042, final) are the handover gate: PT MIB scores answers 1 to 5; an average of at least 3.8, no factual-accuracy score of 1 or 2, at most 15 seconds on average; five test cases, each run three times. NG-2 is reversed.
- The design: fixed steps after one sorting call; exact lookup for a named document; facts filled in by code from the database; every number checked before the AI's text is shown; the whole answer at once, no streaming; one use is one answered message. Still open in
mib-ai#19: how long conversations are kept, the answer budget's values, the page layout. - Summaries are stored once per document version and shared (
mib-ai#3); quota still counts per user and a first view of a stored summary counts; the warning is attached when served. Putusan summaries are in scope (mib-ai#18). - No semantic-search mode on the regulation list. Narrowed 5 Oct: keyword search matches title and number only, through trigram indexes; there is no
search_vector. Consequence:FR-RAG-10's degraded mode now has only titles and numbers to fall back on. content_hashcovers the raw text alone;converter_versionis its own column (mib-ingestion#4).- Vectors: regulations at full precision, putusan at half (
mib-rag#35); each index built after its data is loaded, with Postgres memory settings first (themib-rag#6gate); the corpus is likely near 1 million chunks, not 1.8 million. - Platform: rate limits by the real visitor address (
mib-infra#46); bounded password hashing (mib-identity#35, shipped 13 Oct); fallback PDFs rendered before go-live (mib-regulations#5); one database, measured, not split (mib-infra#6); configurable embedding throughput (mib-rag#6). - New client questions: do search engines list the regulation pages (OQ-35; page caching waits on it), and what can a free account read and download (OQ-36).
- 30 Sep – 1 Oct 2026 PRD v0.12 and v0.13: the source pivot.
- Regulations from JDIH Kemenkeu as full PDFs (HTML fulltext for the older 54%); tax-court rulings from SETPP, the listed risalah only (the client decided not to take the unlisted full putusan, whose scans are about 70,000 to 116,000 ids). Both are treated as freely reusable. The previous source is retired; the decision record is the closed tracker
.github#20. - Raw text drives search and chunking. LLM output is presentation only (AD-10), with one exception: LLM-corrected text, the only fix path for imperfect text (FR-IN-27); the administrator override screen is dropped.
- Two RAG setups, JDIH regulations and SETPP putusan, in separate tables and indexes (AD-11): about 8,700 regulations plus about 34,000 risalah, about 1.8 million chunks and 7 GB of vectors before any index (re-sized in v0.14).
- A document whose structure is not recognised is never processed silently (FR-RAG-29): partial is embedded generically and flagged, unknown is held back, both counted and alerted.
needs_ocris live again (mib-rag#5reopened): a PDF with no text layer is flagged, alerted, kept out of RAG and still downloadable. OCR stays out of scope; if ever needed it is offline and self-hosted, not a paid service. None of 71 sampled JDIH PDFs was fully image-only, but about half of those from 2015 carry a faulty text layer.- JDIH facts: status and relations are reliable only on each document's detail page (typed relations); the search index and feeds are stale; stable ids are
produk_hukum_idand the slug; there is no publicupdated_at. - Out of RAG: Kurs KMK and Tarif Sanksi Bunga KMK are plain lookups taken from JDIH (
mib-ingestion#22); JDIH's own 210 putusan are not ingested. - Chunking defaults 2,000 characters with 200 overlap, revisable until the first full embedding run. The reranker is a hosted call for summary grounding only, built behind a switch and off until measured (
mib-retrieval#15). Alerts go to Telegram. A retrieval request names exactly one corpus (FR-RS-18), and a putusan summary summarises only the putusan (FR-RS-19). Anonymous visitors get a free trial by signed cookie, then a paywall, with a cookie notice (mib-web#17).
- Regulations from JDIH Kemenkeu as full PDFs (HTML fulltext for the older 54%); tax-court rulings from SETPP, the listed risalah only (the client decided not to take the unlisted full putusan, whose scans are about 70,000 to 116,000 ids). Both are treated as freely reusable. The previous source is retired; the decision record is the closed tracker
- 26 Aug 2026 PRD v0.10: ingestion and embedding no longer share a window. The window covers ingestion only and closes when its sweep finishes;
mib-ragembeds afterwards, in the background, with the site up. FR-IN-07 moves from a daily sweep to a weekly one at weekend midnight WIB (narrower than what was said under OQ-23, flagged to the client). A second guard joins the maintenance flag: the next sweep refuses to start while the previous embedding is still draining. This removed the hash race (mib-rag#24) and the OSS torn read (mib-regulations#14) by removing the concurrency rather than patching each symptom. - 26 Aug 2026 English schema naming (
mib-ingestion#2, shipped 5 Oct, PR #28; PRD v0.16):jenis→type,nama→title,nomor→number,tahun→year,status_keberlakuan→legal_status.legal_statusisin_force/revoked/unknownonly, withis_amendedandis_partly_revokedas flags; partial revocations arepartly_revokes/partly_revoked_by;numberis the bare number andnumber_displaythe written form; the JDIHsource_refisproduk_hukum_id(PRD v0.18, OQ-38).data/samples/jdih/metadata.csvis the source of truth for stored values excepttype. Personal data about people at the source (staff identifiers, the SETPPauthorfield) is not stored. - 25 Aug 2026 Generation model
qwen3.7-flash(client-confirmed,ap-southeast-1, closingmib-rag#7; superseded for chat and summaries on 12 Oct). Context window 1M tokens, but input above 32k tokens bills at 4x, and a medium regulation's grounding already lands in 10k to 33k, an omnibus one 50k or more.build_contexthas no token or chunk cap yet (.github#7,mib-ai#1). - 25 Aug 2026 Vector index: IVFFlat over HNSW, decided without the benchmark (
mib-rag#8), because resident memory on the shared host (AD-3, AD-6) favours its lighter, more predictable footprint. Provisional. Tradeoff:listsis fixed at build time and recall degrades as chunks are inserted until the index is rebuilt with a recomputed value; the rebuild cadence is still to be decided once its duration is measured. - 24 Aug 2026 Embedding answered (
mib-rag#7):qwen3.7-text-embedding, 1024 dimensions, verified against the live API, so theVector(1024)column stands. DashScope is called throughTracedAsyncClient, never thedashscopeSDK (no timeout, retry ortraceparent). It runs against Alibaba's OpenAI-compatible API inap-southeast-1(a dedicated deployment, not the native endpoint); 20 texts per request is a hard cap and a larger batch is rejected rather than split.QWEN_API_BASEis a base, not an endpoint (mib-aiappends/chat/completions). The key is required, not defaulted, so a worker without it fails to start. - 23 Aug 2026
mib-adminis part ofmib-webonadmin.DOMAIN(mib-admin#6, archived): a second Next.js container cost 384 MiB on the 2 GiB dev host. Two Traefik routers over one service keep the token-audience separation. Super-administrators exist only via a seeding script run on the host (FR-ADM-15); seed at least two.
Known broken
Live defects, with the closed ones below so nobody re-diagnoses them. Conventions and the rules that bite are in CLAUDE.md.
Rate limits, logs and the download audit see Cloudflare's address (mib-infra#46, p1)
The API rate limiter keys on the edge address, not the visitor's. Until it is fixed the per-IP limits (and the visitor quota that falls back to IP, mib-web#17, mib-regulations#34) are wrong.
mib-rag#32 and #33 (p1)
The corpus sync deletes chunks and embedding state for a regulation whose content_hash is transiently null; run_sweep ends the whole sweep when a batch's claims all fail, leaving the rest stale.
mib-ai#14, #15, #16 (p1)
A 404 from mib-regulations is treated as an outage and retried; unknown legal status is treated as fully in force with no warning (a wrong status scores 1 on the UAC); retrieved chunks are not cross-checked to belong to the requested regulation.
mib-shared#31 (p1) and mib-infra#51 (p1)
run_migrations starts with CREATE SCHEMA IF NOT EXISTS, which fails for a role without CREATE on the database; the migrator roles hold that grant as a workaround, so the fix is still to make in mib-shared. MAINTENANCE_DIR is not mounted into mib-rag, so it cannot see window.flag.
Deploy order and the trigram extension
mib-ingestion's migration runs CREATE EXTENSION IF NOT EXISTS pg_trgm, which its role can only do with CREATE on the database; install it as the owner before its next deploy (on a fresh volume provision-db.sql should, as it does pgvector). The English-schema migration renames the columns and status values that mib-regulations and mib-ai read; mib-regulations#29 (13 Oct) moved regulations to them and keeps mib-ai's wire values mapped; .github#17, which coordinates the downstream migration, is still open.
No live-container smoke test for cross-service contract shape (mib-infra#38, p3)
mib-ai#9 and mib-regulations#16 merged against two different response shapes for the same endpoint and neither side's tests caught it. Contract tests (mib-regulations#44) cover the public API only.
The regulations CI runs no database
Every requires_db test is skipped there, including mib-regulations#25's end-to-end 422 and the change_amount tests. They pass locally against a database migrated by mib-ingestion main. Fixed by mib-regulations#44.
mib-web findings deferred until the pages use real data (mib-web#35)
Detail pages pass the full bodyHtml to client components (FR-REG-20); bodyHtml is rendered unsanitised; ?q=a&q=b crashes PortalList; accessFor should read the token's capabilities; /tarif-sanksi months are not rounded or capped; npm run lint crashes and CI runs only the build. Registration and login are real; never move login into a mib-web route, which would bypass Traefik's rate limit on /api/v1/auth.
Closed
13 Oct: login burst, unknown user, JSON-LD, page → 500, CI install
mib-identity#35 (PR #39: one limiter, argon2 cost in configuration), #34 (PR #38: 404), mib-regulations#24 (PR #47: <, >, & escaped in the JSON-LD block), #25 (PR #48: page capped at 100,000), #41 (sqlalchemy[asyncio]). Live search against a migrated database also works since mib-regulations#29.
mib-identity#21: interactive API docs served in production
Every FastAPI service set docs_url, openapi_url and redoc_url unconditionally, exposing every route including /internal/* (FR-BE-19). They are None when ENVIRONMENT=production in all five services (mib-identity#33, mib-regulations#23, mib-ai#13, mib-lms#5, mib-retrieval#11).
mib-shared#18: fallback skipped on a failing status
A retryable status left standing after the retry budget was returned as-is instead of through fallback, so a caller's degraded path (FR-RAG-10) was unreachable. Fixed in mib-shared#27. Consumer note: mib-ai/tests/test_retrieval_client.py::test_a_final_503_is_returned_rather_than_falling_back still pins the old behaviour and needs updating when mib-ai bumps its pin.
mib-identity#22: /ready healthy on an unmigrated database
SELECT 1 succeeds on an empty schema. A migrations check now compares identity.alembic_version with the local Alembic head and fails in either direction (mib-identity#32).
mib-rag#23 and mib-shared#19
chunk_regulation crashed on an orphan Bagian/Paragraf (mib-rag#31); a call with no request context sent no traceparent, so both sides landed in unrelated traces (mib-shared#23: a fresh trace is started, though a worker still has to bind_context itself). mib-infra#30 (no published images) is fully closed: every service repo has a verified v0.1.0 image in GHCR.
Blocked on the client
Only the questions that actually stop or shape work. The full living list is .github#7, which also carries the open items from the 8 Oct feature list (Q1 to Q12, each with a working default). Other tbd-decision issues are open but block nothing yet.
| Decision | Needed by | Blocks | Whose call |
|---|---|---|---|
What a visitor with no token gets from a download (the contract's open edge; client Q8, .github#7) | Before A9 (Day 9) | The download contract, mib-regulations#5 and the page's download button | Client; proposed default: signed in |
What Compile & Compare does (mib-web#22, mib-regulations#33) | Before it is built | Both issues | Client |
Premium price and annual price, Premium's window, Korporasi terms (mib-identity#14; Q6, Q7, Q9) | Launch only | The pricing page; does not block the build | Client |
| What a free account can read and download (OQ-36) | Before the portal's access rules (Phase 2) and the plans (Phase 4) | Portal access rules; what Premium sells (mib-identity#14) | Client |
| Whether search engines may list the regulation pages (OQ-35) | Before the regulation pages are built | Page caching; robots.txt/noindex and the schema.org block | Client |
Putusan in the product: a separate section or mixed, filters; and Putusan Peninjauan Kembali (mib-ingestion#41) | Before the putusan screens | The putusan screens (mib-web#36) | Client |
| Daily cap on anonymous AI answers that need a model call | Before public launch | The launch value only (mib-web#17) | Client, budget |
Periodic full re-check of every stored regulation (mib-ingestion#58) | Before launch | Late status updates and replaced PDFs are missed until then | Client |
UU PDP basis for processing personal data in Singapore (AD-4, NFR-15) | Before production holds real accounts | Production data | Client, legal |
| Production instance sizing: the application requests exactly 8,192 MB of limits before the OS, observability or a growing vector index; the observability instance needs its own figure | Before the production host is provisioned | The production deploy | Client, budget |
| Whether the UAC's test cases (TC-RAG-01 to 05) are scored on M-01 and M-02 only, as their table's columns suggest; the average is each answer's weighted score over every run (UAC §2 to §5) | Before UAT | mib-ai#20 | Client, PT MIB's testers |
Multi-factor authentication for administrators (FR-ADM-11) | Before Phase 6 | mib-web#8 only; the backend is shaped for it either way | Client |
Data retention policy, accessibility target, bilingual UI (NFR-15, NFR-21, NFR-22) | Before launch | mib-identity#12, #16 | Client |
| Vector index type: settled by benchmark, not preference; which service owns editable site content | Phase 3; before Phase 6 | mib-rag#8; mib-identity#11 | Engineering |
Settled and no longer asks: the payment gateway (Midtrans, 13 Oct); the plan shape and the daily 2 / 50 quota (8 Oct); tax treaties' access (8 Oct); the Lampiran policy (9 Oct); SETPP scope and the unlisted range (30 Sep).
What would change this plan
Each risk has a measurable trigger and a decision attached, so the response is a choice made in advance rather than a scramble. The first group is this sprint's.
Afif's lane is full
A9 (watermarked download) and S1 (quota) are the first to slip. If A9 moves, R10 moves with it and the regulation page ships without download.
Postgres in the regs CI
Adding a database to CI will run tests nobody has run there. Expect failures that are real; budget Day 3 for them rather than treating them as noise.
Visitor download rule
Undecided at the contract. Proposed: if it is still open on Day 9, ship signed-in only; the contract can add a visitor response later as a minor version.
Corpus on dev
A10 needs the backfill (mib-rag#6) finished on dev. It starts on Day 2 so it runs in the background, after the jdih-extract/9 rebuild.
Beyond the sprint
| Trigger | Response |
|---|---|
| A source stops exposing usable change detection, or never did | Freshness is whatever the sweep guarantees, one week, and the client hears that number rather than a hope. The alternative, polling a listing page that quietly misses in-place amendments, is worse than a known lag. |
| Maintenance windows are needed more often than weekly to keep the corpus current | The single-host economics stop working: the platform is offline too often to be a product. Reopen instance sizing with the client; do not shorten the windows. |
| A routine deploy of one service restarts all of them | The independent-deployability rationale for the split has been lost and it is pure cost. Revisit the decision rather than absorbing the overhead. |
| PDF stamping cost exceeds the threshold recorded during load testing | Revert to brand-only stamping at ingestion with pre-signed delivery. Keep that path implementable rather than designing it out. |
| Paid subscriptions exist | Reconsider managed PostgreSQL. User and payment records cannot be re-created; the corpus can be re-crawled. |
Our own UAC run (mib-ai#20) shows a factual-accuracy score of 1 or 2 | Fix before the client's UAT: one such answer fails the AI handover. Tune the prompt, grounding or parser and run again; never hand over on a failing run. qwen3.8-max is the fallback if qwen3.7-plus cannot reach 3. |
| Free-to-paid conversion is poor among accounts with no AI answers left | The allowance and window are plan configuration, so the correction is an edit, not a release. |
| A contract and its backend drift | The contract test (mib-regulations#44) fails CI. The fix is a contract PR if the contract was wrong, never a quiet change on one side. |
Team shape. Sequencing here is driven by dependencies, so it holds at any size, but the staffing for the current sprint is two people: Afif on everything but the web, admin, LMS and ingestion, Raditya on those. Stages 0.1, 0.2 and Design genuinely parallelise. The constraint is Stage 1 and the data it produces (ingestion, the corpus, the relations the portal reads), then the AI chain behind it.
Facts worth not re-deriving
- One venv for the workspace:
D:\hrz\mib\.venv. Neverpip installa service into it (all five declare a package namedapp). mib-identityDB tests needTEST_DATABASE_URL; without it they skip silently. A throwaway works:docker run -d --name pg -e POSTGRES_PASSWORD=test -e POSTGRES_DB=mib -p 55432:5432 pgvector/pgvector:pg16, thenCREATE SCHEMA identity; CREATE EXTENSION pgcrypto;andalembic upgrade head. mib-identity's CI has a PostgreSQL service and a guard that nothing was silently skipped;mib-regulationshas neither yet.- Migrations are generated from
tables.py, never hand-written.alembic checkgates it. - Admin work split:
mib-identity#9closed the 0.3 gate; PPN rate and quota grants moved tomib-identity#29, blocked on#5and#6. - Access tokens are not revocable before expiry. The TTL is the worst-case revocation lag: 15 minutes public, 5 minutes admin.
- argon2 (
argon2-cffi25.1 defaults) holds 64 MiB per password check for about 44 ms; 8 at once is +515 MiB (measured 2 Oct,mib-identity#35). Since 13 Oct at mostPASSWORD_HASH_CONCURRENCY(4) run at once per process; the peak under a burst is still to be recorded by themib-infra#7load test. - The UAC average is each answer's weighted score (25/45/20/10 over M-01 to M-04) averaged over every run; the one gap is that the test-case table scores only M-01 and M-02 (
mib-ai#20). - A transferred GitHub issue loses its board item and has bare
#nreferences in its body, comments and linked PRs rewritten to the old repository. WriteMIB-Devs/repo#nin full and fix every "Closes #n" after a transfer.