Files
redmine/docs/ticket_decomposition_knowledge_ingestion_plan.md
T
Jason Thistlethwaite 9ee9eb3ec6 Organized
2026-10-17 20:50:35 -04:00

789 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:
```text
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
1. Do not ingest every Redmine ticket into the vault as a polished note. Use cache/archive for raw data; decompose only meaningful tickets.
2. Do not overwrite existing entity pages destructively.
3. Do not invent facts. Use evidence blocks and confidence levels.
4. Do not send customer-visible Helpdesk replies.
5. Do not update Redmine contacts until the proposed changes are explicit and reviewable.
6. 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:
```text
/home/jason/lilaBuild/vaults/obsidian-ldr/Timeline/
```
Monthly folders:
```text
Timeline/2026-07/
Timeline/2026-08/
```
Monthly index:
```text
Timeline/2026-07/index.md
```
Event notes:
```text
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:
```text
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:
```markdown
# 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:
```text
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:
```yaml
---
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:
```yaml
---
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
```markdown
# 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.
```markdown
---
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:
```text
https://onyx.int.ldrprep.com/projects/obsidian-ldr/<vault-relative-path>.md
```
Example:
```text
https://onyx.int.ldrprep.com/projects/obsidian-ldr/entities/organizations/repackify.md
```
Implementation detail:
- If a Redmine contact custom field exists for `Obsidian Article`, use `redmine_contacts.py --set-custom-field`.
- If no suitable custom field exists, append a clearly delimited line to contact background/notes, e.g.:
```text
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:
```text
/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:
```text
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:
1. Do **not** write into `wiki/` by default.
2. Do **not** edit `wiki/index.md` or `wiki/log.md`; those are plugin-owned.
3. Do **not** overwrite or reformat plugin-generated `wiki/entities/*`, `wiki/concepts/*`, or `wiki/sources/*` pages.
4. Write Redmine-derived timeline/event artifacts into LDR-owned paths:
- `Timeline/YYYY-MM/...`
- `entities/organizations/...`
- `entities/people/...`
- `Processes/Redmine/...`
5. Use Obsidian-compatible wikilinks so plugin-generated pages and headless-generated pages can link to each other.
6. If linking to plugin pages, use explicit paths such as `[[wiki/concepts/returns-management|returns management]]` rather than ambiguous bare names.
7. Use plugin-compatible frontmatter concepts where practical (`type`, `created`, `updated`, `tags`, `sources`, `confidence`, `contradictions`) but allow LDR-specific types such as `timeline-event` outside `wiki/`.
8. 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-owned `sources/`.
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 schema
- `src/wiki/prompts/generation.ts` — page generation prompts
- `src/wiki/prompts/merge.ts` — merge/update triage prompts
- `src/wiki/source-analyzer.ts` — source analysis normalization/provenance behavior
- `src/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:
1. analyze source into JSON entities/concepts/key points/contradictions;
2. resolve entity slugs against existing vault files;
3. generate or update entity/event notes;
4. produce a reviewable contact-update proposal;
5. leave graph retrieval to Obsidian/gbrain for now.
### Input abstraction
Create a common normalized input model:
```python
@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
```python
@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
1. `decompose --dry-run` — produce JSON/Markdown plan only.
2. `decompose --write-vault` — write timeline/entity notes.
3. `decompose --update-redmine-contact` — update Redmine contact with Onyx article link/tags.
4. `decompose --gbrain-sync` — run/queue gbrain sync/index.
Each phase should be separately idempotent.
---
## CLI Proposal
### Decompose a Redmine ticket
```bash
./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
```bash
./redmine_tickets.py \
--cache-dir .cache/redmine_tickets_prod_meaningful_20260709 \
--project customer-service \
decompose-cached \
--bucket partnership-opportunity \
--dry-run
```
### Decompose arbitrary text
```bash
./decompose.py text \
--title "Supplier proposal" \
--input /tmp/message.txt \
--dry-run
```
### Write vault notes after review
```bash
./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:
1. Write/update Obsidian notes.
2. Run or queue gbrain sync for the LDR vault source.
3. Verify that the new notes are searchable.
Commands need source discovery first:
```bash
gbrain sources list --json
gbrain stats
```
Then likely:
```bash
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:
1. Save this plan.
2. Link it from Redmine `HANDOFF.md`.
3. Optionally create an Obsidian process note at:
```text
/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:
1. Fetch each ticket with `include=journals,helpdesk`.
2. Write one timeline event note per ticket.
3. Create/update entity notes.
4. Prepare Redmine contact update payloads, dry-run first.
5. Review Onyx links.
6. 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.py` or extend `test_redmine_tickets.py`
Tasks:
1. Add `DecompositionSource` and `DecompositionPlan` models.
2. Add `source_from_redmine_issue(issue)`.
3. Add bucket classifier:
- customer-decision
- customer-complaint
- customer-new-project
- durable-purchase-receipt
- recurring-utility-receipt
- partnership-buy-from-us
- meaningful-event
4. Add dialectic plan generator.
5. Add JSON/Markdown dry-run output.
6. 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:
1. Add path builder for `Timeline/YYYY-MM/YYYY-MM-DD_slug.md`.
2. Add monthly index updater.
3. Add entity path resolver.
4. Add frontmatter merge/update helper.
5. 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:
1. Discover contact custom fields.
2. Decide field for Obsidian/Onyx article URL.
3. Add dry-run contact update report.
4. Add `--apply-contact-updates` using `rebot` only when approved.
5. 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:
1. Discover LDR vault gbrain source ID.
2. Add optional command wrapper or instruction to run sync/embed/extract.
3. 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:
1. Add plain-text adapter.
2. Add email adapter.
3. Add PDF adapter using existing OCR/document pipeline if needed.
4. 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:
```text
#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.