docs(envelope importer): name the extraction path, the UTC day, and drop a contradicted key claim

R2. The header promised `type: conversation` "keeps pages eligible for
conversation-facts extraction ... after sync" and stopped there, which
reads as "sync extracts facts from these." It does not. The accepting
path is `gbrain extract-conversation-facts`, run by hand, and its
autopilot wrapper — the `conversation_facts_backfill` cycle phase — is
opt-in and OFF by default. Both names verified against upstream/master
before writing them: src/cli.ts:1964 dispatches the command,
src/core/cycle/conversation-facts-backfill.ts:4-6 and :34 document the
phase as "Default OFF" with `enabled (false)`, and doctor.ts:3373 reads
an absent key as disabled. What the type actually buys is admission to
`ALLOWED_TYPES`, and that is now what the header says.

R3. The page `date` and every turn header are the UTC calendar day, not
the user's, and the limits block did not say so. Reproduced on this box:
a conversation held at 19:30 on Sunday 2 November 2025 in California is
`created_at: "2025-11-03T03:30:00.000Z"`, and the page comes out
`date: "2025-11-03"` — a Monday — with a Monday turn header. The output
is byte-identical under TZ=UTC, America/Los_Angeles, Asia/Kolkata,
Pacific/Kiritimati, Pacific/Midway and Europe/London (one sha across all
six), so this is the format's day, not the importing machine's.

Named as unfixable, with the reason rather than an apology: envelope-v0
renders every timestamp as `...Z` and SPEC.md rule 3 says why — "The
source's true offset is unknowable, and UTC is the only
machine-independent choice." A source offset is normalised away before
this script opens the file, and the vendor fields the reference
converter reads carry none to preserve: over the 22 conversation-level
timestamps in its own input fixtures, 16 are bare unix-epoch numbers, 4
end in `Z`, 2 have no designator, and 0 carry a numeric offset. No fix
is invented; the one available guess (the importing machine's zone)
would break determinism.

R4. Deleted the claim that "the id is the natural key," which the header
disclaimed 127 lines later in its own identity limit. The key is the
PAIR — date and id name the file — and the surviving limit already says
so correctly. The same claim appeared a second time as an in-code
comment at the filename site ("the date only leads as a ... prefix; the
id carries uniqueness"); deleting one statement of a false claim and
leaving its twin is a half-correction, so both went. Flagged as a
deviation: R4 as written names the header only.

The bullet R4 emptied also still described the pre-R1 duplicate-id
behaviour, so it is rewritten to the behaviour that now ships.

Doc-only: 107 pass / 0 fail, unchanged.
This commit is contained in:
Sean Gearin
2026-08-03 07:21:01 -04:00
parent 030018e2a2
commit 81deb83d41
+58 -10
View File
@@ -30,10 +30,12 @@
* conflict, and the import is refused (exit 2).
*
* Output layout:
* - One page per conversation, filename = date + conversation id (shared
* titles cannot collide; the id is the natural key). A duplicate id
* overwrites its own filename and warns on stderr; stdout reports DISTINCT
* files written, not write calls.
* - One page per conversation, filename = date + conversation id, so shared
* titles cannot collide. Two conversations carrying the SAME id land on one
* filename; the copy with the later `updated_at` is kept and the other is
* discarded, with array order as the documented fallback when neither
* carries an orderable one. Either way stderr says which copy went and why,
* and stdout reports DISTINCT files written, not write calls.
* - `id` is `string | null` in envelope-v0 and a converter must not synthesize
* one, so null is a conforming shape, not malformed input. Such a
* conversation falls back to a POSITIONAL filename (`conv-N`) — a function
@@ -52,7 +54,21 @@
*
* Every page here declares `type: conversation`, which is what opens the gate to
* conversation-facts extraction, chronicle eligibility, and the
* conversation_format_coverage check. Until 2026-08-02 the body then presented a
* conversation_format_coverage check.
*
* ELIGIBLE IS NOT AUTOMATIC, and the difference is the whole reason to say this
* out loud. The path that accepts these pages is `gbrain
* extract-conversation-facts` (src/commands/extract-conversation-facts.ts),
* which is run by hand, and its autopilot wrapper is the
* `conversation_facts_backfill` cycle phase — opt-in and OFF by default
* (`cycle.conversation_facts_backfill.enabled`, default false;
* src/core/cycle/conversation-facts-backfill.ts). Importing and syncing these
* pages therefore extracts no facts from any of them until one of those two is
* invoked. What the type buys is admission: `ALLOWED_TYPES` in that command
* requires it, so a page typed anything else is not merely un-run, it is
* ineligible.
*
* Until 2026-08-02 the body then presented a
* turn header — `**Assistant** (2025-11-02T14:22:51.000Z · m2):` — matching NONE
* of the 17 built-in patterns in `src/core/conversation-parser/builtins.ts`
* (`gbrain conversation-parser list-builtins` counts them). The
@@ -214,6 +230,36 @@
* edit that inserts or removes a turn in the body without editing the
* frontmatter silently re-points every id after it — and so does any of the
* parser behavior above.
* - THE DATE IS THE UTC CALENDAR DAY, NOT THE USER'S. The page `date` and
* every turn header are read off a timestamp already normalised to UTC, so
* a conversation held in the evening west of Greenwich files on the
* FOLLOWING day, and one held in the early morning east of it files on the
* PREVIOUS day. Measured here, importing under TZ=America/Los_Angeles a
* conversation that happened at 19:30 on Sunday 2 November 2025 in
* California:
*
* created_at "2025-11-03T03:30:00.000Z"
* -> date: "2025-11-03" (a Monday)
* -> **Me** (2025-11-03 03:30):
*
* Anything that groups, windows or reports these pages by day inherits that
* shift. The importing machine's own zone changes nothing — the output
* above is identical under every TZ, deliberately.
*
* This is not fixable, here or upstream, and the reason is not neglect: the
* offset is not in the file. envelope-v0 renders every timestamp as
* `YYYY-MM-DDTHH:mm:ss.sssZ` — "always UTC, always the `Z` designator" — and
* SPEC.md rule 3 states the reasoning outright: "The source's true offset is
* unknowable, and UTC is the only machine-independent choice." An offset
* present in a source export is therefore normalised away by the format
* before this script ever opens the file. Nor do the vendor fields the
* reference converter reads carry one to preserve: across its 22 conversation
* -level source timestamps, 16 are bare unix-epoch numbers (ChatGPT
* `create_time`/`update_time`, which cannot express a zone), 4 end in `Z`,
* 2 carry no designator at all, and NONE carries a numeric offset. Guessing
* from the importing machine's zone is the one available "fix" and it is
* worse than the bug: it would make one envelope produce different pages on
* two laptops, which is the determinism this script is built on.
* - MINUTE RESOLUTION IS THE CEILING, and it is the parser's, not this
* format's. Every branch of `buildIso` in parse.ts hardcodes `:00` seconds,
* and no built-in pattern captures a seconds group — `signal-export`
@@ -595,11 +641,13 @@ const pages = new Map();
let collisions = 0;
for (const [i, c] of conversations.entries()) {
const date = (c.created_at || '').slice(0, 10);
// Name the file by the conversation's own id — the natural unique key — so two
// conversations that share a date and title can never silently overwrite each
// other. The date only leads as a human/chronological sort prefix; the id
// carries uniqueness. Positional fallback keeps names unique and deterministic
// when an envelope omits an id.
// Name the file by the conversation's own id, so two conversations that share
// a date and title can never silently overwrite each other. The KEY IS THE
// PAIR: date and id together name the file, and the date is not merely a
// human/chronological prefix — a conversation whose `created_at` changes
// between exports lands on a new filename, which is the "orphans its earlier
// page" limit in the header. Positional fallback keeps names unique and
// deterministic when an envelope omits an id.
// One predicate for "this conversation carries its own id", shared by the
// filename, the frontmatter below, and the conflict check further down.
// Keeping it in a single place is what stops them disagreeing about whether