Writing passport: evidence of process instead of an AI score

She's submitting work that gets run through an AI detector and wants to
pre-check she won't be wrongly flagged. Petal should not answer that with
a detector of its own: they misfire badly on non-native English (Stanford
2023 found >50% of TOEFL essays flagged as AI vs. near-zero for native
writers), so a percentage aimed at an ESL writer is worse than nothing —
it either scares her off her own voice or gives false comfort.

So the artifact is provenance, not a verdict. Petal already snapshots
every ~3 minutes; this turns that history into a standalone printable
report: session breakdown, word-count growth, span, active time. No score
is emitted anywhere.

Two schema additions back it. preserve_history opts a document out of the
40-snapshot prune cap — right for recovery, wrong for provenance, where
you want the whole span including the oldest rows. content_hash/prev_hash
chain each snapshot to the one before it, so a history edited or thinned
after the fact fails verification. Pruning legitimately severs links, so a
link break reports as "gaps" unless preserve_history is on; only a hash
that fails against its own contents is unconditionally "broken".

The chart's x axis is snapshot order, not wall-clock, and that is the load
-bearing decision. On a linear time axis an essay written in three
sittings across three days renders as three vertical cliffs separated by
empty space — visually identical to text pasted in three chunks, i.e. the
report would have argued the opposite of the truth. Breaks are compressed
into explicitly labelled gutters instead. TestChartGivesWidthToWriting
pins it.

The report volunteers its largest single word-count jump and states its
own limits: it cannot show who was at the keyboard, or whether typed text
was composed or copied in. Overclaiming would be self-defeating — a reader
who catches it overstating discounts all of it.

HTML rather than server-rendered PDF, as with the other exports: a CJK-safe
PDF needs an embedded Unicode font or a headless browser. Print styles are
there so the browser's Save as PDF is the handoff path.

Claude-Session: https://claude.ai/code/session_016Yr6jELuRc7hyzYLccQKZd
This commit is contained in:
prosolis
2026-07-19 11:45:08 -07:00
parent 49e84278d5
commit 78ed1dd281
9 changed files with 1225 additions and 9 deletions
+25
View File
@@ -365,6 +365,31 @@ SELECT id, doc_id, from_pos, to_pos, original, replacement, explanation, type, s
DROP TABLE suggestions;
ALTER TABLE suggestions_new RENAME TO suggestions;
CREATE INDEX idx_suggestions_doc_id ON suggestions(doc_id);
`,
},
{
// Writing passport: evidence that a document was written, not pasted.
//
// `preserve_history` opts a document out of auto-snapshot pruning. The
// 40-snapshot cap is right for recovery (you want recent states) but
// wrong for provenance (you want the *whole* span, oldest included), so
// a writer who may need to defend authorship flags the doc and keeps
// every snapshot.
//
// `content_hash`/`prev_hash` chain each snapshot to the one before it:
// hash = sha256(prev_hash | doc_id | created_at | word_count | text).
// This proves the local history is internally consistent — no snapshot
// was edited, reordered, or removed after the fact without breaking
// every link downstream. It is NOT third-party attestation: anyone with
// the DB and the algorithm could forge a fresh chain. It raises the cost
// of a doctored history from "edit one row" to "rebuild all of them".
// Pre-existing snapshots keep empty hashes and are reported as
// unverifiable rather than as failures.
name: "0009_writing_passport",
stmt: `
ALTER TABLE documents ADD COLUMN preserve_history INTEGER NOT NULL DEFAULT 0;
ALTER TABLE document_versions ADD COLUMN content_hash TEXT NOT NULL DEFAULT '';
ALTER TABLE document_versions ADD COLUMN prev_hash TEXT NOT NULL DEFAULT '';
`,
},
}
+11
View File
@@ -25,6 +25,10 @@ type Document struct {
WordCount int `json:"word_count"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
// PreserveHistory opts this document out of auto-snapshot pruning so its
// full writing trail survives as authorship evidence (see the passport).
PreserveHistory bool `json:"preserve_history"`
}
// DocumentVersion is a point-in-time snapshot of a document's body, captured so
@@ -42,6 +46,13 @@ type DocumentVersion struct {
WordCount int `json:"word_count"`
Kind string `json:"kind"` // auto | manual | pre_restore
CreatedAt time.Time `json:"created_at"`
// ContentHash chains this snapshot to the previous one (PrevHash), so a
// history that was edited or thinned after the fact fails verification.
// Both are empty for snapshots taken before the chain existed. Omitted from
// list responses; the passport loads them explicitly.
ContentHash string `json:"content_hash,omitempty"`
PrevHash string `json:"prev_hash,omitempty"`
}
// Document version kinds, mirrored from the schema CHECK constraint.