| 1 | package memoria |
| 2 | |
| 3 | import "time" |
| 4 | |
| 5 | // Connector is one configured source outside the suite. |
| 6 | // |
| 7 | // The credential lives here, sealed under its own HKDF purpose, so a |
| 8 | // disclosure of the chunk store does not also yield the key to the source |
| 9 | // those chunks came from — the spec's §3.5, which calls least-privilege |
| 10 | // connector credentials a requirement rather than a nicety. |
| 11 | type Connector struct { |
| 12 | ID int64 |
| 13 | // Kind is "notion". One row per kind. |
| 14 | Kind string `gorm:"uniqueIndex;not null"` |
| 15 | // SealedToken is the integration token. Never rendered back, not even |
| 16 | // masked: a masked secret is still a secret in the DOM. |
| 17 | SealedToken []byte `gorm:"not null"` |
| 18 | // WorkspacePrint is a keyed fingerprint of the workspace the token |
| 19 | // belongs to, so two connectors pointing at one workspace are |
| 20 | // detectable without storing which workspace that is. |
| 21 | WorkspacePrint string |
| 22 | LastVerified *time.Time |
| 23 | CreatedAt time.Time |
| 24 | UpdatedAt time.Time |
| 25 | } |
| 26 | |
| 27 | // SyncRun is one import, and the row that makes it resumable. |
| 28 | type SyncRun struct { |
| 29 | ID int64 |
| 30 | ConnectorID int64 `gorm:"index;not null"` |
| 31 | // State is enumerating | fetching | done | failed | interrupted. |
| 32 | State string `gorm:"index;not null"` |
| 33 | // Roots is how many top-level pages the integration could see, so |
| 34 | // "I expected more than that" is an answerable question. |
| 35 | Roots int |
| 36 | // Total is -1 until enumeration finishes. |
| 37 | // |
| 38 | // Not 0. The precedent is handlers.go's chunkCount, which returned 0 |
| 39 | // on error and made the page assert an absence was real when the app |
| 40 | // had merely failed to look. A total that could not be determined is |
| 41 | // not a total of zero pages. |
| 42 | Total int `gorm:"not null;default:-1"` |
| 43 | Fetched int |
| 44 | Skipped int |
| 45 | Empty int |
| 46 | FailedPages int |
| 47 | UnreadBlocks int |
| 48 | // ErrorCode is a CODE, never a message. Notion's own error bodies |
| 49 | // carry content — a 404 quotes the page id and the connection name — |
| 50 | // and AGENTS.md forbids content in any error string. |
| 51 | ErrorCode string |
| 52 | // Heartbeat lets a run wedged inside a LIVE process be reclaimed. A |
| 53 | // run killed by the platform sleeping is reclaimed at boot instead, |
| 54 | // which is sound by construction rather than by timeout: there can be |
| 55 | // no live run at boot. |
| 56 | Heartbeat *time.Time |
| 57 | StartedAt time.Time |
| 58 | FinishedAt *time.Time |
| 59 | } |
| 60 | |
| 61 | // SourcePage is the ledger: one row per page a connector has seen. |
| 62 | // |
| 63 | // Keyed per CONNECTOR and not per run, which is the whole reason a second |
| 64 | // run resumes instead of restarting. |
| 65 | type SourcePage struct { |
| 66 | ID int64 |
| 67 | ConnectorID int64 `gorm:"index;not null"` |
| 68 | // PagePrint is the keyed page id, used to find this row. |
| 69 | // |
| 70 | // It does NOT join to a Document's ExternalID, though a comment here |
| 71 | // used to say so: both are HMACs, under different purposes, of |
| 72 | // different inputs. The link to the document is DocumentID below. |
| 73 | PagePrint string `gorm:"index;not null"` |
| 74 | // SealedPageID is the id itself, sealed and REVERSIBLE. |
| 75 | // |
| 76 | // Both columns exist because an earlier design stored only the |
| 77 | // fingerprint, and seal.Fingerprint is HMAC-SHA256: a resumed run |
| 78 | // would have held 1,930 fingerprints and no id to fetch with. Found |
| 79 | // by a design review before any of it was written. |
| 80 | SealedPageID []byte `gorm:"not null"` |
| 81 | // SealedPath is the ancestry as enumeration saw it. PathPrint lets a |
| 82 | // move — or an ancestor rename — be detected without opening the row. |
| 83 | SealedPath []byte |
| 84 | PathPrint string |
| 85 | // SeenEdited is what the last enumeration saw; IngestedEdited is what |
| 86 | // memoria actually ingested. Two columns, because one column compares |
| 87 | // against itself and therefore skips every edit. |
| 88 | SeenEdited string |
| 89 | IngestedEdited string |
| 90 | // IngestedPath is the path print as of the last successful ingest. |
| 91 | // A row is up to date only when BOTH the edit time and the ancestry |
| 92 | // match what was ingested — an ancestor rename moves the second |
| 93 | // without moving the first. |
| 94 | IngestedPath string |
| 95 | // State is pending | fetched | empty | failed. |
| 96 | State string `gorm:"index;not null;default:'pending'"` |
| 97 | ErrorCode string |
| 98 | // LastSeen is written on every enumeration, so "no longer in the |
| 99 | // source" stays answerable without a later migration. memoria does |
| 100 | // not delete on absence; it does not follow that it should be unable |
| 101 | // to say what has gone. |
| 102 | LastSeen time.Time |
| 103 | DocumentID int64 |
| 104 | CreatedAt time.Time |
| 105 | UpdatedAt time.Time |
| 106 | } |
| 107 | |