| D-M8-03-0 |
2026-10-22 |
Built on the old integration line and ported to main after M2-09 landed: only the ticket's own commits were brought over. The migrations are renumbered to 00064_importer_import.sql and 00065_messages_import_item_fk_validate.sql because main holds 00057 to 00063; the start of the module moves into wire/module_importer.go (order 1380 in the step chat, after stats, PL/28); the generated clients, sqlc code and goldens are regenerated on main; setMyAvatar belongs to M3-10, and the deferred action of the authz cache test is now interaction.create. |
The rules of the landing replace the integration branches; main is the reference. |
| D-M8-03-1 |
2026-10-21 |
importer owns import_sources and import_items and creates objects only through the ports chat.ImportStructure and identity.ImportPeople, inside the unit of work of the batch, so one batch is one transaction across modules. Every change of a source (state, ownership list, batch, cursor) locks its row first, so the changes of one source are serialized; two batches with the same keys wait for each other at the source row and the second answers the stored targets. Imports.SourceOfTarget answers "where does this object come from" from the index (target_kind, target_id). |
Design note 1; one lock order avoids the deadlock two share locks would give at the counter update. |
| D-M8-03-2 |
2026-10-21 |
The mapping import_source.discord.v1 is the component ImportSourceDiscordConfig; ImportSourceCreate.config stays JsonObject and the server validates it field by field (unknown member, required member, digits, duplicate Discord ID per list, name over space.name_max_chars or channel.name_max_chars, uncategorized_space without category, unknown handle, person or agent not active, existing UUID that names no live space or channel) and reports all violations at once with JSON pointers. A target is ImportMappingTarget without a schema of its own (only a description), because the lint of the contract forbids oneOf without a discriminator and anyOf, and the code generator cannot map a type list; the server checks the three forms. The stored mapping is normalized (planned_operator none, lead discord filled in, names trimmed). kind = archive is 400 validation_failed with not_enabled at /kind. |
Design note 2. |
| D-M8-03-3 |
2026-10-21 |
States: draft/paused to running (start), running to paused, draft/running/paused to failed (cancel, with stats.server.cancelled = true), running/paused to completed (complete, only when every mapped non-skip channel whose item is posted has lead = bothy, else lead_not_bothy). Start on running and pause on paused answer unchanged with one audit entry no_change and no event; every other transition is 409 conflict import_state. cancelled lives under stats.server like every server counter. |
Design notes 3 and 10. |
| D-M8-03-4 |
2026-10-21 |
setImportLead changes lead in the stored mapping and channels.read_only through chat.ImportStructure.SetReadOnly in one unit; runs cancels the active runs through chat.ChannelReadOnlySet. setChannelReadOnly answers 409 conflict import_lead_controls while the target channel of a posted channel item belongs to a source that is neither completed nor failed; chat asks the source port chat.ReadOnlyGuard, implemented by importer/app.Guard over the store alone and injected by internal/wire. |
Design note 4, 02 R-AR-04. |
| D-M8-03-5 |
2026-10-21 |
An existing origin key is locked and answered with its stored item; a content_sha256 that differs from the stored one adds source_changed = true to the item and raises stats.server.source_changed, nothing else changes. Counters are raised only for items with a new transition in this batch, and only such a batch appends import.progress, so a fully replayed batch appends no event. |
Design note 5 b, 22 7.2 item 4. |
| D-M8-03-6 |
2026-10-21 |
Forum tag key forum_tag:<channel digits>:<tag digits>; the item's channel_origin_key must be channel:<channel digits> of the key, else target_outside_source. |
Design note 6: a tag ID is unique only within its forum in the data the tool sees. |
| D-M8-03-7 |
2026-10-21 |
The import creates through ports, never through SQL of another module. The stored mapping decides: a category or channel not named in it is target_outside_source, one mapped to skip is stored skipped (also a channel whose category is skipped, and one without category while uncategorized_space is skip); a new space or channel takes the name of the mapping, not of the item data; an existing target is adopted only when the item names exactly it. A new name equal to a live space (globally) or channel (in its space) is 409 conflict name_collision. New channels get operator mode none (no inheritance of a space default), no grant and read_only = (lead = discord); text channels get their main thread in the search language of the instance. sort_key of a new space or channel is a fractional index key the tool derives from the Discord position; another value is 400 validation_failed at /items/<i>/data/sort_key. An adopted object keeps its name and position. |
Design note 7, 22 5.1 rules 1 to 4, 14 3.1. |
| D-M8-03-8 |
2026-10-21 |
Placeholders: handle from the Discord username (the global_name only when the username is blank) by the rule of design note 8, the first free handle wins: HandleFree skips taken candidates, and a candidate a concurrent batch of another source takes before the insert (INSERT ... ON CONFLICT DO NOTHING of identity, a conflict on /handle that leaves the unit usable) moves on to the next counter (red team finding M1); the display name is global_name or username without control and bidi characters, cut to principal.display_name_max_chars, the handle when nothing visible is left. Identity inserts the person and sets disabled with disabled_at in the same unit and appends user.updated. Webhook authors are always placeholders; an unmapped bot or person follows defaults. The target_kind of a person or placeholder is user, of an agent agent. |
Design note 8, 22 5.3. |
| D-M8-03-9 |
2026-10-21 |
setImportCursor replaces cursor with the given object and merges the top-level members of stats into stats.tool; the server keeps its counters under stats.server. getImport, listImports, setImportCursor and every source answer carry the member cursor. |
Design note 10, 22 7.3 and 7.4. |
| D-M8-03-10 |
2026-10-21 |
A source that is not running and a target outside the source answer 409 conflict with reason import_not_running or target_outside_source (the brief, and M8-04 expects 409), although 07 4.1 lists import_not_running as an authorize reason with decision forbidden; import.apply is the requirement owner in authz and the use case records each of the two refusals as a denial with reason import_not_running (RecordDenial, which survives the rollback). |
The audit keeps the reason of the closed list; the problem answer follows the brief. |
| D-M8-03-11 |
2026-10-21 |
The collection of import sources (listImports, createImport, resource import_source without an id) is visible like the collection of apps: the requirement owner answers a person without owner rights and a personal token of an owner 403 forbidden with reason owner_only; a single source stays owner-only visible (404). |
Brief: a personal token of an owner gets 403 (SR5). |
| D-M8-03-12 |
2026-10-21 |
The integration tests of the brief live in test/integration/importer (package importer_test on the harness of M1-71) instead of internal/importer/adapters/httpapi; internal/importer/adapters/httpapi holds a unit test of the handlers. "Stored, then 502" drops the answer of the committed batch in the client, because the harness has no hook into the response writer; the server sees exactly the same as after a lost answer. The personal token of TestCreateImport_StepUpAndAudit is one of the setup owner, because an owner cannot create a token for another person (not_self). That authorize refuses a placeholder message.create and agent.invoke is held by TestPlaceholderNeverTriggers in internal/authz/app, because a placeholder cannot sign in to send a request. |
The harness runs the real wire graph only from test/. |
| D-M8-03-13 |
2026-10-21 |
runs reads the threads of a channel that became read-only through chat.Structure.ThreadIDsOfChannels (runs/app.ChannelThreads, defaulting to the thread port when it implements it) and locks their active runs with LockActiveRunsByThreads; the run is not ended at once but gets run.cancel_requested with channel_read_only and the job runs.cancel_timeout, like a revocation. |
runs has no channel_id (sql visibility); M2-09 design note 7 g. |
| D-M8-03-14 |
2026-10-21 |
Red team NEIN (B1 the rights test of the migrations lacked the two tables, M1 the placeholder handle without retry on a concurrent conflict): the one fix adds the rows of import_sources and import_items to appPrivileges and the conflict-safe insert with the next counter, each with a test that failed before; then one delta red team. |
Process of the brief and of the steering. |