21 KiB
Ticket Decomposition & Knowledge Ingestion Implementation Plan
Date: 2026-07-09
Project repo: /home/jason/projects/redmine
Primary vault: /home/jason/lilaBuild/vaults/obsidian-ldr
Primary Redmine project to start: customer-service
Production Redmine: https://rm.ldrprep.com
Onyx vault URL prefix: https://onyx.int.ldrprep.com/projects/obsidian-ldr/
Goal
Build a ticket decomposition process that converts meaningful Redmine tickets into durable organizational memory: timeline event notes, entity/contact notes, Redmine contact enrichment, cross-links, tags, and gbrain-ingestable vault content.
The immediate wedge is Redmine tickets, especially customer-service, but the final interface should support:
decompose <target>
where <target> may be:
- Redmine issue ID / Redmine project query
- email text
- PDF/document file
- pasted text
- cached ticket JSON
Scope
This plan is about knowledge ingestion and decomposition, not spam closure. It should build on the ticket sync substrate, not replace it.
Existing substrate to reuse:
/home/jason/projects/redmine/redmine_tickets.py/home/jason/projects/redmine/redmine_contacts.py- Redmine issue API with
include=journals,helpdesk - Obsidian LDR vault
- Onyx browser links
- gbrain source/index tooling
Ticket Buckets Worth Decomposing
A ticket should be eligible for decomposition if it appears to fit one or more of these buckets:
1. Definite customer ticket with meaningful event
Customer ticket where:
- a decision was made
- a complaint was involved
- a new project started
- a workflow/process implication was exposed
- an account/service/pricing change happened
Examples:
- customer complaint about missing photos, missing package, wrong condition, wrong item
- pricing/service inquiry that caused a meaningful answer or decision
- customer onboarding/project kickoff
- customer-specific exception or policy decision
2. Durable purchase receipt
Receipt about a durable item or business asset worth remembering.
Examples:
- equipment
- tools
- technology/hardware
- warehouse/office asset
- anything likely to matter for warranty, accounting, maintenance, or future context
3. Regular utility / recurring service receipt or invoice
Receipt/invoice/payment confirmation about a regular service.
Examples:
- utilities
- insurance
- phone/VoIP
- shipping/billing services
- cloud/SaaS subscriptions
- recurring vendors
This may not need rich event notes every time, but should enrich vendor/entity records and optionally create monthly financial/operations timeline entries.
4. Partnership / inbound opportunity / buy-from-us request
Not a customer and not a sales person trying to sell us something, but someone proposing a relationship or wanting to buy something from us.
Examples:
- buy Gaylords
- buy liquidation
- advertise/affiliate/blog partnership
- ask about partnership or marketplace cooperation
- use LDR as a source/supplier
These should become opportunity/entity notes, not spam.
5. Any meaningful operational event
Catch-all for tickets that refer to a meaningful event:
- policy change
- operational incident
- vendor problem
- staff/customer decision
- process drift or known-failure-method recurrence
- new obligation or future follow-up
Non-Goals / Safety Boundaries
- Do not ingest every Redmine ticket into the vault as a polished note. Use cache/archive for raw data; decompose only meaningful tickets.
- Do not overwrite existing entity pages destructively.
- Do not invent facts. Use evidence blocks and confidence levels.
- Do not send customer-visible Helpdesk replies.
- Do not update Redmine contacts until the proposed changes are explicit and reviewable.
- Do not let spam-cleanup closures trigger decomposition unless the ticket also has meaningful non-spam content.
Vault Information Architecture
Timeline root
Create a new timeline tree:
/home/jason/lilaBuild/vaults/obsidian-ldr/Timeline/
Monthly folders:
Timeline/2026-07/
Timeline/2026-08/
Monthly index:
Timeline/2026-07/index.md
Event notes:
Timeline/2026-07/2026-07-09_customer-name-short-subject.md
Filename rules:
- kebab-case subject slug
- no spaces
- prefix
YYYY-MM-DD_ - keep short but human-readable
Example:
Timeline/2026-07/2026-07-09_repackify-gaylord-inquiry.md
Timeline/2026-07/2026-07-09_u2wear-returns-provider-followup.md
Timeline/2026-07/2026-07-09_cyberstorm-order-return-confirmation.md
Monthly index format
Timeline/YYYY-MM/index.md should contain:
# YYYY-MM Timeline Index
## Key events
- [[2026-07-09_repackify-gaylord-inquiry]] — Partnership / buy-from-us inquiry. Redmine #41424.
- [[2026-07-09_u2wear-returns-provider-followup]] — Existing/prospective customer follow-up. Redmine #41448.
## Decisions
- ...
## Complaints / service failures
- ...
## Purchases / receipts
- ...
## Partnerships / opportunities
- ...
Entity notes
Use the existing vault entities/ tree as the default entity home.
Proposed paths:
entities/people/<person-slug>.md
entities/organizations/<organization-slug>.md
entities/concepts/<concept-slug>.md
entities/places/<place-slug>.md
If the repo prefers flat entities/<slug>.md, the first implementation can create flat notes and later migrate. But the decomposition code should centralize path decisions in one function, e.g. entity_note_path(entity).
Entity note frontmatter
Minimum frontmatter for Redmine contacts:
---
type: organization
name: RePackify
redmine_contact_id: 4470
redmine_contact_url: https://rm.ldrprep.com/contacts/4470
onyx_url: https://onyx.int.ldrprep.com/projects/obsidian-ldr/entities/organizations/repackify.md
tags:
- redmine-contact
- maybe-customer
- partnership-opportunity
aliases:
- RePackify
---
For a person:
---
type: person
name: Sarah Johnson
organization: [[repackify]]
redmine_contact_id: 4470
redmine_contact_url: https://rm.ldrprep.com/contacts/4470
onyx_url: https://onyx.int.ldrprep.com/projects/obsidian-ldr/entities/people/sarah-johnson.md
tags:
- redmine-contact
- contact-person
---
Entity body format
# Entity Name
## Summary
Brief factual summary with confidence level.
## Redmine links
- Contact: https://rm.ldrprep.com/contacts/4470
- Related tickets:
- https://rm.ldrprep.com/issues/41424
## Timeline
- [[Timeline/2026-07/2026-07-09_repackify-gaylord-inquiry]]
## Known facts
- Fact with source link.
## Open questions
- Question that needs human/agent follow-up.
Event Note Format
Each event note should be evidence-first and dialectic.
---
type: timeline-event
date: 2026-07-09
source_type: redmine-ticket
redmine_issue_id: 41424
redmine_url: https://rm.ldrprep.com/issues/41424
helpdesk_contact_id: 4470
entities:
- [[repackify]]
tags:
- timeline
- redmine
- partnership-opportunity
- buy-from-us
confidence: medium
---
# 2026-07-09 — RePackify Gaylord Inquiry
## One-line summary
RePackify asked whether LDR has used Gaylord boxes available to purchase.
## Source evidence
- Redmine: https://rm.ldrprep.com/issues/41424
- From: support@repackify.com
- Subject: Gaylord inquiry-RePackify
- Key excerpt:
> We’re interested in purchasing used Gaylord boxes and wanted to see if you have any available.
## Classification
- Bucket: partnership / buy-from-us request
- Customer status: not confirmed customer
- Suggested contact tag: maybe-customer or partnership-opportunity
## Dialectic analysis
### Claim
This is not spam; it is a potential inbound business/opportunity ticket.
### Supporting evidence
- The sender wants to buy something from LDR, not sell LDR a generic service.
- The request references a concrete asset class: used Gaylord boxes.
### Counterpoints / uncertainty
- It may be a one-off opportunistic buyer, not a durable customer relationship.
- Needs human judgment on whether LDR wants to sell Gaylords.
### Decision / outcome
- No final business decision captured yet.
- Needs follow-up if LDR wants a liquidation/Gaylord sale process.
## Entities to update
- [[repackify]]
## Redmine contact enrichment proposal
```json
{
"contact_id": 4470,
"add_tags": ["maybe-customer", "partnership-opportunity", "buy-from-us"],
"obsidian_article_url": "https://onyx.int.ldrprep.com/projects/obsidian-ldr/entities/organizations/repackify.md"
}
Follow-up candidates
- Decide whether LDR sells used Gaylords.
- If yes, define a standard response/process.
---
## Dialectic Reasoning Requirements
Each decomposition should include:
1. **Claim** — what seems to be true or meaningful.
2. **Evidence** — exact ticket excerpts, sender, date, Redmine link.
3. **Counterpoints** — why the interpretation might be wrong.
4. **Decision/outcome** — what was decided or what remains undecided.
5. **Recommended graph updates** — timeline note, entity notes, contact updates, tags.
6. **Confidence** — low/medium/high.
This prevents the vault from becoming a pile of confident hallucinated summaries. A small mercy in a cruel world.
---
## Redmine Contact Cross-Linking
### Vault → Redmine
Entity frontmatter should include:
```yaml
redmine_contact_id: 4470
redmine_contact_url: https://rm.ldrprep.com/contacts/4470
Redmine → Vault
The Redmine contact should be updated with an Onyx URL, not an obsidian:// link.
Preferred URL format:
https://onyx.int.ldrprep.com/projects/obsidian-ldr/<vault-relative-path>.md
Example:
https://onyx.int.ldrprep.com/projects/obsidian-ldr/entities/organizations/repackify.md
Implementation detail:
- If a Redmine contact custom field exists for
Obsidian Article, useredmine_contacts.py --set-custom-field. - If no suitable custom field exists, append a clearly delimited line to contact background/notes, e.g.:
Obsidian article: https://onyx.int.ldrprep.com/projects/obsidian-ldr/entities/organizations/repackify.md
Do not guess custom field IDs. Discover available custom fields from get_contact() payloads first.
decompose <target> Architecture
KarpathyWiki plugin reference
Jason identified the Obsidian karpathywiki plugin as a close match for the desired behavior. Inspection notes are saved at:
/home/jason/projects/redmine/docs/karpathywiki_plugin_inspection.md
Finding: the plugin does not expose a true standalone CLI. It registers Obsidian commands and its WikiEngine depends on Obsidian App/Vault/TFile/metadataCache APIs. However, its logic is highly relevant and portable as a design reference:
- source analysis prompt / JSON schema
- entity vs concept decision tree
- alias handling
- duplicate/semantic entity resolution
- entity/concept/source page generation prompts
- contradiction and related-page fields
- graph-based query/retrieval ideas
Recommended implementation path for Redmine decomposition: build a Python headless mirror integrated with redmine_tickets.py, using KarpathyWiki's prompts/schema as reference. Consider a TypeScript adapter later only if maintaining a Python mirror becomes too costly.
Compatibility contract with the Obsidian KarpathyWiki plugin
The LDR vault already contains a plugin-managed wiki tree:
wiki/entities/
wiki/concepts/
wiki/sources/
wiki/index.md
wiki/log.md
The headless Redmine decomposer must be a companion, not a competing owner of that tree.
Default rules:
- Do not write into
wiki/by default. - Do not edit
wiki/index.mdorwiki/log.md; those are plugin-owned. - Do not overwrite or reformat plugin-generated
wiki/entities/*,wiki/concepts/*, orwiki/sources/*pages. - Write Redmine-derived timeline/event artifacts into LDR-owned paths:
Timeline/YYYY-MM/...entities/organizations/...entities/people/...Processes/Redmine/...
- Use Obsidian-compatible wikilinks so plugin-generated pages and headless-generated pages can link to each other.
- If linking to plugin pages, use explicit paths such as
[[wiki/concepts/returns-management|returns management]]rather than ambiguous bare names. - Use plugin-compatible frontmatter concepts where practical (
type,created,updated,tags,sources,confidence,contradictions) but allow LDR-specific types such astimeline-eventoutsidewiki/. - If we ever want the plugin itself to ingest Redmine-derived material, create a deliberate handoff/export file outside
wiki/first and let the plugin ingest it through Obsidian; do not silently write into plugin-ownedsources/.
This keeps Obsidian’s KarpathyWiki plugin useful in the desktop vault while allowing Redmine decomposition to run headlessly/cron/agent-side without fighting plugin state.
Concrete upstream files to mirror first:
src/wiki/prompts/ingestion.ts— extraction prompt / JSON schemasrc/wiki/prompts/generation.ts— page generation promptssrc/wiki/prompts/merge.ts— merge/update triage promptssrc/wiki/source-analyzer.ts— source analysis normalization/provenance behaviorsrc/wiki/page-factory.ts— slug/alias/page merge behavior
The first Python implementation should not attempt full parity. It should clone only the useful minimum:
- analyze source into JSON entities/concepts/key points/contradictions;
- resolve entity slugs against existing vault files;
- generate or update entity/event notes;
- produce a reviewable contact-update proposal;
- leave graph retrieval to Obsidian/gbrain for now.
Input abstraction
Create a common normalized input model:
@dataclass
class DecompositionSource:
source_type: str # redmine-ticket, email, pdf, text, file
source_id: str | None
title: str
body: str
date: str | None
sender: str | None
redmine_issue_id: int | None
redmine_contact_id: int | None
attachments: list[Path]
metadata: dict[str, Any]
Adapters:
from_redmine_issue(issue_json)from_email_text(raw_text)from_pdf(path)from_plain_text(text)from_file(path)
Decomposition output model
@dataclass
class DecompositionPlan:
source: DecompositionSource
buckets: list[str]
timeline_events: list[TimelineEvent]
entities: list[EntityUpdate]
redmine_contact_updates: list[ContactUpdate]
tags: list[str]
gbrain_actions: list[GBrainAction]
confidence: str
review_required: bool
Apply phases
decompose --dry-run— produce JSON/Markdown plan only.decompose --write-vault— write timeline/entity notes.decompose --update-redmine-contact— update Redmine contact with Onyx article link/tags.decompose --gbrain-sync— run/queue gbrain sync/index.
Each phase should be separately idempotent.
CLI Proposal
Decompose a Redmine ticket
./redmine_tickets.py \
--base-url https://rm.ldrprep.com \
--project customer-service \
decompose \
--issue-id 41424 \
--dry-run
Decompose from cached tickets matching a bucket
./redmine_tickets.py \
--cache-dir .cache/redmine_tickets_prod_meaningful_20260709 \
--project customer-service \
decompose-cached \
--bucket partnership-opportunity \
--dry-run
Decompose arbitrary text
./decompose.py text \
--title "Supplier proposal" \
--input /tmp/message.txt \
--dry-run
Write vault notes after review
./redmine_tickets.py \
--base-url https://rm.ldrprep.com \
--project customer-service \
decompose \
--issue-id 41424 \
--write-vault
gbrain Integration
Initial approach should be simple and explicit:
- Write/update Obsidian notes.
- Run or queue gbrain sync for the LDR vault source.
- Verify that the new notes are searchable.
Commands need source discovery first:
gbrain sources list --json
gbrain stats
Then likely:
gbrain sync --source obsidian-ldr --no-embed --json
gbrain embed --stale
gbrain extract --stale
gbrain search "RePackify Gaylord inquiry"
Do not assume the exact source ID is obsidian-ldr until verified by gbrain sources list --json.
Implementation Phases
Phase 0 — Immediate schema/docs checkpoint
Goal: Preserve this architecture and make future implementation straightforward.
Tasks:
- Save this plan.
- Link it from Redmine
HANDOFF.md. - Optionally create an Obsidian process note at:
/home/jason/lilaBuild/vaults/obsidian-ldr/Processes/Redmine/ticket-decomposition-and-knowledge-ingestion.md
Definition of done:
- Plan exists.
- Handoff links it.
- Next worker can start implementation without transcript context.
Phase 1 — Manual pilot decomposition for 3 tickets
Goal: Prove note structure and cross-linking manually before automating.
Suggested pilot tickets:
- RePackify Gaylord inquiry (
#41424) - u2wear returns-provider follow-up (
#41448) - Snuggle Puppy / 3PL & FBA Returns (
#41504)
Tasks:
- Fetch each ticket with
include=journals,helpdesk. - Write one timeline event note per ticket.
- Create/update entity notes.
- Prepare Redmine contact update payloads, dry-run first.
- Review Onyx links.
- Optionally run gbrain sync/search smoke test.
Definition of done:
- 3 timeline notes exist.
- Monthly index links them.
- Entity notes exist or are updated.
- Redmine contact update payloads are reviewed.
- No customer-visible Redmine replies are sent.
Phase 2 — Implement decomposition dry-run engine
Goal: Generate decomposition plans from cached Redmine tickets without writing vault/Redmine.
Status — 2026-07-10: First deterministic slice implemented in redmine_tickets.py decompose. It supports --issue-id, --input-json, and --format json|markdown. It produces dry-run plans for the three pilot fixtures and respects the KarpathyWiki compatibility boundary by planning writes outside wiki/. A KarpathyWiki-style structured extraction layer now cleans Helpdesk/forwarded-message noise and emits analysis.summary, analysis.entities, analysis.concepts, analysis.key_points, analysis.contradictions, analysis.related_pages, and analysis.clean_text. Tests pass (23 tests). Current limitation: extraction is still deterministic/rule-backed rather than LLM-backed; next slice should add prompt-driven JSON extraction for broader ticket types before vault-write mode.
Files:
- Modify:
/home/jason/projects/redmine/redmine_tickets.py - Add tests:
/home/jason/projects/redmine/tests/test_ticket_decomposition.pyor extendtest_redmine_tickets.py
Tasks:
- Add
DecompositionSourceandDecompositionPlanmodels. - Add
source_from_redmine_issue(issue). - Add bucket classifier:
- customer-decision
- customer-complaint
- customer-new-project
- durable-purchase-receipt
- recurring-utility-receipt
- partnership-buy-from-us
- meaningful-event
- Add dialectic plan generator.
- Add JSON/Markdown dry-run output.
- Add KarpathyWiki-inspired source cleanup and LLM JSON extraction for higher-quality summaries/entities.
Definition of done:
- Unit tests cover each bucket.
- Dry-run output includes source evidence, entities, proposed notes, and proposed contact updates.
- Output quality is comparable to the hand-written pilot notes for the first three fixtures.
Phase 3 — Implement vault writer
Goal: Write idempotent timeline/entity notes.
Tasks:
- Add path builder for
Timeline/YYYY-MM/YYYY-MM-DD_slug.md. - Add monthly index updater.
- Add entity path resolver.
- Add frontmatter merge/update helper.
- Add idempotency marker using source Redmine issue ID in frontmatter.
Definition of done:
- Re-running same decomposition does not duplicate notes or index bullets.
- Existing entity notes are updated, not overwritten.
Phase 4 — Implement Redmine contact updater
Goal: Update Redmine contacts with Onyx article links and useful tags.
Tasks:
- Discover contact custom fields.
- Decide field for Obsidian/Onyx article URL.
- Add dry-run contact update report.
- Add
--apply-contact-updatesusingrebotonly when approved. - Verify contact after update.
Definition of done:
- Contact has vault article URL.
- Vault article has
redmine_contact_id. - Writes are impersonated appropriately.
Phase 5 — gbrain sync/index integration
Goal: Make decomposed notes searchable in gbrain.
Tasks:
- Discover LDR vault gbrain source ID.
- Add optional command wrapper or instruction to run sync/embed/extract.
- Verify search for a newly created entity/event.
Definition of done:
- gbrain search finds the decomposed event/entity.
Phase 6 — Generalize decompose <target>
Goal: Support non-ticket inputs with the same decomposition outputs.
Tasks:
- Add plain-text adapter.
- Add email adapter.
- Add PDF adapter using existing OCR/document pipeline if needed.
- Keep output model the same.
Definition of done:
- Same event/entity/timeline/contact note logic works for non-ticket sources.
Recommended First Execution Step
Do Phase 1 manual pilot before writing more automation.
Use the 3 known “meaningful but not spam” tickets from the recent spam-cleanup work:
#41424 RePackify Gaylord inquiry
#41448 u2wear returns-provider follow-up
#41504 Snuggle Puppy / 3PL & FBA Returns
That gives us immediate signal on:
- note format quality
- entity matching
- Onyx link usefulness
- Redmine contact update shape
- gbrain ingestion friction
Only after that should we automate the decomposition engine. Otherwise we risk building a very elegant machine for generating annoying notes. We have enough of those already.