docs: align issue-core with work-record connector role (ISSUE-WP-0004)
Retarget INTENT, SCOPE, README, ROADMAP, and agent docs from "task landing zone" to external-tracker connector per work-record canon and architecture draft §4.2. Add UUID↔external-id mapping design; mark ISSUE-WP-0004 finished and cross-file activity-core ACTIVITY-WP-0022 for IssueSink policy.
This commit is contained in:
parent
5e556028ab
commit
d60175c354
9 changed files with 760 additions and 226 deletions
287
docs/uuid-external-id-mapping.md
Normal file
287
docs/uuid-external-id-mapping.md
Normal file
|
|
@ -0,0 +1,287 @@
|
|||
# Design: UUID ↔ external-id mapping
|
||||
|
||||
**Status:** design only (ISSUE-WP-0004-T04)
|
||||
**Date:** 2026-07-21
|
||||
**Implementation:** deferred to stage-3 work-record architecture in
|
||||
the-custodian (not this workplan).
|
||||
**Normative product role:** `INTENT.md`, `SCOPE.md`; founder decision
|
||||
`the-custodian/research/WorkOrchestrationArchitectureDraft.md` §4.2;
|
||||
kinds in `the-custodian/canon/standards/work-record-types_v0.1.md`
|
||||
(task kind: *issue-core issues become external projections only*).
|
||||
|
||||
## Problem
|
||||
|
||||
When collaboration requires a third-party tracker (Forgejo/Gitea, GitHub,
|
||||
Jira, …), the fleet still needs a stable link between:
|
||||
|
||||
| Side | Identity today | Owner |
|
||||
| --- | --- | --- |
|
||||
| Internal work record | UUIDv7 written back as `state_hub_*_id` (workplan/task/intake/…) | repo file + state-hub |
|
||||
| External issue | Backend issue id / number (+ URL) | Gitea, GitHub, … via issue-core |
|
||||
|
||||
Without an explicit mapping:
|
||||
|
||||
1. Projections are fire-and-forget — no reverse lookup from issue → work record.
|
||||
2. Emitters invent ad hoc back-references (`task_spawn_log.external_id`,
|
||||
labels, description footers).
|
||||
3. `TaskSpec.triggering_event_id` is overloaded as "some upstream id" and
|
||||
cannot represent work-record identity cleanly.
|
||||
4. Two-way sync at the boundary has nothing durable to key on.
|
||||
|
||||
## Goals
|
||||
|
||||
1. **One mapping row per projection:** work-record UUID ↔ (backend, external id).
|
||||
2. **Mirror the hub write-back pattern:** optional field on the work-record
|
||||
source (or generated index) pointing at the external projection, similar
|
||||
to `state_hub_task_id`.
|
||||
3. **Extend, do not overload, `triggering_event_id`.**
|
||||
4. **Zero load** when no external integration / mapping is configured.
|
||||
5. **Idempotent project:** re-projecting the same UUID does not create
|
||||
duplicate issues.
|
||||
|
||||
## Non-goals (this design)
|
||||
|
||||
- Replacing work records as origin of work.
|
||||
- Making Forgejo the fleet task board.
|
||||
- Implementing the store, CLI, or API in ISSUE-WP-0004.
|
||||
- Full CRDT merge of comments/body (v1 can be status + simple comment
|
||||
push/pull; advanced merge is later).
|
||||
|
||||
## Current traceability (as-is)
|
||||
|
||||
### Ingestion metadata
|
||||
|
||||
`POST /issues/` stores emitter fields under `Issue.sync_metadata.ingestion`:
|
||||
|
||||
```text
|
||||
triggering_event_id # activity event UUID or stable key e.g. "scheduled"
|
||||
source_type # rule | instruction
|
||||
source_id
|
||||
activity_definition_id
|
||||
target_repo
|
||||
```
|
||||
|
||||
See `issue_core/api/ingest.py` and `issue_core/api/schemas.py`.
|
||||
|
||||
### What `triggering_event_id` is
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Purpose | Emitter-side traceability / idempotency key for **activity** spawns |
|
||||
| Type | non-empty string (UUID or stable key) |
|
||||
| Scope | Links to activity-core event or schedule, **not** to a work-record UUID |
|
||||
| Stored on | `sync_metadata.ingestion` of the created issue |
|
||||
|
||||
### What it is not
|
||||
|
||||
- Not a work-record UUID
|
||||
- Not a substitute for `state_hub_task_id` / intake UUID
|
||||
- Not sufficient for "this Forgejo issue projects task X"
|
||||
|
||||
**Decision:** keep `triggering_event_id` for activity-core emit lineage.
|
||||
Add a **separate** identity channel for work records (`work_record_uuid`
|
||||
and durable mapping rows). Do not overload the existing field.
|
||||
|
||||
## Target shape
|
||||
|
||||
### Mapping record
|
||||
|
||||
Conceptual schema (storage TBD — local SQLite table is the natural first
|
||||
home; remote backends may only get labels/footers):
|
||||
|
||||
```yaml
|
||||
mapping:
|
||||
id: <mapping-uuid> # optional row id
|
||||
work_record_uuid: <uuidv7> # primary internal key
|
||||
work_record_id: "ISSUE-WP-0004-T01" # canonical name, optional denorm
|
||||
work_record_kind: task # task | intake | workplan | …
|
||||
backend: gitea # gitea | github | gitlab | jira | sqlite
|
||||
external_id: "176" # backend-native issue id/number
|
||||
external_url: "https://…/issues/176"
|
||||
target_repo: "coulomb/example" # optional routing hint
|
||||
direction: outward # outward | linked (pre-existing issue)
|
||||
status: active # active | detached | superseded
|
||||
created_at: …
|
||||
updated_at: …
|
||||
# optional sync cursors
|
||||
last_pushed_at: …
|
||||
last_pulled_at: …
|
||||
```
|
||||
|
||||
Uniqueness:
|
||||
|
||||
- Unique on `(backend, external_id)` while `status=active`
|
||||
- Unique on `(work_record_uuid, backend)` while `status=active`
|
||||
(one active projection per tracker per work record; multi-tracker later
|
||||
can relax to one per backend type)
|
||||
|
||||
### Work-record side back-reference (optional denorm)
|
||||
|
||||
Mirror hub write-back style so humans/agents see the link without querying
|
||||
issue-core only:
|
||||
|
||||
```yaml
|
||||
# on a task / intake YAML block (illustrative — exact key names TBD with canon)
|
||||
external_tracker:
|
||||
backend: gitea
|
||||
issue_id: "176"
|
||||
issue_url: "https://…"
|
||||
mapped_by: issue-core
|
||||
```
|
||||
|
||||
Alternatively a single string field if canon prefers flat keys:
|
||||
|
||||
```text
|
||||
issue_core_external_ref: "gitea:coulomb/example#176"
|
||||
```
|
||||
|
||||
Canon PR owns the field name; issue-core owns the mapping authority.
|
||||
|
||||
### issue-core side storage
|
||||
|
||||
Recommended v1:
|
||||
|
||||
1. **Local SQLite** table `work_record_issue_map` (even when primary CRUD
|
||||
backend is Gitea) — mapping is fleet bookkeeping, not a Gitea concept.
|
||||
2. **Issue.sync_metadata.mapping** (or top-level) echo for convenience when
|
||||
the issue is loaded:
|
||||
```json
|
||||
{
|
||||
"mapping": {
|
||||
"work_record_uuid": "…",
|
||||
"work_record_id": "…",
|
||||
"work_record_kind": "task"
|
||||
}
|
||||
}
|
||||
```
|
||||
3. Optional Gitea label / body footer for human visibility in the tracker UI
|
||||
(non-authoritative).
|
||||
|
||||
### API / CLI (sketch only)
|
||||
|
||||
```text
|
||||
# Project an existing work record outward
|
||||
issue project <work_record_uuid> [--backend gitea] [--title …]
|
||||
|
||||
# Link an existing external issue to a work record (no create)
|
||||
issue map link <work_record_uuid> <external_id>
|
||||
|
||||
# Resolve
|
||||
issue map show --uuid <work_record_uuid>
|
||||
issue map show --external <external_id> [--backend gitea]
|
||||
|
||||
# Detach
|
||||
issue map detach <work_record_uuid>
|
||||
```
|
||||
|
||||
REST sketch:
|
||||
|
||||
```text
|
||||
POST /mappings/ { work_record_uuid, kind?, create_issue? | external_id? }
|
||||
GET /mappings/?work_record_uuid=…
|
||||
GET /mappings/?backend=gitea&external_id=…
|
||||
DELETE /mappings/{id} # or POST detach
|
||||
```
|
||||
|
||||
`POST /issues/` remains for **intentional** issue creation without a work
|
||||
record. When a work record is known, prefer `POST /mappings/` (or
|
||||
`POST /issues/` with required `work_record_uuid` in a later schema version).
|
||||
|
||||
## Intersection with `TaskSpec` / ingestion
|
||||
|
||||
### Today
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "…",
|
||||
"triggering_event_id": "…",
|
||||
"activity_definition_id": "…",
|
||||
…
|
||||
}
|
||||
```
|
||||
|
||||
### Proposed extension (backward compatible)
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "…",
|
||||
"triggering_event_id": "…",
|
||||
"work_record_uuid": "optional-uuidv7",
|
||||
"work_record_id": "optional-canonical-name",
|
||||
"work_record_kind": "optional-kind",
|
||||
…
|
||||
}
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
| Case | Behavior |
|
||||
| --- | --- |
|
||||
| No `work_record_uuid` | Current behavior: create issue; store ingestion meta; **no mapping row** |
|
||||
| With `work_record_uuid` | Create or reuse issue; **upsert mapping row**; echo UUID in `sync_metadata` |
|
||||
| Same UUID + active mapping | Idempotent: return existing `issue_id`, do not create duplicate |
|
||||
| `triggering_event_id` only | Unchanged meaning (activity lineage) |
|
||||
|
||||
**Supersede vs extend:** **extend**. `triggering_event_id` stays for
|
||||
activity-core. Work-record identity is additive. Long term, emitters that
|
||||
only need fleet work should not call issue-core at all; emitters that
|
||||
project a known work record pass `work_record_uuid`.
|
||||
|
||||
### activity-core implications (out of band)
|
||||
|
||||
- Default sink must not always POST for internal findings (see follow-up
|
||||
workplan filed under ISSUE-WP-0004-T05).
|
||||
- When a rule *does* intentionally project, pass the work-record UUID once
|
||||
intake/task promotion exists — not only the activity event id.
|
||||
|
||||
## Sync semantics (boundary only)
|
||||
|
||||
| Direction | Trigger | Writes |
|
||||
| --- | --- | --- |
|
||||
| Outward | `project` / status change on work record when sync enabled | Update external issue state/labels/comment |
|
||||
| Inward | Tracker webhook or poll when enabled | Update mapping cursors; **optionally** suggest work-record status change via hub/file write tools — never silent mutation of ADR-001 files without a defined writer |
|
||||
|
||||
v1 can implement outward-only + mapping durability; inward sync is a later
|
||||
increment. Until enabled, connector load is zero beyond idle process cost.
|
||||
|
||||
## Failure and edge cases
|
||||
|
||||
- **Unknown UUID:** reject project with 4xx; do not create orphan mapping.
|
||||
- **Backend down:** no mapping row until create succeeds (or store
|
||||
`pending` if a queue is introduced later — not required for v1).
|
||||
- **Issue deleted remotely:** mark mapping `detached`; leave work record
|
||||
intact.
|
||||
- **Multiple backends:** one active mapping per `(uuid, backend)`;
|
||||
documenting multi-tracker projection is future work.
|
||||
- **SQLite-only offline:** mapping table lives with local DB; sync of
|
||||
issues and mappings to remote is coordinated (mapping rows are not
|
||||
pushed as Gitea native objects).
|
||||
|
||||
## Implementation sketch (later workplan)
|
||||
|
||||
1. Schema: `work_record_issue_map` in local backend (+ migration path).
|
||||
2. Domain service: `MappingService` independent of HTTP.
|
||||
3. Wire `project` / `map` CLI + REST.
|
||||
4. Extend `TaskIngestionRequest` with optional work-record fields; tests for
|
||||
idempotent mapping.
|
||||
5. Canon PR for optional back-reference field on work-record YAML.
|
||||
6. activity-core: only emit with UUID when intentional; default sink policy
|
||||
already fixed by its follow-up.
|
||||
|
||||
## Open questions (for implementers)
|
||||
|
||||
1. Exact work-record back-reference key names (canon vs issue-core-owned).
|
||||
2. Whether mapping table is *only* SQLite or also dual-written to state-hub.
|
||||
3. Inward sync writer: who may edit work-record files (agent harness vs
|
||||
fix-consistency-only).
|
||||
4. Whether `kind: intake` projections are allowed before promotion, or only
|
||||
after `intake → task|workplan`.
|
||||
|
||||
## See also
|
||||
|
||||
- `INTENT.md` — connector role
|
||||
- `SCOPE.md` — mapping in scope as target shape
|
||||
- `issue_core/api/schemas.py` — current TaskSpec fields
|
||||
- `issue_core/api/ingest.py` — current ingestion metadata storage
|
||||
- ISSUE-WP-0004 — alignment workplan
|
||||
- activity-core IssueSink follow-up (filed from ISSUE-WP-0004-T05)
|
||||
Loading…
Add table
Add a link
Reference in a new issue