Date. 2026-08-25 Author. Claude Code on DUNIN7-M4. Status. Read-only inspection. Nothing edited, staged, committed, or pushed in any repository. Question. When a document or file is uploaded into Loomworks today, does ingest produce a plain-English description or summary of the document that is stored anywhere, or does it produce only extracted assertions (plus a source record)?
Path note. The inspection brief's greps target loomworks/; the package actually lives at src/loomworks/. Paths below are as they exist on disk. All line numbers are at the HEAD recorded in §1.
| Item | Value |
|---|---|
| Repository | /Users/dunin7/loomworks-engine |
| Branch | main |
| HEAD | 2f41aa7 |
| Tree state | clean — git status --short produced no output |
All four upload-pathway routes are registered in src/loomworks/api/routers/uploads.py and mounted at src/loomworks/api/app.py:770,789.
| Route | Method | Handler | Registration |
|---|---|---|---|
| /engagements/{engagement_id}/uploads | POST | operator_upload (uploads.py:668) | uploads.py:1170–1171 |
| /engagements/{engagement_id}/uploads/store-only | POST | store_only_upload (uploads.py:1212) | uploads.py:1298–1299 |
| /engagements/{engagement_id}/uploads/{upload_event_id}/source-file | GET | serve_source_file (uploads.py:1325) | uploads.py:1436–1437 |
| /engagements/{engagement_id}/files/{file_id}/manual-content | POST | submit_manual_content (uploads.py:1497) | uploads.py:1607–1608 |
The ingest route is operator_upload. store-only explicitly skips detection and transformation (uploads.py:1302–1305: "Persists a single file's bytes… with no detection and no transformation skill run").
execute_upload (src/loomworks/uploads/executor.py:442), invoked from uploads.py:75.src/loomworks/uploads/skills/; base class at src/loomworks/uploads/skills/base.py:60.src/loomworks/uploads/detection_registry.py (get_purpose_framings_for_chains, imported uploads.py:74).summarize_if_long (src/loomworks/uploads/summarization.py:47), imported uploads.py:81.src/loomworks/uploads/skills/image_vision_analysis.py, calling run_vision (src/loomworks/uploads/vision/claude_vision.py:108).
operator_upload performs exactly three classes of persistent write.
Blob written via the object store; the resulting blob_reference supplies uri, content_hash, size_bytes consumed at uploads.py:1000–1004. Key generation at src/loomworks/storage/object_store.py:160.
add_assertion — a held assertion, per file
uploads.py:965–972. Written only when result.status == "completed" and the transformed text is non-empty (uploads.py:931–935).
| Field | Value | Line |
|---|---|---|
| content | assertion_content — the extracted text, or its summary if over threshold | uploads.py:967 |
| grammar_element | "definition" | uploads.py:968 |
| normative_force | "optional" | uploads.py:969 |
| metadata.source_file_id | str(upload_event_id) | uploads.py:953 |
| metadata.original_filename | filename | uploads.py:954 |
| metadata.extraction_method | ",".join(result.selected_chain) | uploads.py:955 |
| metadata.summarized | was_summarized (bool) | uploads.py:956 |
| metadata.original_character_length | set only when summarized | uploads.py:957–958 |
| metadata.source_mode | when non-None | uploads.py:959–960 |
append_event — the source record (UploadEventReceived)
Constructed uploads.py:991–1023, appended uploads.py:1025–1030, event kind upload_event_received. Model at src/loomworks/uploads/upload_event.py:182.
Fields populated: source_artifact_uri, source_artifact_content_hash, source_artifact_filename, source_artifact_size_bytes, source_artifact_content_type, parent_upload_event_id, folder_manifest, detection_result, purpose_declaration_text, purpose_classification_result, accompanying_message, selected_chain, per_step_provenance, transformed_content_uri, transformed_content_inline, assertions_drafted, operator_initiator, upload_moment, transformation_completed_moment, status, reliability.
**transformed_content_inline (upload_event.py:73, set at uploads.py:1016) carries the verbatim extraction — not the summary.** The summary, when one is produced, is passed only to add_assertion (uploads.py:967). The two diverge whenever summarization fires.
record_turn ×2 — conversation turns
uploads.py:1120–1138, gated on actor.person_id is not None (uploads.py:1091); skipped with a log line otherwise (uploads.py:1140–1145). See §6.
No other database write is performed by the ingest handler.
**Two mechanisms produce plain-English prose. Neither is stored as a description of the document; both become the document's extracted content itself.**
src/loomworks/uploads/summarization.py. Threshold SUMMARIZATION_THRESHOLD_CHARS = 2000 (summarization.py:32). Prompt at summarization.py:38–45:
> "Summarize the following file content for a Memory record. Preserve concrete facts, names, numbers, and structure an Operator would want to recall; omit boilerplate and filler. Write plain prose, no preamble, no markdown formatting."
The module's own docstring states the intent (summarization.py:3–8): text over threshold is "summarized before becoming a held assertion, rather than storing the full raw text verbatim." And summarization.py:10–12: "This is a scale guard, not a general summarization feature."
Where it is stored: as content on the held assertion (uploads.py:946–950 → uploads.py:967). Nowhere else. It is substituted for the raw text, not stored beside it.
Applies to at most a minority of files — content at or under 2000 characters passes through unchanged with no API call (summarization.py:55–57).
Fails open to verbatim. A missing key or API error falls back to the original content (summarization.py:58–62), recorded via metadata.summarized (uploads.py:956).
src/loomworks/uploads/skills/image_vision_analysis.py:60–65:
> CALL_1_PROMPT = ("Describe what is in this image in one brief paragraph — 1 to 3 sentences covering the key subject and one or two notable details, not an exhaustive inventory. Use complete prose. Do not preface; describe directly.")
Invoked at image_vision_analysis.py:250–255. For images, ingest does produce a plain-English description. But it is the skill's extraction output — it becomes the transformed content, and from there the assertion content and transformed_content_inline. There is no separate description field holding it.
_content_summary is not a summary
uploads.py:397–411. Despite the name, this generates nothing: it returns the transformed text inline when short, or truncated to a preview with "[abbreviated — full output is N characters]" when long. Used only for the wire response (uploads.py:510) and the turn's structured_data. It is a truncation of the raw extraction, not a description.
UploadEventReceived (upload_event.py:182–260) has no field named or resembling description, summary, abstract, caption, gist, or narrative. The description= occurrences in that file (upload_event.py:126,135,140,148,156,165,174) are Pydantic Field(description=…) API documentation strings, not stored values.
Confirmed against the live schema rather than only the source: querying information_schema.columns across all non-system schemas for column names matching (description|summary|abstract|caption|gist|narrative|tags|keywords|labels|topics) returned exactly one column in the entire database — credit.evaluator_state.last_run_summary — which is scheduler bookkeeping, unrelated to uploads.
Corresponding source greps: Mapped column declarations matching those names across src/ — not found. sa.Column("<name>") matching those names across migrations/versions/ — not found. uploaded_files (src/loomworks/files/models.py:38) carries id, engagement_id, uploaded_by_person_id, original_filename, content_type, file_size_bytes, storage_path, created_at — and nothing else.
Not found. No field named or resembling tags, keywords, labels, or topics exists on UploadEventReceived (upload_event.py:182–260), on UploadedFileRow (files/models.py:38–45), in any SQLAlchemy model under src/, in any alembic migration, or anywhere in the live schema (see §4.4 for the authoritative query and its single unrelated hit).
The assertion metadata written by ingest (uploads.py:952–960) contains six keys — source_file_id, original_filename, extraction_method, summarized, original_character_length, source_mode — none of which is a tag, keyword, label, or topic.
Ingest performs no classification into a controlled vocabulary. The one classification it does perform is purpose classification (purpose_classification_result, upload_event.py:50), which selects an extraction chain; it is not a topical label on the document.
Generated: yes. Persisted: yes — but it names files, it does not summarize content.
Composed by _compose_upload_companion_message (uploads.py:595), persisted via record_turn with role="companion" (uploads.py:1128–1137), alongside an operator turn (uploads.py:1120–1127). Both are gated on actor.person_id is not None (uploads.py:1091); when absent, both are skipped and only a log line is written (uploads.py:1140–1145) — the blob, assertion, and source-record writes are unaffected (uploads.py:1088–1090).
The message deliberately does not carry a description. Its docstring (uploads.py:604–612) records the removal:
> "Duplicate-description fix (post-CR-2026-141) — this no longer appends the full description text (CR-2026-136's approach)… The card is now the sole carrier of the full description; this message stays a short acknowledgment naming the file(s)."
The composed text is of the form "Here's what I found in that file: {filename}." (uploads.py:625–633) — the filename, not the content.
**What is persisted with content:** structured_data on the companion turn (uploads.py:1111–1119,1136), carrying kind: KIND_UPLOAD_RESULTS and a list of UploadResultItem. Each item includes content_summary — which per §4.3 is the truncated raw extraction, not a generated description. Its stated purpose (uploads.py:1105–1108) is so "a reload rebuilds UploadResultCard from history."
Not found — there is no fallback, because the condition cannot arise at this layer.
All four upload routes are engagement-scoped by path (uploads.py:1171,1299,1437,1608), and engagement_id is a required path parameter (uploads.py:670: engagement_id: uuid.UUID = Path(...)). A grep for upload routes not under /engagements/ returned nothing. An upload without an engagement is not expressible against this API.
host_account.personal_engagement_id exists as a concept (src/loomworks/memory/embeddings.py:33,97,104) but a grep for personal_engagement_id intersected with upload returned no hits — it is not used as an upload destination or fallback.
Scope limit: this establishes there is no engine-side fallback. Whether the Operator Layer surface prevents, queues, or redirects an upload attempted with no engagement selected was not inspected and is not claimed here.
Ingest produces only extracted assertions plus a source record. It does not produce or store a plain-English description of the document as a distinct, retrievable thing. What prose it does generate is substituted for the document's content, never stored alongside it as a description of it: text over 2,000 characters is summarized and that summary becomes the held assertion's content (summarization.py:32, uploads.py:946–967), while the source record keeps the verbatim extraction in transformed_content_inline (uploads.py:1016); and an uploaded image is genuinely described in prose by the vision skill's prompt (image_vision_analysis.py:60–65), but that description is the extraction output and lands in the same two places. There is no description, summary, abstract, caption, or gist field on the source record or on uploaded_files, and no such column exists anywhere in the live schema apart from one unrelated scheduler field — so there is nowhere for a document-level description to live even if one were produced. There are likewise no tags, keywords, labels, or topics: ingest writes six metadata keys, none of them topical. The Companion does narrate the upload and that turn is persisted, but by post-CR-2026-141 decision it names the files rather than describing them, and the only content-bearing payload on that turn is a truncated copy of the raw extraction for card rehydration. In short: a file arrives, its text is extracted (or, for images and long text, rewritten into prose), that text becomes one held assertion, and a source record captures provenance — and nothing anywhere holds an answer to "what is this document about?" as a separate stored field.
DUNIN7 — Done In Seven LLC — Miami, Florida Loomworks — Upload ingest: is a description or summary stored? — Inspection v0.1 — 2026-08-25